Java synchronized 關鍵字原理、用法、優(yōu)化與實戰(zhàn)指南
一、synchronized 核心定義
synchronized(同步的)是 Java 的關鍵字,用于實現線程互斥和同步,核心作用:
- 原子性:保證臨界區(qū)代碼(被 synchronized 修飾的代碼)同一時間只有一個線程執(zhí)行,解決復合操作的線程安全問題;
- 可見性:線程釋放鎖時,會將工作內存中的修改刷新到主內存;獲取鎖時,會清空工作內存,從主內存重新加載變量,保證變量值的實時同步;
- 有序性:通過 “鎖的獲取 - 釋放” 規(guī)則,阻止指令重排序(臨界區(qū)代碼按順序執(zhí)行)。
設計初衷:以最簡單的方式解決多線程并發(fā)訪問共享資源的安全問題,無需手動管理鎖的生命周期(JVM 自動加鎖 / 釋放鎖)。
二、synchronized 解決的核心問題
多線程下對共享資源的 “復合操作”(如 i++、修改集合、更新對象屬性)會導致線程安全問題(如數據臟讀、重復寫入、值覆蓋),synchronized 通過 “互斥鎖” 解決該問題:
- 互斥性:同一時刻只有一個線程能進入同步代碼塊 / 方法;
- 排他性:持有鎖的線程可重復進入(可重入鎖),其他線程需等待鎖釋放。
線程安全問題演示(無 synchronized):
public class SyncProblem {
// 共享資源
private int count = 0;
// 非同步方法:多線程下count++會出現值覆蓋
public void increment() {
count++; // 復合操作:讀→加1→寫,非原子性
}
public static void main(String[] args) throws InterruptedException {
SyncProblem demo = new SyncProblem();
// 10個線程,每個執(zhí)行1000次increment
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
demo.increment();
}
}).start();
}
Thread.sleep(2000);
// 預期10000,實際遠小于(如9876)
System.out.println("count最終值:" + demo.count);
}
}三、synchronized 核心用法
synchronized 可修飾實例方法、靜態(tài)方法、代碼塊,不同修飾方式對應不同的 “鎖對象”:
3.1 修飾實例方法(對象鎖)
- 鎖對象:當前實例(
this); - 特點:不同實例的同步方法互不影響,同一實例的同步方法互斥。
修復上面的線程安全問題:
public class SyncInstanceMethod {
private int count = 0;
// synchronized修飾實例方法:鎖對象是this
public synchronized void increment() {
count++;
}
public static void main(String[] args) throws InterruptedException {
SyncInstanceMethod demo = new SyncInstanceMethod();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
demo.increment();
}
}).start();
}
Thread.sleep(2000);
// 預期10000,實際準確
System.out.println("count最終值:" + demo.count);
}
}3.2 修飾靜態(tài)方法(類鎖)
- 鎖對象:當前類的
Class對象(如SyncStaticMethod.class); - 特點:所有實例的該靜態(tài)方法互斥(因為類對象唯一)。
示例:
public class SyncStaticMethod {
private static int staticCount = 0;
// synchronized修飾靜態(tài)方法:鎖對象是SyncStaticMethod.class
public static synchronized void staticIncrement() {
staticCount++;
}
public static void main(String[] args) throws InterruptedException {
// 兩個不同實例,調用靜態(tài)同步方法仍互斥
SyncStaticMethod demo1 = new SyncStaticMethod();
SyncStaticMethod demo2 = new SyncStaticMethod();
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
demo1.staticIncrement();
}
}).start();
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
demo2.staticIncrement();
}
}).start();
Thread.sleep(2000);
// 預期2000,實際準確
System.out.println("staticCount最終值:" + SyncStaticMethod.staticCount);
}
}3.3 修飾代碼塊(自定義鎖對象)
- 鎖對象:手動指定(如
this、類對象、任意非 null 對象); - 特點:粒度更細,僅鎖定關鍵代碼,性能更高(推薦優(yōu)先使用)。
示例 1:鎖當前實例(this)
public class SyncCodeBlockThis {
private int count = 0;
public void increment() {
// 僅鎖定count++,而非整個方法
synchronized (this) {
count++;
}
}
// 其他非同步代碼(不影響)
public void printCount() {
System.out.println("count:" + count);
}
}示例 2:鎖類對象(類鎖)
public class SyncCodeBlockClass {
private static int staticCount = 0;
public void staticIncrement() {
// 鎖類對象,等價于靜態(tài)同步方法
synchronized (SyncCodeBlockClass.class) {
staticCount++;
}
}
}示例 3:鎖自定義對象(私有鎖,推薦)
public class SyncCodeBlockCustom {
private int count = 0;
// 私有鎖對象:避免外部對象干擾(推薦寫法)
private final Object lock = new Object();
public void increment() {
synchronized (lock) {
count++;
}
}
}3.4 關鍵對比:static synchronized 與 synchronized 方法的執(zhí)行邏輯
核心結論
static synchronized(類鎖,鎖對象為類的 Class 對象)和 synchronized(實例鎖,鎖對象為 this)是不同的鎖對象,二者執(zhí)行時互不阻塞。
完整示例代碼
public class SyncStaticVsInstance {
// 計數器:記錄方法執(zhí)行順序
private static int step = 0;
// 1. synchronized修飾實例方法(鎖對象:this)
public synchronized void instanceSyncMethod() {
System.out.println(Thread.currentThread().getName() + " 進入實例同步方法");
try {
// 模擬耗時操作(3秒),放大執(zhí)行差異
Thread.sleep(3000);
step++;
System.out.println(Thread.currentThread().getName() + " 實例同步方法執(zhí)行完成,step=" + step);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 2. static synchronized修飾靜態(tài)方法(鎖對象:SyncStaticVsInstance.class)
public static synchronized void staticSyncMethod() {
System.out.println(Thread.currentThread().getName() + " 進入靜態(tài)同步方法");
try {
// 模擬耗時操作(3秒)
Thread.sleep(3000);
step++;
System.out.println(Thread.currentThread().getName() + " 靜態(tài)同步方法執(zhí)行完成,step=" + step);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
// 創(chuàng)建類實例(用于調用實例方法)
SyncStaticVsInstance demo = new SyncStaticVsInstance();
// 線程1:調用實例同步方法(鎖this)
Thread thread1 = new Thread(() -> demo.instanceSyncMethod(), "線程1");
// 線程2:調用靜態(tài)同步方法(鎖類對象)
Thread thread2 = new Thread(() -> SyncStaticVsInstance.staticSyncMethod(), "線程2");
// 啟動兩個線程(幾乎同時執(zhí)行)
thread1.start();
thread2.start();
}
}執(zhí)行結果(無阻塞,并行執(zhí)行)
線程1 進入實例同步方法
線程2 進入靜態(tài)同步方法
// 等待3秒后,兩個方法同時完成
線程1 實例同步方法執(zhí)行完成,step=1
線程2 靜態(tài)同步方法執(zhí)行完成,step=2
結果分析
- 無阻塞:線程 1 和線程 2 幾乎同時進入各自的同步方法,沒有先后等待 —— 因為實例鎖和類鎖是兩個完全獨立的鎖對象;
- 執(zhí)行時序:兩個方法都耗時 3 秒,但總執(zhí)行時間僅約 3 秒(而非 6 秒),證明二者并行執(zhí)行;
- step 計數:step 最終為 2,說明兩個方法都完成了耗時操作,進一步驗證無阻塞。
擴展:如何讓兩個方法互斥?(統一鎖對象)
若希望實例方法和靜態(tài)方法互斥,需將二者的鎖對象統一為類對象,修改實例方法為 “鎖類對象的同步代碼塊”:
public class SyncUnifiedLock {
private static int step = 0;
// 改造實例方法:鎖類對象(而非this),與靜態(tài)方法互斥
public void instanceSyncMethod() {
synchronized (SyncUnifiedLock.class) { // 統一鎖對象:類對象
System.out.println(Thread.currentThread().getName() + " 進入實例同步方法");
try {
Thread.sleep(3000);
step++;
System.out.println(Thread.currentThread().getName() + " 實例同步方法執(zhí)行完成,step=" + step);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 靜態(tài)同步方法(鎖對象:SyncUnifiedLock.class)
public static synchronized void staticSyncMethod() {
System.out.println(Thread.currentThread().getName() + " 進入靜態(tài)同步方法");
try {
Thread.sleep(3000);
step++;
System.out.println(Thread.currentThread().getName() + " 靜態(tài)同步方法執(zhí)行完成,step=" + step);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
SyncUnifiedLock demo = new SyncUnifiedLock();
Thread thread1 = new Thread(() -> demo.instanceSyncMethod(), "線程1");
Thread thread2 = new Thread(() -> SyncUnifiedLock.staticSyncMethod(), "線程2");
thread1.start();
thread2.start();
}
}改造后執(zhí)行結果(互斥執(zhí)行)
線程1 進入實例同步方法
// 等待3秒后線程1完成
線程1 實例同步方法執(zhí)行完成,step=1
// 線程2才開始執(zhí)行
線程2 進入靜態(tài)同步方法
// 再等待3秒后線程2完成
線程2 靜態(tài)同步方法執(zhí)行完成,step=2
四、synchronized 底層原理(JVM 層面)
4.1 字節(jié)碼層面
synchronized 修飾的代碼塊 / 方法,在字節(jié)碼中會生成 monitorenter(進入鎖)和 monitorexit(釋放鎖)指令:
monitorenter:嘗試獲取鎖(monitor對象的所有權),成功則進入,失敗則阻塞;monitorexit:釋放鎖,喚醒等待隊列中的線程。
4.2 JVM 鎖優(yōu)化(從重量級到輕量級)
Java 6 后,JVM 對 synchronized 做了大量優(yōu)化,使其性能大幅提升,核心優(yōu)化點:
- 偏向鎖:針對單線程重復獲取同一鎖的場景,減少鎖競爭開銷(默認開啟);
- 輕量級鎖:多線程交替獲取鎖(無競爭),通過 CAS 實現,無需操作系統內核態(tài)切換;
- 重量級鎖:多線程競爭激烈,需操作系統介入(阻塞線程),性能較低;
- 鎖消除:JVM 識別到無共享資源的同步代碼,自動移除鎖;
- 鎖粗化:將多次連續(xù)的鎖獲取 / 釋放合并為一次,減少開銷。
鎖升級流程:偏向鎖 → 輕量級鎖 → 重量級鎖(不可逆)。
五、synchronized 在 JDK 源碼中的應用
5.1 集合類(如 Vector、Hashtable)
Vector 的 add()、get() 等方法被 synchronized 修飾,保證線程安全(但性能低,推薦用 CopyOnWriteArrayList):
public class Vector<E> extends AbstractList<E> {
// synchronized修飾實例方法:保證線程安全
public synchronized boolean add(E e) {
modCount++;
ensureCapacityHelper(elementCount + 1);
elementData[elementCount++] = e;
return true;
}
public synchronized E get(int index) {
if (index >= elementCount)
throw new ArrayIndexOutOfBoundsException(index);
return elementData(index);
}
}5.2 Object 類的 wait ()/notify ()
synchronized 是使用 wait()、notify()、notifyAll() 的前提(必須在同步代碼塊 / 方法中調用):
public class WaitNotifyDemo {
private final Object lock = new Object();
public void waitDemo() throws InterruptedException {
synchronized (lock) {
lock.wait(); // 釋放鎖并等待
}
}
public void notifyDemo() {
synchronized (lock) {
lock.notify(); // 喚醒一個等待線程
}
}
}5.3 java.lang.Thread 的 join () 方法
join() 底層通過 synchronized + wait() 實現線程等待:
public class Thread implements Runnable {
public final synchronized void join(long millis) throws InterruptedException {
long base = System.currentTimeMillis();
long now = 0;
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (millis == 0) {
while (isAlive()) {
wait(0); // 等待當前線程結束
}
} else {
// ... 超時等待邏輯
}
}
}六、synchronized 核心特性
6.1 可重入性
synchronized 是可重入鎖:同一線程持有鎖后,可再次進入該鎖的同步代碼(避免死鎖)。
示例:
public class ReentrantSync {
public synchronized void method1() {
System.out.println("進入method1");
method2(); // 同一線程可再次進入同步方法
}
public synchronized void method2() {
System.out.println("進入method2");
}
public static void main(String[] args) {
ReentrantSync demo = new ReentrantSync();
demo.method1();
// 輸出:
// 進入method1
// 進入method2
}
}6.2 不可中斷性
線程獲取鎖失敗時,會進入阻塞狀態(tài),且無法被中斷(直到獲取到鎖);而 Lock 鎖支持可中斷獲取。
七、synchronized vs Lock(ReentrantLock)
| 特性 | synchronized | ReentrantLock(Lock 接口實現) |
|---|---|---|
| 鎖實現 | JVM 層面(內置鎖) | JDK 層面(手動實現) |
| 可重入性 | 支持 | 支持(可指定公平 / 非公平) |
| 中斷性 | 不支持(阻塞不可中斷) | 支持(lockInterruptibly ()) |
| 超時獲取 | 不支持 | 支持(tryLock (long time)) |
| 公平鎖 | 僅非公平鎖 | 可指定公平 / 非公平鎖 |
| 條件變量 | 僅 wait ()/notify ()(單一條件) | 支持多個 Condition(精準喚醒) |
| 性能 | Java 6 后優(yōu)化,與 Lock 接近 | 高并發(fā)下略優(yōu) |
| 使用復雜度 | 簡單(自動加鎖 / 釋放) | 復雜(需手動 unlock (),建議 try-finally) |
八、synchronized 常見面試題 & 易錯點
8.1 面試高頻問題
- synchronized 能保證可見性和有序性嗎?能。釋放鎖時刷新主內存(可見性),鎖的獲取 / 釋放阻止指令重排序(有序性),同時保證原子性。
- synchronized 是公平鎖嗎?不是。默認是非公平鎖(JVM 優(yōu)先喚醒先獲取鎖的線程,但不保證絕對公平),且無法指定為公平鎖。
- synchronized 修飾實例方法和靜態(tài)方法的鎖對象有什么區(qū)別?
- 實例方法:鎖是
this(當前實例); - 靜態(tài)方法:鎖是
類名.class(類對象);兩者互不干擾(鎖對象不同)。
- 實例方法:鎖是
- 為什么 synchronized 是可重入的?JVM 會記錄鎖的持有線程和重入次數,線程再次獲取同一鎖時,僅增加重入次數,釋放時減少次數,次數為 0 時才真正釋放鎖。
- 一個類中同時有 static synchronized 和 synchronized 方法,多線程調用會互斥嗎?不會。因為鎖對象不同(類對象 vs this),只有當二者鎖對象統一時(如都鎖類對象),才會互斥。
8.2 開發(fā)易錯點
- 鎖對象為 null:synchronized 的鎖對象不能為 null(編譯通過,運行拋
NullPointerException); - 鎖粒度太大:將整個方法同步,而非僅同步關鍵代碼,導致性能低下;
- 混淆對象鎖和類鎖:以為靜態(tài)同步方法和實例同步方法互斥(實際不互斥,鎖對象不同);
- 死鎖:多個線程互相持有對方需要的鎖(如線程 A 持有鎖 1,等待鎖 2;線程 B 持有鎖 2,等待鎖 1)。
死鎖示例:
public class DeadLockDemo {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();
public static void main(String[] args) {
// 線程1:持有l(wèi)ock1,等待lock2
new Thread(() -> {
synchronized (lock1) {
System.out.println("線程1持有l(wèi)ock1,等待lock2");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lock2) {
System.out.println("線程1獲取lock2");
}
}
}).start();
// 線程2:持有l(wèi)ock2,等待lock1
new Thread(() -> {
synchronized (lock2) {
System.out.println("線程2持有l(wèi)ock2,等待lock1");
try { Thread.sleep(100); } catch (InterruptedException e) {}
synchronized (lock1) {
System.out.println("線程2獲取lock1");
}
}
}).start();
// 現象:死鎖,兩個線程永遠阻塞
}
}死鎖解決方案:
- 固定鎖的獲取順序(如所有線程先獲取 lock1,再獲取 lock2);
- 使用
Lock的tryLock()超時機制; - 避免嵌套鎖。
總結
synchronized核心作用是保證原子性、可見性、有序性,通過 “互斥鎖” 實現多線程安全;- 三種用法:修飾實例方法(鎖 this)、靜態(tài)方法(鎖類對象)、代碼塊(自定義鎖對象,推薦);
static synchronized(類鎖)和synchronized(實例鎖)鎖對象不同,默認互不阻塞,需統一鎖對象才能實現互斥;- Java 6 后 JVM 對其做了偏向鎖、輕量級鎖等優(yōu)化,性能大幅提升,無需過度擔心 “重量級” 問題;
- 核心邊界:可重入、非公平、不可中斷,鎖對象不能為 null,避免死鎖和過大的鎖粒度。
synchronized 是 Java 并發(fā)編程的基礎,掌握它的原理和用法,是寫出安全、高效并發(fā)代碼的關鍵。日常開發(fā)中,優(yōu)先使用 synchronized(簡單、不易出錯),復雜場景(如公平鎖、可中斷、多條件喚醒)再考慮 Lock。
到此這篇關于Java synchronized 關鍵字原理、用法、優(yōu)化與實戰(zhàn)指南的文章就介紹到這了,更多相關Java synchronized 關鍵字用法內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
如何解決Mybatis-plus中@TableLogic注解失效問題
這篇文章主要介紹了如何解決Mybatis-plus中@TableLogic注解失效問題,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-05-05
詳解Java中的checked異常和unchecked異常區(qū)別
這篇文章主要介紹了詳解Java中的checked異常和unchecked異常區(qū)別,小編覺得挺不錯的,現在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-02-02

