Java 同步關(guān)鍵字 synchronized用法 、場景及避坑指南
Java 同步關(guān)鍵字 synchronized:用法 + 場景 + 避坑指南
一、介紹
在 Java 中,synchronized 是 內(nèi)置的線程同步機制,用于解決多線程并發(fā)訪問共享資源時的線程安全問題(如競態(tài)條件、數(shù)據(jù)不一致)。其核心原理是 通過 “鎖” 控制多個線程對共享資源的互斥訪問,確保同一時刻只有一個線程能執(zhí)行臨界區(qū)代碼。
二、主要特點
- 互斥性:同一時刻,只有一個線程能持有鎖并執(zhí)行臨界區(qū)代碼(其他線程需阻塞等待鎖釋放)。
- 可見性:鎖釋放時,線程對共享變量的修改會被 “刷新” 到主內(nèi)存;其他線程獲取鎖時,會從主內(nèi)存重新讀取變量(避免緩存一致性問題)。
- 有序性:禁止指令重排序(臨界區(qū)代碼在多線程視角下按順序執(zhí)行)。
舉個例子:
synchronized 就像一個 “共享資源的專屬門衛(wèi)”:
- 它守著一個 “資源入口”(臨界區(qū));
- 每個想用資源的 “線程”,都得先跟門衛(wèi)要 “通行證”(獲取鎖);
- 門衛(wèi)只給一個人發(fā)通行證,其他人只能在門口排隊(阻塞);
- 拿到通行證的人用完資源后,必須把通行證還給門衛(wèi)(釋放鎖),下一個排隊的人才能拿到;
- 門衛(wèi)還認人(可重入性):同一個人再要通行證,直接給,不用排隊;
- 門衛(wèi)還能靈活守 “小門”(代碼塊)或 “大門”(整個方法):守小門效率高,守大門簡單但容易堵。
synchronized 的核心作用 ——讓多線程 “排隊使用共享資源”,避免混亂和沖突。
三、使用場景(3 種形式)
synchronized 可修飾 方法 或 代碼塊,本質(zhì)是對 “鎖對象” 加鎖,鎖的粒度決定同步范圍。
1. 修飾實例方法(對象鎖)
- 鎖對象:當前 實例對象(this)。
- 效果:同一實例的多個線程,競爭同一把鎖;不同實例的線程互不影響(各自持有自己的鎖)。
public class SynchronizedDemo {
// 實例方法加鎖:鎖是當前對象(this)
public synchronized void instanceMethod() {
// 臨界區(qū):共享資源操作(如修改實例變量)
System.out.println(Thread.currentThread().getName() + " 執(zhí)行實例方法");
try { Thread.sleep(1000); } catch (InterruptedException e) {}
}
public static void main(String[] args) {
SynchronizedDemo demo = new SynchronizedDemo();
// 兩個線程競爭 demo 實例的鎖,串行執(zhí)行
new Thread(demo::instanceMethod, "線程1").start();
new Thread(demo::instanceMethod, "線程2").start(); // 等待線程1釋放鎖
}
}2. 修飾靜態(tài)方法(類鎖)
- 鎖對象:當前類的 Class 對象(類的唯一全局鎖)。
- 效果:所有該類的實例(無論多少個對象)共享同一把鎖,多線程競爭類鎖時串行執(zhí)行。
public class SynchronizedDemo {
// 靜態(tài)方法加鎖:鎖是 SynchronizedDemo.class
public static synchronized void staticMethod() {
System.out.println(Thread.currentThread().getName() + " 執(zhí)行靜態(tài)方法");
try { Thread.sleep(1000); } catch (InterruptedException e) {}
}
public static void main(String[] args) {
SynchronizedDemo demo1 = new SynchronizedDemo();
SynchronizedDemo demo2 = new SynchronizedDemo();
// 兩個線程競爭同一把類鎖(SynchronizedDemo.class),串行執(zhí)行
new Thread(demo1::staticMethod, "線程A").start();
new Thread(demo2::staticMethod, "線程B").start(); // 等待線程A釋放鎖
}
}3. 修飾代碼塊(顯式指定鎖對象)
- 鎖對象:可自定義(任意 非 null 對象,如
this、Class 對象、自定義對象)。 - 效果:僅對代碼塊內(nèi)的邏輯加鎖,粒度更細(推薦,減少鎖競爭開銷)。
public class SynchronizedDemo {
private final Object lock = new Object(); // 自定義鎖對象(推薦用 final,避免鎖對象被修改)
private int count = 0;
public void syncBlock() {
// 1. 自定義對象鎖:鎖定 lock 對象
synchronized (lock) {
count++;
System.out.println(Thread.currentThread().getName() + ":count=" + count);
}
// 2. 實例鎖(等價于修飾實例方法)
synchronized (this) {
// 臨界區(qū)邏輯
}
// 3. 類鎖(等價于修飾靜態(tài)方法)
synchronized (SynchronizedDemo.class) {
// 臨界區(qū)邏輯
}
}
public static void main(String[] args) {
SynchronizedDemo demo = new SynchronizedDemo();
// 多線程競爭 lock 對象,count 自增線程安全
for (int i = 0; i < 3; i++) {
new Thread(demo::syncBlock, "線程" + i).start();
}
}
}舉個示例:
假設某電商平臺有一款爆款商品,庫存僅剩 10 件。同時有 100 個用戶搶購(每個用戶對應一個線程),每個用戶搶購 1 件商品。要求:
- 庫存不能為負數(shù);
- 最終賣出的商品數(shù)量 = 初始庫存(不能多賣,也不能少賣)。
如果不加 synchronized,會出現(xiàn) “超賣”(比如庫存 10,但賣出 12 件);加了 synchronized 后,能保證線程安全,精準扣減庫存。
不加synchronized(線程不安全,會超賣)
public class StockDemo {
// 商品初始庫存:10件
private int stock = 10;
// 搶購方法(未加鎖)
public void buy() {
// 1. 檢查庫存是否充足
if (stock > 0) {
// 模擬網(wǎng)絡延遲(放大線程安全問題)
try { Thread.sleep(10); } catch (InterruptedException e) {}
// 2. 庫存扣減
stock--;
// 3. 打印搶購結(jié)果
System.out.println(Thread.currentThread().getName() + " 搶購成功!剩余庫存:" + stock);
} else {
System.out.println(Thread.currentThread().getName() + " 搶購失敗!庫存不足");
}
}
public static void main(String[] args) {
StockDemo stock = new StockDemo();
// 100個用戶同時搶購(100個線程)
for (int i = 0; i < 100; i++) {
new Thread(stock::buy, "用戶" + (i + 1)).start();
}
}
}結(jié)果:
用戶1 搶購成功!剩余庫存:9
用戶2 搶購成功!剩余庫存:8
...
用戶10 搶購成功!剩余庫存:0
用戶11 搶購成功!剩余庫存:-1 // 超賣了!庫存為負數(shù)
用戶12 搶購成功!剩余庫存:-2 // 繼續(xù)超賣
問題原因:多個線程同時進入 if (stock > 0) 判斷(比如庫存還剩 1 時,5 個線程同時通過判斷),之后都執(zhí)行 stock--,導致庫存被多次扣減,出現(xiàn)負數(shù)。
加synchronized(線程安全,無超賣)
給 buy() 方法加 synchronized,或給 “庫存檢查 + 扣減” 的臨界區(qū)加鎖,保證同一時刻只有一個線程能執(zhí)行核心邏輯:
public class StockDemo {
private int stock = 10;
// 方案1:修飾實例方法(鎖對象是當前 StockDemo 實例)
public synchronized void buy() {
if (stock > 0) {
try { Thread.sleep(10); } catch (InterruptedException e) {}
stock--;
System.out.println(Thread.currentThread().getName() + " 搶購成功!剩余庫存:" + stock);
} else {
System.out.println(Thread.currentThread().getName() + " 搶購失??!庫存不足");
}
}
// 方案2:修飾代碼塊(鎖對象自定義,粒度更細,推薦)
/*
public void buy() {
synchronized (this) { // 鎖當前實例,也可以用自定義鎖對象(如 private final Object lock = new Object();)
if (stock > 0) {
try { Thread.sleep(10); } catch (InterruptedException e) {}
stock--;
System.out.println(Thread.currentThread().getName() + " 搶購成功!剩余庫存:" + stock);
} else {
System.out.println(Thread.currentThread().getName() + " 搶購失??!庫存不足");
}
}
}
*/
public static void main(String[] args) {
StockDemo stock = new StockDemo();
// 100個用戶同時搶購
for (int i = 0; i < 100; i++) {
new Thread(stock::buy, "用戶" + (i + 1)).start();
}
}
}結(jié)果:
用戶1 搶購成功!剩余庫存:9
用戶2 搶購成功!剩余庫存:8
...
用戶10 搶購成功!剩余庫存:0
用戶11 搶購失敗!庫存不足
用戶12 搶購失?。齑娌蛔?br />...
用戶100 搶購失?。齑娌蛔?/p>
擴展:如果是多實例場景?
如果電商平臺部署了多個服務實例(每個實例都是一個 StockDemo 對象),此時 synchronized(對象鎖 / 類鎖)只能保證 “單個實例內(nèi)的線程安全”,無法跨實例同步(比如實例 1 的庫存 10,實例 2 的庫存 10,可能導致總超賣 20 件)。
這種情況需要 分布式鎖(如 Redis 分布式鎖、ZooKeeper 分布式鎖),本質(zhì)是把 “鎖” 放到多個實例都能訪問的公共地方(如 Redis),實現(xiàn)跨實例的互斥。
但單實例內(nèi)的并發(fā)安全,synchronized 完全能搞定,且簡單高效。
四、鎖的實現(xiàn)原理(JVM 層面)
synchronized 早期基于 重量級鎖(依賴操作系統(tǒng)內(nèi)核的互斥量 mutex,切換成本高),JDK 6 后引入 鎖優(yōu)化(偏向鎖、輕量級鎖、重量級鎖),通過 對象頭(Mark Word) 存儲鎖狀態(tài):
1. 對象頭(Mark Word)結(jié)構(gòu)
Java 對象在內(nèi)存中分為 3 部分:對象頭、實例數(shù)據(jù)、對齊填充。其中 Mark Word 是實現(xiàn)鎖的核心,存儲內(nèi)容隨鎖狀態(tài)變化:
| 鎖狀態(tài) | Mark Word 存儲內(nèi)容 |
|---|---|
| 無鎖 | 對象哈希碼、GC 分代年齡、是否偏向鎖(0) |
| 偏向鎖 | 偏向線程 ID、GC 分代年齡、是否偏向鎖(1) |
| 輕量級鎖 | 指向線程棧中鎖記錄(Lock Record)的指針 |
| 重量級鎖 | 指向 JVM 中監(jiān)視器鎖(Monitor)的指針 |
2. 鎖升級流程(從低開銷到高開銷)
JVM 按 “競爭程度” 動態(tài)升級鎖,避免不必要的性能損耗:
- 偏向鎖:單線程場景下,線程獲取鎖后,Mark Word 記錄線程 ID,后續(xù)無需再次競爭(直接復用鎖),開銷極低。
- 輕量級鎖:多線程交替執(zhí)行臨界區(qū)(無激烈競爭),線程通過 CAS(原子操作) 將 Mark Word 替換為自己的鎖記錄指針,避免內(nèi)核態(tài)切換。
- 重量級鎖:多線程同時競爭鎖(激烈競爭),CAS 失敗的線程阻塞等待(依賴操作系統(tǒng) mutex),開銷最高。
五、關(guān)鍵特性
- 可重入性:同一線程可多次獲取同一把鎖(不會死鎖)。例如:
public synchronized void methodA() {
methodB(); // 同一線程再次獲取當前對象鎖,允許執(zhí)行
}
public synchronized void methodB() {}
- 原理:鎖記錄中維護 重入計數(shù)器,線程首次獲取鎖時計數(shù)器 = 1,再次獲取時計數(shù)器 + 1,釋放時計數(shù)器 - 1,直至為 0 釋放鎖。
- 不可中斷性:線程獲取鎖時(如重量級鎖),若鎖被占用,線程會進入 BLOCKED 狀態(tài),無法被中斷(需等待鎖釋放)。
- 非公平鎖:默認是 非公平鎖(線程釋放鎖后,等待隊列中的線程和新請求鎖的線程競爭,新線程可能 “插隊”),無法直接改為公平鎖(需借助
ReentrantLock)。
六、與 Lock 接口的對比
synchronized 是 JVM 內(nèi)置鎖,java.util.concurrent.locks.Lock 是 API 層面的鎖,核心差異如下:
| 特性 | synchronized | Lock(如 ReentrantLock) |
|---|---|---|
| 鎖實現(xiàn) | JVM 內(nèi)置(C++ 實現(xiàn)) | Java 代碼實現(xiàn)(API 層面) |
| 鎖類型 | 非公平鎖(默認) | 可公平 / 非公平(構(gòu)造函數(shù)指定) |
| 可中斷性 | 不可中斷(BLOCKED 狀態(tài)) | 可中斷(lockInterruptibly()) |
| 超時獲取 | 無(只能無限等待) | 支持超時獲?。?code>tryLock(long, TimeUnit)) |
| 條件變量 | 隱式(通過 wait()/notify()) | 顯式(Condition 對象,支持多條件) |
| 鎖釋放 | 自動釋放(方法退出 / 代碼塊結(jié)束) | 必須手動釋放(unlock(),需在 finally 中) |
| 性能 | 優(yōu)化后(偏向 / 輕量級鎖)接近 Lock | 高并發(fā)下更靈活(如非阻塞獲取) |
七、使用注意事項
- 鎖粒度不宜過大:避免對整個方法加鎖(尤其是包含非臨界區(qū)邏輯時),優(yōu)先使用代碼塊鎖定最小臨界區(qū)(減少鎖競爭)。
- 鎖對象不可變:代碼塊鎖的對象需用
final修飾(避免鎖對象被重新賦值,導致多線程持有不同鎖,失去同步效果)。 - 避免死鎖:
- 多個線程獲取鎖的順序保持一致(如線程 1 先鎖 A 再鎖 B,線程 2 也先鎖 A 再鎖 B);
- 避免鎖嵌套過深;
- 可使用超時鎖(
Lock.tryLock())替代synchronized防止死鎖。
- 避免鎖競爭激烈場景:
- 高并發(fā)下,
synchronized重量級鎖的性能較差,可考慮用ConcurrentHashMap等并發(fā)容器,或通過分段鎖、無鎖編程(CAS)優(yōu)化。
- 高并發(fā)下,
八、總結(jié)
synchronized 是 Java 最基礎、最常用的線程同步方式,優(yōu)點是 簡單易用、無需手動釋放鎖、JVM 自動優(yōu)化,適合解決大多數(shù)線程安全問題(如簡單的共享變量修改、方法同步)。其核心是通過 “鎖對象” 實現(xiàn)互斥訪問,結(jié)合 JDK 6 后的鎖升級機制,在低競爭場景下性能優(yōu)異,高競爭場景下可考慮用 Lock 接口或并發(fā)容器替代,若是多實例場景使用分布式鎖。
到此這篇關(guān)于Java 同步關(guān)鍵字 synchronized:用法 + 場景 + 避坑指南的文章就介紹到這了,更多相關(guān)Java 關(guān)鍵字 synchronized內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot詳解MySQL如何實現(xiàn)讀寫分離
當響應的瓶頸在數(shù)據(jù)庫的時候,就要考慮數(shù)據(jù)庫的讀寫分離,當然還可以分庫分表,那是單表數(shù)據(jù)量特別大,當單表數(shù)據(jù)量不是特別大,但是請求量比較大的時候,就要考慮讀寫分離了.具體的話,還是要看自己的業(yè)務...如果還是很慢,那就要分庫分表了...我們這篇就簡單講一下讀寫分離2022-09-09
Mybatis XML文件中使用CDATA標簽的實現(xiàn)
本文主要介紹了Mybatis XML文件中使用CDATA標簽,文中通過示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2026-03-03
Java之注解@Data和@ToString(callSuper=true)解讀
在使用Lombok庫的@Data注解時,若子類未通過@ToString(callSuper=true)注明包含父類屬性,toString()方法只打印子類屬性,解決方法:1. 子類重寫toString方法;2. 子類使用@Data和@ToString(callSuper=true),父類也應使用@Data2024-11-11
vue+springboot項目上傳部署tomcat的方法實現(xiàn)
本文主要介紹了vue+springboot項目上傳部署tomcat的方法實現(xiàn),包括環(huán)境準備、配置調(diào)整以及部署步驟,文中通過圖文及示例代碼介紹的非常詳細,對大家的學習或者工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧2024-01-01

