在開發多線程程序時,若每個線程處理的任務互不相關,開發難度相對較低。但在實際場景中,多線程往往需要同時訪問同一個物件或變數,這時若缺乏同步控制,極易導致處理結果與預期不符。本文將深入探討如何實現線程安全的併發訪問,確保多線程環境下數據的一致性與完整性。
當多個線程同時訪問同一物件的方法時,若無同步機制,執行結果將不可預測。使用 synchronized 關鍵字修飾方法,可確保同一時間僅有一個線程能執行該方法,其他線程需等待鎖釋放後方可進入。
public class HasSelfPrivateNum {
private int num = 0;
public void addI(String username) {
try {
if (username.equals("a")) {
num = 100;
System.out.println("a set over!");
Thread.sleep(2000);
} else {
num = 200;
System.out.println("b set over!");
}
System.out.println(username + " num=" + num);
} catch (Exception e) {
e.printStackTrace();
}
}
}
測試時,線程 A 先進入方法並睡眠 2 秒,期間線程 B 修改了 num 為 200,導致線程 A 輸出時讀取到被覆蓋的值,出現 a num=200 的錯誤結果。
synchronized public void addI(String username) {
// 方法體不變
}
執行結果變為:a set over! a num=100 b set over! b num=200,符合預期。這證明 synchronized 方法實質是物件鎖,鎖住的是當前物件實例。不同物件實例調用同步方法互不干擾。
同一物件的兩個同步方法,線程分別調用時仍會串行執行,證明鎖針對的是整個物件,而非單個方法。
若讀取方法未加同步,寫入方法在執行過程中被打斷,讀取線程可能獲取到未寫入完成的中間狀態數據(臟讀)。解決方案是對讀寫方法均加 synchronized。
Java 的內建鎖支援可重入:線程持有鎖時可再次獲取同一鎖,避免死鎖。此特性在繼承關係中同樣生效,子類同步方法可調用父類同步方法。但需注意:同步修飾符不會被繼承,子類覆寫父類同步方法時若未加 synchronized,則失去同步效果。同步方法拋出未捕獲異常時會自動釋放鎖。
同步方法鎖定整個方法,若方法包含耗時操作(如 I/O、複雜計算),會降低併發性能。同步代碼塊 synchronized(obj) { ... } 允許僅對關鍵代碼加鎖,縮小鎖範圍,提升吞吐量。
this、任意共享物件、類實例synchronized(x){} 同步塊時互斥synchronized 方法時同步synchronized(this) 代碼塊時同步將耗時的遠程調用移出同步塊,僅在賦值共享變數時加鎖,兩線程併發耗時從 6 秒降至 3 秒。此外,對不同業務邏輯使用不同鎖對象,可實現方法間的異步並行執行。
類鎖針對類的所有實例生效,實現方式有二:
synchronized 修飾靜態方法synchronized(ClassName.class) 同步代碼塊類鎖與物件鎖互不干擾:靜態同步方法之間串行,但與實例同步方法可並行執行。類鎖同樣具備可重入特性。
鎖定的是物件的內存地址,修改鎖物件的屬性值不會改變鎖的身份,同步機制依然有效。
多線程訪問共享變數時,線程會將主內存變數複製到線程工作內存操作,寫回主內存存在延遲,導致可見性問題。volatile 強制線程每次從主內存讀取最新值,寫入後立即刷新主內存。
關鍵限制:volatile 僅保證可見性,不保證原子性。複合操作(如 count++)在併發下仍會出現數據競爭,需配合 synchronized 或原子類使用。
Java 同步機制核心在於:
實際開發中應根據業務場景選擇:追求簡單正確用同步方法,高併發性能敏感用同步代碼塊縮小鎖粒度,單純狀態標誌可用 volatile。完整代碼示例請參考 GitHub 倉庫 char02 目錄,多線程通訊內容見 char03。