一把鎖的兩種承諾:synchronized如何同時保證互斥與內存可見性?
在多線程環境中,?臨界區(Critical Section)是指一次只能由一個線程執行的代碼段,這些代碼通常涉及對共享資源(如變量、數據結構、文件或數據庫連接)的訪問或修改。臨界區的存在是為了解決并發控制中的兩大核心問題。
? 1)數據不一致性?:如果多個線程同時對共享資源進行寫操作,可能會破壞數據的完整性,導致其狀態與預期不符。
? 2)競態條件:程序的執行結果依賴于線程調度和執行的偶然順序,這使得程序行為變得不可預測,難以調試。

為了保護臨界區,Java提供了多種互斥(Mutual Exclusion)機制,其中synchronized關鍵字是最常用且強大的工具之一。
synchronized實現互斥的基礎是Java中的每一個對象都可以作為鎖,這個鎖是排他的,在任意時刻只有兩種狀態:被占用和未被占用。當線程請求一個由其他線程持有的鎖時,請求的線程會被阻塞,直到鎖被釋放。這種機制確保了在任何時刻,只有一個線程能夠進入臨界區執行代碼。
synchronized 有兩種使用方式。
1)synchronized修飾方法:鎖是當前實例對象。它修飾的方法稱為同步方法。
public synchronized void method() {
// ...
}
2)synchronized修飾代碼塊:鎖是synchronized括號里配置的對象。它修飾的代碼塊稱為同步代碼塊。
public void method() {
synchronized (this) {
// ...
}
}
synchronized與happens-before關系
在Java內存模型中,對synchronized關鍵字建立如下的happens-before關系:釋放鎖的操作happens-before之后對同一把鎖的獲取的鎖操作。
class LockingExample {
int x = 0;
public synchronized void set() { // 1
x++; // 2
} // 3
public synchronized void get() { // 4
int i = x; // 5
// ......
} //6
}
假設線程A執行set()方法,隨后線程B執行get()方法。
假設線程A獲取鎖執行set()方法,在set()方法中,對共享變量x自增+1,然后釋放鎖。線程B獲取鎖執行get()方法,在get()方法中,讀取變量x,并賦值給本地變量i,然后釋放鎖。根據happens-before規則,可以確定線程A對x的修改happens-before線程B對x的讀取,從而保證了數據的一致性。
這個過程建立的happens-before關系可以分為3類。
1)程序次序規則:1 happens-before 2,2 happens-before 3;4 happens-before 5,5 happens-before 6;
2)監視器鎖規則:3 happens-before 4;
3)happens-before的傳遞性規則: happens-before 5。
上述happens-before關系的圖形化表現形式如下。

synchronized內存語義
synchronized釋放鎖的內存語義:當線程釋放鎖時,Java內存模型會把該線程對應的本地內存中的共享變量刷新到主內存中。
A線程釋放鎖后,共享數據的狀態如圖所示。

synchronized獲取鎖的內存語義:當線程獲取鎖時,Java內存模型會把該線程對應的本地內存置為無效。從而使得被監視器保護的臨界區代碼必須從主內存中讀取共享變量。
B線程釋放鎖后,共享數據的狀態如圖所示。

對比鎖釋放-獲取與volatile寫-讀的內存語義可以看出:鎖釋放與volatile寫有相同的內存語義;鎖獲取與volatile讀有相同的內存語
義。這表明synchronized不僅提供了互斥訪問的同步機制,還具備了volatile的內存可見性保障。
未完待續
很高興與你相遇!如果你喜歡本文內容,記得關注哦!
本文來自博客園,作者:poemyang,轉載請注明原文鏈接:http://www.rzrgm.cn/poemyang/p/19108676
浙公網安備 33010602011771號