同步機制
資料一致性原則
撰寫多執行緒應用程式時,確保資料一致性的兩大原則:
互斥性 (Mutual Exclusion)
Critical section 裡的程式碼一次只能被一個執行緒執行,即在同一時間只允許一個執行緒訪問該變量。
可見性 (Visibility)
共享資料的值被某執行緒更改時,其他執行緒可及時看見。
volatile 關鍵字
在 Java 裡,每個執行緒有各自的記憶體空間 (working memory),當執行完一段操作後,執行緒會再將剛才使用到的變數的值更新到主記憶體 (main memory) 裡,其他執行緒則可從主記憶體讀取到變數的最新值。
上述特性加快了程式處理的效率,但在多執行緒環境裡卻可能為我們帶來變數可視性 (visibility) 的問題。
當一個變數的讀取和寫入發生在不同的執行緒時,讀取變數的執行緒有時無法及時看到變數的值的改變(被其他執行緒寫入),導致資料不一致。此時,可在變數前加上
volatile,此變數會改為不使用各執行緒的 working memory,永遠從主記憶體做存取與讀寫。synchronized 關鍵字
不同 Thread 可能同時存取同一份資源。synchronized 的目的是控制每次只能有一個 Thread 在使用同一份資源(共同一個物件),另外的 Thread 無法同時使用此同一資源。
程式發生異常時,會自動釋放 Thread 和 Lock,不會導致 Deadlock 發生。但不能讓等待的 Thread Response 中斷,等待的 Thread 會一直等待下去。
鎖物件 (Lock Object)
作為鎖的物件:
synchronized method- 鎖是當前物件實例static synchronized method- 鎖是當前類別的 Class 物件synchronized(obj)- 鎖是括號中指定的物件
synchronized 的限制
- 同一時間只有一個執行緒可以進入 synchronized 區塊
- 無法保證等待中的執行緒獲得 synchronized 區塊的順序
效能考量
- 低開銷:當 synchronized 區塊未被競爭(尚未被鎖定)
- 高開銷:當 synchronized 區塊被競爭(已被其他執行緒鎖定)
volatile vs synchronized
| 特性 | volatile | synchronized |
|---|---|---|
| 互斥性 | 否 | 是 |
| 可見性 | 是 | 是 |
| 適用情境 | 單純的讀寫操作 | 複合操作 |
| 效能 | 較好 | 較差 |
volatile 可以幫助我們寫出更簡潔的程式碼,相較用 synchronized 鎖住某個區塊,因為用 volatile 像是將同步責任交給 JVM,會比我們自己處理更不容易出錯。但如果宣告為 volatile 的變數經常被使用的話,可能導致程式的效能不如鎖住整個區塊。CPU 快取一致性
flowchart LR
subgraph CPU1[CPU Core 1]
C1[Cache]
end
subgraph CPU2[CPU Core 2]
C2[Cache]
end
MM[Main Memory]
C1 <--> MM
C2 <--> MM
可見性問題
當不同 CPU Core 的 Thread 共享同一個 Memory(如 Class),若沒有正確的同步機制,一個執行緒對變數的修改可能無法及時被其他執行緒看到。
Happens-Before 保證
Happens-Before 是一組限制指令重排序的規則,用於避免指令重排序破壞 Java 的可見性保證。
- volatile 的 happens-before 保證:對 volatile 變數的寫入 happens-before 後續對該變數的讀取
- synchronized 的 happens-before 保證:釋放鎖 happens-before 後續獲取同一個鎖
Race Condition
當兩個或多個執行緒以可能導致不正確結果的方式存取相同變數時,就會發生競態條件。
發生條件
- 兩個或多個執行緒同時讀寫相同的變數或資料
- 執行緒使用以下模式存取變數:
- Check-then-act:檢查後執行
- Read-modify-write:修改的值依賴於先前讀取的值
- 執行緒對變數的存取不是原子性的
解決方案
使用 synchronized 區塊確保 mutual exclusion:
CheckThenAct.java
synchronized (lock) {
if (condition) {
// 執行操作
}
}