synchronized
上一篇 【Java 併發編程】 05 一個能和面試官扯很久的 volatile 關鍵字 中講到了 volatile 可以解決線程併發執行中可見性和有序性問題。 在【Java 併發編程】 07 線程安全 中簡單介紹了可以通過 CAS 解決原子性問題。volatile 關鍵字和 樂觀鎖只能解決特定的問題,那有沒有一種方法可以解決這些統一的問題呢? 萬惡的 Bug 起源—併發編程的三大特性 有,有請我們今天的主角閃亮登場 synchronized 關鍵字
synchronized 關鍵字就像一個看門的大爺,這個看門的大爺說讓你進,你纔可以進,凡是synchronized 修飾的代碼塊,必須串行執行。它解決的是多個線程之間訪問資源的同步性,synchronized關鍵字可以保證被它修飾的方法或者代碼塊在任意時刻只能有一個線程執行。
Java 6之前,synchronized屬於重量級鎖,效率低下,因爲監視器鎖(monitor)是依賴於底層的操作系統的 Mutex Lock 來實現的,Java 的線程是映射到操作系統的原生線程之上的。如果要掛起或者喚醒一個線程,都需要操作系統幫忙完成,而操作系統實現線程之間的切換時需要從用戶態轉換到內核態,這個狀態之間的轉換需要相對比較長的時間,時間成本相對較高。
Java 6 之後 ,Java 官方在 JVM 層面對synchronized較大優化,如偏向鎖、輕量級鎖、自旋鎖、適應性自旋鎖、鎖消除、鎖粗化等技術來減少鎖操作的開銷。
鎖主要存在四種狀態,依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態,他們會隨着競爭的激烈而逐漸升級。注意鎖可以升級不可降級,這種策略是爲了提高獲得鎖和釋放鎖的效率。
synchronized 關鍵字三種常用的使用方式
修飾非靜態方法 :
作用於當前對象實例加鎖,進入同步代碼前要獲得當前對象實例的鎖。如果沒有鎖對象,則需要創建。
public synchronized void x(){
.......
}
同步方法的鎖對象就是 this ,這兩端代碼的效果是一樣的。
public void x(){
synchronized(this){
.......
}
}
修飾靜態方法
靜態方法屬於類成員,不屬於任何一個實例對象,所以 synchronized 修飾靜態方法就是給當前類加鎖,作用於類的所有對象實例。
線程A調用一個實例對象的非靜態 synchronized 方法,線程B需要調用這個實例對象所屬類的靜態 synchronized 方法,不會發生互斥現象,因爲訪問靜態 synchronized 方法佔用的鎖是當前類的鎖,而訪問非靜態 synchronized 方法佔用的鎖是當前實例對象鎖。
public static synchronized void x(){
.......
}
實現單例模式
public class Singleton {
static Singleton instance;
static Singleton getInstance(){
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
synchronized 關鍵字修飾 static 靜態方法和 synchronized(class)代碼塊上都是是給 Class 類上鎖。
修飾代碼塊
指定加鎖對象,進入同步代碼庫前要獲得給定對象的鎖。鎖對象鎖的是同步代碼塊,並不是自己。
不同類型的多個線程同步執行,此時鎖對象必須是所有線程共有的同一個對象,如果沒有,則創建一個,鎖對象不能爲null。多個不同的線程只有使用同一個對象作爲鎖對象,才能同步。
例如下面代碼塊中的 t ,t 可以人任何對象,就像看門的大爺,大爺說讓那個線程進來,哪個線程纔可以進來。當沒有線程執行同步代碼塊,t 就會通知其它線程來執行。
我們發現同步的代碼寫在了{}中,在{}中的代碼保證了併發的原子性,可見性和有序性。
synchronized (t) {
if (t.size() > 0) {
t= t.removeFirst();
sleep(100);
t.notifyAll();
} else {
t.wait();
}
}
synchronized 實現原理
我們上面講到了synchronized 鎖的是一個對象,那它是如何通過鎖這個對象做到同步的呢?
每一個對象都會關聯一個monitor lock ,一個監視器鎖。例如A線程獲取monitor後,B線程也嘗試獲取該鎖,則該線程被阻塞。直到A線程釋放 monitor lock。 JMM 中 happens -before 原則中有一項,獲取鎖操作總是在釋放鎖操作之後,先釋放鎖纔可以加鎖。
深入分析,每個 Java 對象在 JVM 的對等對象的頭中保存鎖狀態,指向 ObjectMonitor,ObjectMonitor 保存了當前持有鎖的線程引用,EntryList 中保存目前等待獲取鎖的線程,WaitSet 保存 wait 的線程。
synchronized 同步語句塊的實現使用的是 monitorenter 和 monitorexit 指令, monitorenter 指令指向同步代碼塊的開始位置,monitorexit 指令則指明同步代碼塊的結束位置。
執行 monitorenter 指令時,線程開始搶monitor lock 對象,獲得monitor lock 的持有權。此外還有一個計數器,每當線程獲得 monitor 鎖,計數器 +1,當線程重入此鎖時,計數器還會 +1。當計數器不爲0時,其它嘗試獲取 monitor 鎖的線程將會被保存到EntryList中,並被阻塞。
執行 monitorexit 指令後,持有鎖的線程釋放了monitor lock 對象,計數器 -1,重置爲0,所有 EntryList 中的線程會嘗試去獲取鎖,此時只會有一個線程成功,沒有成功的線程仍舊被阻塞保存在 EntryList 中。由此可以看出 monitor 鎖是非公平鎖。
關於 synchronized 關鍵字我們就先介紹到這裏,此時你會發現 synchronized 關鍵字的實現 依賴於 底層JVM,封裝了很多實現細節。那還要其它的鎖呢?下一節 ReentrantLock。