在 Java 并發編程中,“線程安全” 是核心議題之一。本文將深入講解線程安全的實現手段、
synchronized
的使用方式、可重入鎖、死鎖的成因與避免、wait/notify
通信機制等,并配合實際代碼案例,幫助你徹底搞懂 Java 線程協作機制。
一、線程安全與加鎖機制
1. synchronized 的使用方式
synchronized
是 Java 最基本的加鎖工具,保證代碼塊在多個線程中“互斥”執行。
① 修飾普通方法(鎖的是當前實例 this
)
public synchronized void syncMethod() {// 線程安全的邏輯
}
② 修飾靜態方法(鎖的是當前類的 .class
對象)
public synchronized static void staticSyncMethod() {// 靜態同步邏輯
}
③ 修飾代碼塊(可以靈活選擇鎖對象)
public void method() {synchronized (this) {// 同步邏輯}
}
2. 鎖競爭與鎖沖突
-
同一對象加鎖:多個線程競爭同一把鎖,會造成阻塞等待(鎖沖突)。
-
不同對象加鎖:互不干擾,線程可并發執行。
Runnable task = () -> {synchronized (lockObject) {// 臨界區代碼}
};
二、可重入性:不會死鎖的“重復加鎖”
Java 的 synchronized
是可重入鎖。也就是說,一個線程可以多次獲得同一把鎖,不會導致死鎖。
public synchronized void outer() {inner(); // 同一線程再次進入 synchronized 方法
}public synchronized void inner() {// 安全執行
}
三、死鎖問題與避免
死鎖產生的典型場景
1. 兩個線程兩把鎖,互相等待對方釋放
class DeadlockExample {private final Object lock1 = new Object();private final Object lock2 = new Object();public void task1() {synchronized (lock1) {System.out.println("Task1 獲得了lock1");try { Thread.sleep(100); } catch (InterruptedException ignored) {}synchronized (lock2) {System.out.println("Task1 獲得了lock2");}}}public void task2() {synchronized (lock2) {System.out.println("Task2 獲得了lock2");try { Thread.sleep(100); } catch (InterruptedException ignored) {}synchronized (lock1) {System.out.println("Task2 獲得了lock1");}}}
}
?這個例子滿足死鎖的 4 個必要條件,其中最核心的是“循環等待”。
死鎖避免策略
-
統一加鎖順序:總是先加 lock1,再加 lock2,避免循環依賴。
-
使用
tryLock()
+ 超時機制(需使用ReentrantLock
)。
四、線程通信:wait 和 notify 的正確使用方式
使用wait()
和 notify()
方法。
-
wait()
:線程 自愿等待,進入“暫停”狀態,直到被別人叫醒。 -
notify()
:叫醒一個正在等待的線程。 -
notifyAll()
:叫醒所有等待的線程(但只有一個能拿到鎖繼續執行)。
使用場景舉例:先執行線程 t1 的一部分,再由線程 t2 接力。
class Task {private final Object lock = new Object();private boolean ready = false;public void part1() {synchronized (lock) {System.out.println("T1 正在執行前半部分任務");try { Thread.sleep(1000); } catch (InterruptedException ignored) {}ready = true;lock.notify(); // 喚醒 T2}}public void part2() {synchronized (lock) {while (!ready) {try {lock.wait(); // 主動釋放鎖并阻塞} catch (InterruptedException ignored) {}}System.out.println("T2 收到通知,繼續執行后續任務");}}
}public class WaitNotifyDemo {public static void main(String[] args) {Task task = new Task();Thread t1 = new Thread(task::part1);Thread t2 = new Thread(task::part2);t2.start(); // T2 先 waitt1.start(); // T1 后 notify}
}
與
join()
和sleep()
相比,wait/notify
更靈活,支持提前喚醒和條件控制。
wait 的底層流程
-
釋放鎖
-
阻塞等待
-
被喚醒后重新競爭鎖
-
重新獲取鎖并繼續執行
notify 與 notifyAll 的區別
-
notify()
:隨機喚醒一個正在wait()
的線程。 -
notifyAll()
:喚醒所有等待線程,但只有一個能成功獲得鎖。
五、volatile 與內存可見性
在多線程環境中,每個線程可能并不直接操作主內存中的變量,而是從主內存讀取變量到自己的緩存中進行操作。這就可能出現這樣的情況:
-
一個線程修改了變量的值,但另一個線程看不到這個變化(因為仍在用舊的緩存)。
-
導致線程間的通信出現“看不見的修改”。
這就是內存可見性問題。
示例代碼
public class VisibilityProblem {private static boolean running = true;public static void main(String[] args) {Thread t = new Thread(() -> {while (running) {// 執行代碼}System.out.println("線程停止");});t.start();try { Thread.sleep(1000); } catch (InterruptedException ignored) {}running = false; // 主線程修改 runningSystem.out.println("主線程修改 running 為 false");}
}
可能結果:
即使主線程已經把 running
改為 false
,t
線程可能還一直在死循環,因為它使用的是本地緩存值而不是主內存的值。
解決方式:使用 volatile
private static volatile boolean running = true;
?一旦使用 volatile
修飾變量,修改后的值會立刻刷新到主內存,并且所有線程每次訪問變量時都會從主內存讀取,從而保證了內存可見性。
結語
Java 多線程的本質是對“共享資源 + 并發訪問”下的一種控制與協作。理解 synchronized 的使用方式、死鎖的本質、以及 wait/notify 的協作機制,能有效幫助我們寫出更安全、靈活的并發程序。