Java synchronized關(guān)鍵字從入門到原理解析

適用對象
本課程適合具備Java基礎(chǔ)語法知識,初步了解多線程概念但尚未系統(tǒng)學(xué)習(xí)并發(fā)編程的開發(fā)者。無論你是準(zhǔn)備面試的求職者,還是希望提升并發(fā)編程能力的工程師,這門課程都將為你打下堅實基礎(chǔ)。
學(xué)習(xí)目標(biāo)
通過兩個課時的系統(tǒng)學(xué)習(xí),你將能夠:
- 掌握 synchronized的三種使用方式及其區(qū)別
- 理解 synchronized解決線程安全問題的核心原理
- 剖析 synchronized底層的Monitor機(jī)制和鎖升級過程
- 區(qū)分 synchronized與volatile、ReentrantLock的適用場景
- 應(yīng)對 面試中關(guān)于synchronized的高頻問題
課程安排
- 第一課時(約40分鐘):基礎(chǔ)概念、使用方式、特性詳解
- 第二課時(約50分鐘):底層原理、鎖升級機(jī)制、對比與實戰(zhàn)
第一課時:synchronized基礎(chǔ)與使用
1.1 從一個線程安全問題開始
讓我們先看一個經(jīng)典問題:兩個線程同時對共享變量執(zhí)行自增操作。
public class Counter {
private int count = 0;
public void increment() {
count++; // 不是原子操作!
}
public int getCount() {
return count;
}
}
// 測試代碼
public class Test {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) counter.increment();
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) counter.increment();
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("最終結(jié)果: " + counter.getCount()); // 期望20000,實際小于20000
}
}運(yùn)行結(jié)果:多次執(zhí)行,結(jié)果總是小于20000,有時甚至相差甚遠(yuǎn)。
問題根源:count++看似一行代碼,但在JVM中分解為三條指令:
- 從主內(nèi)存讀取count到工作內(nèi)存(讀)
- 對count進(jìn)行加1操作(改)
- 將結(jié)果寫回主內(nèi)存(寫)
當(dāng)兩個線程同時執(zhí)行這三步時,可能發(fā)生交替執(zhí)行,導(dǎo)致最后寫入的值覆蓋了之前的計算結(jié)果——這就是典型的競態(tài)條件。
1.2 synchronized是什么?
synchronized是Java提供的一個關(guān)鍵字,譯為“同步”。它可以保證在同一時刻,最多只有一個線程執(zhí)行被它修飾的代碼段,從而解決并發(fā)環(huán)境下的線程安全問題。
從專業(yè)角度說,synchronized實現(xiàn)了互斥訪問,它保證了三個關(guān)鍵特性:
| 特性 | 說明 |
|---|---|
| 原子性 | 被保護(hù)的代碼塊要么全部執(zhí)行,要么完全不執(zhí)行,不會被打斷 |
| 可見性 | 一個線程修改共享變量后,其他線程能立即看到最新值 |
| 有序性 | 禁止編譯器和處理器對同步塊內(nèi)的代碼進(jìn)行指令重排序 |
1.3 初識synchronized的三種用法
synchronized的使用非常靈活,可以根據(jù)需要選擇不同的加鎖對象。下面我們通過三個典型場景來理解它們的區(qū)別。
1.3.1 修飾實例方法
public class SyncMethodDemo {
private int count = 0;
// synchronized修飾實例方法,鎖是當(dāng)前對象this
public synchronized void increment() {
count++;
}
}特點:
- 鎖對象是調(diào)用該方法的實例對象(即this)
- 同一個對象的多個synchronized實例方法,共用同一把鎖
- 不同對象的方法互不干擾
示意圖:
線程A ──> obj1.increment() ──> 獲取obj1鎖 ──> 執(zhí)行 線程B ──> obj1.increment() ──> 等待obj1鎖 線程C ──> obj2.increment() ──> 獲取obj2鎖 ──> 執(zhí)行(與A不沖突)
1.3.2 修飾靜態(tài)方法
public class SyncStaticDemo {
private static int count = 0;
// synchronized修飾靜態(tài)方法,鎖是當(dāng)前類的Class對象
public static synchronized void increment() {
count++;
}
}特點:
- 鎖對象是當(dāng)前類的Class對象(如SyncStaticDemo.class)
- 所有實例對象共享同一把類鎖
- 靜態(tài)方法鎖與實例方法鎖互不干擾
思考題:如果一個類既有synchronized靜態(tài)方法,又有synchronized實例方法,兩個線程分別調(diào)用它們,會互斥嗎?
- 答案:不會。因為一個是類鎖,一個是對象鎖,兩把不同的鎖。
1.3.3 修飾代碼塊
public class SyncBlockDemo {
private Object lock = new Object();
private int count = 0;
public void increment() {
// 同步代碼塊,鎖是指定的lock對象
synchronized (lock) {
count++;
}
}
public void decrement() {
// 也可以鎖this
synchronized (this) {
count--;
}
}
}特點:
- 鎖對象可以任意指定,靈活性最高
- 可以精確控制同步范圍,提高并發(fā)性能
- 常用鎖對象:this、自定義鎖對象、類.class
1.4 深入理解鎖的范圍
1.4.1 三種鎖的對比表格
| 使用形式 | 鎖對象 | 作用范圍 | 典型應(yīng)用場景 |
|---|---|---|---|
| 修飾實例方法 | 當(dāng)前實例對象 | 單個實例內(nèi) | 保護(hù)實例變量 |
| 修飾靜態(tài)方法 | 當(dāng)前類的Class對象 | 所有實例間 | 保護(hù)靜態(tài)變量 |
| 修飾代碼塊 | 指定的任意對象 | 代碼塊內(nèi) | 細(xì)粒度控制 |
1.4.2 常見面試題解析
問題1:synchronized修飾代碼塊可以給類加鎖嗎?
當(dāng)然可以!只要在括號內(nèi)傳入類名.class即可。例如:
synchronized (SyncBlockDemo.class) {
// 這是類鎖
}問題2:構(gòu)造方法可以用synchronized修飾嗎?
不可以!Java語法規(guī)定構(gòu)造方法不能是同步的。實際上,構(gòu)造方法本身就是線程安全的,因為JVM會保證在對象初始化完成前,其他線程無法訪問該對象。
問題3:靜態(tài)同步方法和非靜態(tài)同步方法同時調(diào)用會互斥嗎?
不會互斥。假設(shè)線程A調(diào)用實例對象的非靜態(tài)同步方法,線程B同時調(diào)用該對象所屬類的靜態(tài)同步方法,它們持有的是兩把不同的鎖(對象鎖 vs 類鎖),因此可以并行執(zhí)行。
1.5 synchronized的核心特性
1.5.1 可重入性
概念:同一個線程在持有鎖的情況下,可以再次獲取同一把鎖,而不會被阻塞。
public class ReentrantDemo {
public synchronized void methodA() {
System.out.println("進(jìn)入methodA");
methodB(); // 直接調(diào)用,不會死鎖
}
public synchronized void methodB() {
System.out.println("進(jìn)入methodB");
}
}為什么需要可重入?
如果沒有可重入性,線程在調(diào)用methodA獲取鎖后,調(diào)用methodB時發(fā)現(xiàn)自己還要申請同一把鎖,就會形成死鎖。這顯然不合理。
實現(xiàn)原理:每個鎖關(guān)聯(lián)一個持有線程和一個計數(shù)器。線程第一次獲取鎖時,計數(shù)器置為1;同一線程再次獲取時,計數(shù)器遞增;釋放一次,計數(shù)器遞減。直到計數(shù)器歸零,鎖才真正釋放。
1.5.2 可見性保證
synchronized不僅能保證原子性,還能保證內(nèi)存可見性。JMM(Java內(nèi)存模型)規(guī)定:
- 線程釋放鎖時,會將工作內(nèi)存中的共享變量刷新到主內(nèi)存
- 線程獲取鎖時,會清空工作內(nèi)存,從主內(nèi)存重新讀取共享變量
這就確保了:一個線程修改的共享變量,在釋放鎖后,其他線程能立即看到最新值。
1.6 第一課時小結(jié)
- synchronized是Java內(nèi)置的同步關(guān)鍵字,解決原子性、可見性、有序性問題
- 三種使用方式:實例方法(鎖this)、靜態(tài)方法(鎖Class對象)、代碼塊(鎖任意對象)
- 核心特性:可重入性、可見性、互斥性
- 不同鎖對象決定了不同的同步范圍
第二課時:synchronized原理與優(yōu)化
2.1 從字節(jié)碼看synchronized的本質(zhì)
2.1.1 同步代碼塊的字節(jié)碼
先看一段簡單的同步代碼塊:
public class SyncCodeBlock {
public void doSomething() {
synchronized (this) {
System.out.println("hello");
}
}
}使用javap -c -v反編譯后,關(guān)鍵字節(jié)碼如下:
3: monitorenter // 進(jìn)入同步塊,獲取監(jiān)視器鎖 4: getstatic // 調(diào)用System.out 7: ldc // 加載字符串 9: invokevirtual // 調(diào)用println 12: aload_1 13: monitorexit // 退出同步塊,釋放監(jiān)視器鎖 14: goto 22 // 正常結(jié)束跳轉(zhuǎn) 17: astore_2 // 異常處理開始 18: aload_1 19: monitorexit // 異常時也釋放鎖 20: aload_2 21: athrow // 拋出異常 22: return
關(guān)鍵發(fā)現(xiàn):
- 同步代碼塊使用
monitorenter和monitorexit指令 - 有兩個
monitorexit:一個正常退出,一個異常退出,確保鎖一定被釋放 - 這就是為什么synchronized即使拋出異常也不會死鎖
2.1.2 同步方法的字節(jié)碼
public synchronized void syncMethod() {
System.out.println("hello");
}反編譯結(jié)果:
public synchronized void syncMethod(); descriptor: ()V flags: ACC_PUBLIC, ACC_SYNCHRONIZED // 注意這個標(biāo)志 Code: 0: getstatic #2 3: ldc #3 5: invokevirtual #4 8: return
同步方法沒有monitorenter/monitorexit指令,而是通過方法表flags中的ACC_SYNCHRONIZED標(biāo)識。JVM根據(jù)這個標(biāo)識判斷是否需要獲取鎖。
結(jié)論:兩種實現(xiàn)方式本質(zhì)上都是獲取對象的監(jiān)視器鎖(Monitor Lock),只是表現(xiàn)形式不同。
2.2 Monitor機(jī)制深度剖析
2.2.1 什么是Monitor?
Monitor(監(jiān)視器)是操作系統(tǒng)中用于實現(xiàn)線程同步的機(jī)制。Java中的每個對象都與一個Monitor關(guān)聯(lián),這個關(guān)聯(lián)關(guān)系存儲在對象頭的Mark Word中。
可以把Monitor理解為一個“接待室”,里面有三個關(guān)鍵區(qū)域:
┌─────────────────────┐
│ Owner │ ← 當(dāng)前持有鎖的線程
├─────────────────────┤
│ EntryList │ ← 等待獲取鎖的線程隊列
├─────────────────────┤
│ WaitSet │ ← 調(diào)用了wait()的線程隊列
└─────────────────────┘2.2.2 Monitor的核心數(shù)據(jù)結(jié)構(gòu)
在HotSpot虛擬機(jī)中,Monitor由C++的ObjectMonitor類實現(xiàn),關(guān)鍵字段如下:
| 字段 | 作用 |
|---|---|
_owner | 指向當(dāng)前持有鎖的線程 |
_EntryList | 等待獲取鎖的線程隊列 |
_WaitSet | 調(diào)用了wait()的線程隊列 |
_recursions | 記錄鎖的重入次數(shù) |
_count | 記錄線程獲取鎖的次數(shù) |
2.2.3 鎖獲取和釋放的完整流程
以兩個線程T0和T1競爭鎖為例:
步驟1:線程T0嘗試獲取鎖
- 根據(jù)對象頭找到對應(yīng)的ObjectMonitor
- 檢查
_owner是否為null - 通過CAS操作將
_owner設(shè)為T0,_count設(shè)為1 - T0獲取鎖成功,進(jìn)入同步塊執(zhí)行
步驟2:線程T1嘗試獲取鎖
- 此時
_owner為T0,T1 CAS失敗 - T1會先自旋幾次嘗試(適應(yīng)性自旋)
- 如果T0很快釋放鎖,T1就成功獲取
- 如果T0未釋放,T1進(jìn)入
_EntryList阻塞等待
步驟3:T0釋放鎖
- 執(zhí)行
monitorexit,_count減1 - 若
_count變?yōu)?,_owner設(shè)為null - 喚醒
_EntryList中的線程(通常是隊首線程) - T1被喚醒,重新競爭鎖
2.3 鎖升級:從偏向鎖到重量級鎖
早期Java的synchronized性能較差,被稱為“重量級鎖”。JDK 1.6之后引入了一系列優(yōu)化,讓synchronized的性能大幅提升。這就是著名的鎖升級機(jī)制。
2.3.1 為什么需要鎖升級?
想象一下這些場景:
- 場景A:只有一個線程訪問同步代碼,根本不需要鎖競爭
- 場景B:兩個線程交替訪問,幾乎沒有同時競爭
- 場景C:多個線程激烈競爭,需要操作系統(tǒng)級別的鎖
如果用重量級鎖統(tǒng)一處理所有場景,場景A和B會造成不必要的性能開銷。鎖升級就是讓synchronized能夠根據(jù)競爭激烈程度,動態(tài)調(diào)整鎖的“重量”。
2.3.2 鎖的四種狀態(tài)
從低到高,鎖有四種狀態(tài):
- 無鎖狀態(tài)
- 偏向鎖
- 輕量級鎖
- 重量級鎖
鎖可以升級,但不能降級(除了一次GC清理)。
2.3.3 偏向鎖(Biased Locking)
適用場景:只有一個線程反復(fù)獲取同一把鎖。
原理:
- 第一次獲取鎖時,通過CAS將線程ID記錄到對象頭
- 之后該線程再來時,只需檢查對象頭中是否是自己ID
- 如果是,直接進(jìn)入,無需任何同步操作
示例代碼:
public class BiasedLockDemo {
private static List<Integer> list = new Vector<>(); // Vector的方法都是同步的
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
list.add(i); // 同一線程反復(fù)獲取鎖
}
}
}優(yōu)點:幾乎沒有加鎖開銷,性能極高。
缺點:一旦有其他線程嘗試競爭,偏向鎖立即撤銷并升級。
注意:從JDK 15開始,偏向鎖被標(biāo)記為廢棄,未來可能移除。因為在實際應(yīng)用中,它的性能提升有限,且維護(hù)成本高。
2.3.4 輕量級鎖(Lightweight Locking)
適用場景:兩個線程交替執(zhí)行同步塊,沒有真正的競爭。
原理:
- 線程在自己的棧幀中創(chuàng)建Lock Record,存儲對象頭的拷貝
- 通過CAS嘗試將對象頭指向Lock Record
- 如果成功,獲取輕量級鎖
- 如果失敗,說明有競爭,升級為重量級鎖
輕量級鎖的解鎖:
- 通過CAS將對象頭恢復(fù)為原Mark Word
- 如果恢復(fù)成功,解鎖完成
- 如果失敗,說明已經(jīng)升級為重量級鎖,走重量級鎖解鎖流程
2.3.5 重量級鎖(Heavyweight Locking)
適用場景:多個線程激烈競爭。
原理:
- 線程進(jìn)入
_EntryList隊列阻塞 - 依賴操作系統(tǒng)Mutex Lock實現(xiàn)
- 涉及用戶態(tài)和內(nèi)核態(tài)切換,開銷較大
2.3.6 鎖升級流程總結(jié)
偏向鎖
│
│ 有其他線程嘗試獲取
↓
輕量級鎖
│
│ 有真實競爭
↓
重量級鎖2.4 synchronized與volatile的對比
| 特性 | synchronized | volatile |
|---|---|---|
| 原子性 | ? 保證 | ? 不保證 |
| 可見性 | ? 保證 | ? 保證 |
| 有序性 | ? 保證 | ? 保證(禁止重排序) |
| 用法 | 修飾方法或代碼塊 | 修飾變量 |
| 性能 | 較重(但有優(yōu)化) | 極輕 |
| 適用場景 | 復(fù)合操作 | 單一變量狀態(tài)標(biāo)志 |
volatile適用示例:
public class VolatileDemo {
private volatile boolean flag = true; // 只作為狀態(tài)標(biāo)志
public void stop() {
flag = false; // 簡單寫操作
}
public void run() {
while (flag) {
// 執(zhí)行任務(wù)
}
}
}2.5 synchronized與ReentrantLock的對比
2.5.1 對比表格
| 對比維度 | synchronized | ReentrantLock |
|---|---|---|
| 實現(xiàn)方式 | JVM關(guān)鍵字 | Java API(基于AQS) |
| 鎖釋放 | 自動釋放 | 必須手動unlock |
| 公平性 | 非公平 | 可設(shè)置公平/非公平 |
| 可中斷 | 不支持 | 支持lockInterruptibly() |
| 超時獲取 | 不支持 | 支持tryLock(timeout) |
| 嘗試獲取 | 不支持 | 支持tryLock() |
| 條件變量 | 一個等待集 | 多個Condition |
| 鎖狀態(tài)查詢 | 不支持 | 支持查詢持有線程等 |
2.5.2 如何選擇?
優(yōu)先使用synchronized的情況:
- 同步邏輯簡單,不需要高級功能
- 希望代碼簡潔不易出錯
- JVM持續(xù)優(yōu)化,性能已很好
選擇ReentrantLock的情況:
- 需要公平鎖
- 需要嘗試獲取鎖或超時獲取
- 需要可中斷的鎖獲取
- 需要多個條件變量(如生產(chǎn)者-消費(fèi)者模式)
- 高競爭場景需要更精細(xì)控制
2.5.3 ReentrantLock使用示例
ReentrantLock lock = new ReentrantLock(true); // 公平鎖
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();
// 標(biāo)準(zhǔn)使用模式
lock.lock();
try {
// 臨界區(qū)代碼
while (條件不滿足) {
notEmpty.await(); // 等待
}
// 執(zhí)行操作
notFull.signal();
} finally {
lock.unlock(); // 必須釋放
}2.6 實戰(zhàn):線程安全的單例模式
綜合運(yùn)用synchronized,實現(xiàn)幾種經(jīng)典的單例模式。
2.6.1 餓漢式(線程安全)
public class EagerSingleton {
private static final EagerSingleton INSTANCE = new EagerSingleton();
private EagerSingleton() {}
public static EagerSingleton getInstance() {
return INSTANCE;
}
}2.6.2 懶漢式(同步方法版)
public class LazySingleton {
private static LazySingleton instance;
private LazySingleton() {}
public static synchronized LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}2.6.3 雙重檢查鎖(DCL)
public class DCLSingleton {
// volatile保證可見性和禁止重排序
private static volatile DCLSingleton instance;
private DCLSingleton() {}
public static DCLSingleton getInstance() {
if (instance == null) {
synchronized (DCLSingleton.class) {
if (instance == null) {
instance = new DCLSingleton();
}
}
}
return instance;
}
}為什么需要volatile?instance = new DCLSingleton()不是原子操作,可能發(fā)生指令重排序。如果不加volatile,其他線程可能拿到一個未初始化完成的對象。這是DCL的關(guān)鍵細(xì)節(jié)。
2.7 第二課時小結(jié)
- synchronized底層基于Monitor實現(xiàn),字節(jié)碼層面使用monitorenter/monitorexit或ACC_SYNCHRONIZED
- 鎖升級機(jī)制(偏向鎖→輕量級鎖→重量級鎖)大幅提升了性能
- synchronized保證原子性、可見性、有序性,是全面的同步工具
- 與volatile相比,synchronized更重量但功能更全
- 與ReentrantLock相比,synchronized簡單易用,ReentrantLock功能更豐富
- 實際開發(fā)中,根據(jù)場景選擇合適的同步機(jī)制
課程總結(jié)
通過兩個課時的學(xué)習(xí),我們?nèi)嬲莆樟藄ynchronized關(guān)鍵字:
- 第一課時:從線程安全問題出發(fā),學(xué)習(xí)了synchronized的三種使用方式、鎖的范圍、可重入性等基礎(chǔ)概念
- 第二課時:深入底層,剖析了Monitor機(jī)制、鎖升級流程,并對比了volatile和ReentrantLock
synchronized作為Java最基礎(chǔ)的同步工具,雖然簡單,但背后的原理并不簡單。理解它的實現(xiàn)機(jī)制,不僅能幫助我們寫出更正確的并發(fā)程序,還能在面試中脫穎而出。
課后思考題
- 如果一個線程在同步塊中拋出異常,鎖會自動釋放嗎?為什么?
- 偏向鎖在JDK 15中被廢棄的原因是什么?談?wù)勀愕睦斫狻?/li>
- 如何用synchronized實現(xiàn)一個阻塞隊列?
- 為什么說synchronized是“悲觀鎖”,而CAS是“樂觀鎖”?
到此這篇關(guān)于Java synchronized關(guān)鍵字詳解:從入門到原理(兩課時)的文章就介紹到這了,更多相關(guān)Java synchronized關(guān)鍵字內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Spring boot如何通過@Scheduled實現(xiàn)定時任務(wù)及多線程配置
這篇文章主要介紹了Spring boot如何通過@Scheduled實現(xiàn)定時任務(wù)及多線程配置,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2019-12-12
最新Spring Security的基于內(nèi)存用戶認(rèn)證方式
本文講解SpringSecurity內(nèi)存認(rèn)證配置,適用于開發(fā)、測試等場景,通過代碼創(chuàng)建用戶及權(quán)限管理,支持密碼加密,雖簡單但不持久化,生產(chǎn)環(huán)境推薦使用數(shù)據(jù)庫或JWT等認(rèn)證方式,感興趣的朋友一起看看吧2025-07-07
Java實現(xiàn)在線五子棋對戰(zhàn)游戲(人機(jī)對戰(zhàn))
這篇文章主要為大家詳細(xì)介紹了如何利用Java語言實現(xiàn)在線五子棋對戰(zhàn)游戲(人機(jī)對戰(zhàn)),文中的實現(xiàn)步驟講解詳細(xì),感興趣的可以嘗試一下2022-09-09
JDBC利用C3P0數(shù)據(jù)庫連接池連接數(shù)據(jù)庫
這篇文章主要為大家詳細(xì)介紹了JDBC利用C3P0數(shù)據(jù)庫連接池連接數(shù)據(jù)庫,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-08-08
Java Socket+多線程實現(xiàn)多人聊天室功能
這篇文章主要為大家詳細(xì)介紹了Java Socket+多線程實現(xiàn)多人聊天室功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-07-07

