一、Java中synchronized關鍵字的作用
總所周知,在並發環境中多個線程對同一個資源進行訪問很可能出現臟讀等一系列線程安全問題。這時我們可以用加鎖的方式對訪問共享資源的代碼塊進行加鎖,以確保同一時間段內只能有一個線對資源進行訪問,在它釋放鎖之前其他競爭鎖的線程只能等待。而synchronized關鍵字是加鎖的一種方式。
舉個通俗易懂的例子:比如你上廁所之后,你要鎖門,此時其他人只能在外面等待,直到你出來后,下一個人才能進去。這就是現實中一個加鎖和釋放鎖的例子。
二、Java中synchronized關鍵字的運用
synchronized關鍵字的運用主要包括三方面:
- 鎖代碼塊(鎖對象可指定,可為this、XXX.class、全局變量)
- 鎖普通方法(鎖對象是this,即該類實例本身)
- 鎖靜態方法(鎖對象是該類,即XXX.class)
接下來,我們具體分析一下以上三種情況的運用。
1、鎖代碼塊
代碼:
public class Sync{ private int a = 0; public void add(){ synchronized(this){ System.out.println("a values " + ++a); } } }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
反編譯結果:
由反編譯結果可以看出:synchronized代碼塊主要是靠monitorenter和monitorexit這兩個原語來實現同步的。當線程進入monitorenter獲得執行代碼的權利時,其他線程就不能執行里面的代碼,直到鎖Owner線程執行monitorexit釋放鎖后,其他線程才可以競爭獲取鎖。
在這里,我們先闡釋一下Java虛擬機規范中相關內容:
(1)、monitorenter
每個對象有一個監視器鎖(monitor)。當monitor被占用時就會處於鎖定狀態,線程執行monitorenter指令時嘗試獲取monitor的所有權,過程如下:
- 如果monitor的進入數為0,則該線程進入monitor,然后將進入數設置為1,該線程即為monitor的所有者。
- 如果線程已經占有該monitor,只是重新進入,則進入monitor的進入數加1.
- 如果其他線程已經占用了monitor,則該線程進入阻塞狀態,直到monitor的進入數為0,再重新嘗試獲取monitor的所有權。
上述第2點就涉及到了可重入鎖,意思就是說當一個線程已經獲取一個鎖時,它可以再獲取無數次,從代碼的角度上將就是有無數個相同的synchronized語句塊嵌套在一起。在進入時,monitor的進入數+1;退出時就-1,直到為0的時候才可以被其他線程競爭獲取。
(2)、monitorexit
執行monitorexit的線程必須是objectref所對應的monitor的所有者。
指令執行時,monitor的進入數減1,如果減1后進入數為0,那線程退出monitor,不再是這個monitor的所有者。其他被這個monitor阻塞的線程可以嘗試去獲取這個 monitor 的所有權。
2、鎖普通方法
代碼:
public class Sync{ private int a = 0; public synchronized void add(){ System.out.println("a values " + ++a); } }
- 1
- 2
- 3
- 4
- 5
- 6
反編譯結果:
從上圖可以看出,這里並沒有monitorenter和monitorexit,但是常量池中多了ACC_SYNCHRONIZED標示符。JVM就是根據該標示符來實現方法的同步的:當方法調用時會檢查方法的 ACC_SYNCHRONIZED 訪問標志是否被設置,如果設置了,執行線程將先獲取monitor,獲取成功之后才能執行方法體,方法執行完后再釋放monitor。在方法執行期間,其他任何線程都無法再獲得同一個monitor對象。這種方式與語句塊沒什么本質區別,都是通過競爭monitor的方式實現的。只不過這種方式是隱式的實現方法。
在這里,我們將以上兩種方法進行一下說明:
首先是代碼塊,當程序運行到monitorenter時,競爭monitor,成功后繼續運行后續代碼,直到monitorexit才釋放monitor;而ACC_SYNCHRONIZED則是通過標志位來提示線程去競爭monitor。也就是說,monitorenter和ACC_SYNCHRONIZED只是起標志作用,並無實質操作。
3、鎖靜態方法
代碼:
public class Sync{ private static int a = 0; public synchronized static void add(){ System.out.println("a values " + ++a); } }
- 1
- 2
- 3
- 4
- 5
- 6
反編譯結果:
常量池中用ACC_STATIC標志了這是一個靜態方法,然后用ACC_SYNCHRONIZED標志位提醒線程去競爭monitor。由於靜態方法是屬於類級別的方法(即不用創建對象就可以被調用),所以這是一個類級別(XXX.class)的鎖,即競爭某個類的monitor。
三、鎖的競爭過程
上面只是闡述了如何提醒線程去爭奪鎖,所以接下來我們闡述一下線程是怎樣競爭鎖的。其實總的來說,JVM中是通過隊列來控制線程去競爭鎖的。
- (1)、多個線程請求鎖,首先進入Contention List,它可以接納所有請求線程,而且是一個后進先出(LIFO)的虛擬隊列,通過結點Node和next指針構造。
- (2)(3)、ContentionList會被線程並發訪問,EntryList為了降低線程對ContentionList隊尾的爭用而構造出來。當Owner釋放鎖時,會從ContentionList中遷移線程到EntryList,並會指定EntryList中的某個線程(一般為Head結點)為Ready Thread,也就是說某個時刻最多只有一個線程正在競爭鎖。
- (4)、Owner並不是直接把鎖交給OnDeck線程,而是將競爭鎖的權利交給OnDeck(將鎖釋放了),然后讓OnDeck自己去競爭。競爭成功后,OnDeck線程就變成Owner;否則繼續留在EntryList的隊頭。
- (5)(6)、當線程調用wait方法被阻塞時,進入WaitSet;當其他線程調用notifyAll()(notify())方法后,阻塞隊列的(某個)線程就會進入EntryList中。
處於ContetionList、EntryList、WaitSet的線程均處於阻塞狀態。而線程被阻塞涉及到用戶態與內核態的切換(Liunx),系統切換嚴重影響鎖的性能。解決這個問題的辦法就是自旋。自旋就是線程不斷進行內部循環,即for循環什么也不做,防止線程wait()阻塞,在自旋過程中不斷嘗試獲取鎖,如果自旋期間,Owner剛好釋放鎖,此時自旋線程就可以去競爭鎖。如果自旋了一段時間還沒獲取到鎖,那沒辦法,只能調用wait()阻塞了。
為什么自旋了一段時間后又調用wait()方法呢?因為自旋是要消耗CPU的,而且還有線程上下文切換,因為CPU還可以調度線程,只不過執行的是空的for循環罷了。
對自旋鎖周期的選擇上,HotSpot認為最佳時間應是一個線程上下文切換的時間,但目前並沒有做到。
所以,synchronized是什么時候進行自旋的?答案是在進入ContetionList之前,因為它自旋一定時間后還沒獲取鎖,最后它只好在ContetionList中阻塞等待了。
四、通過JVM了解synchronized
把鎖說得那么玄乎,到底鎖是何方神聖呢?首先,我們來了解一下對象頭。
從圖中可以看到,Java對象Mark Word中的是否含偏向鎖、鎖標志位都與鎖有關。是否含偏向鎖很明顯與偏向鎖有關,而鎖標記位指的是用了什么鎖。接下來用一張圖表示不同狀態的鎖下各個部分的含義。
為了減少鎖釋放帶來的消耗,鎖有一個升級的機制,從輕到重依次是:無鎖狀態 ——> 偏向鎖 ——> 輕量級鎖 ——>重量級鎖。
1、偏向鎖
(1)、運行原理
重量級鎖使用互斥量實現同步;輕量級鎖使用CAS操作,避免重量級鎖的互斥量;而偏向鎖則是在無競爭條件下把整個同步都刪除掉,連CAS都不用做了(在設置偏向鎖的時候只需要一步CAS操作)。
偏向鎖,在無其它線程與它競爭的情況下,持有偏向鎖的線程永遠也不需要同步。它的加鎖過程很簡單:線程訪問同步代碼塊時檢查偏向鎖中線程ID是否指向自己,如果是表明該線程已獲得鎖;否則,檢測偏向鎖標記是否為1,不是的話則CAS競爭鎖,如果是就將對象頭中線程ID指向自己。
當存在線程競爭鎖時,偏向鎖才會撤銷,轉而升級為輕量級鎖。而這個撤銷過程則需要有一個全局安全點(即這個時間點上沒有正在執行的字節碼)。過程如下:
在撤銷鎖的時候,棧中對象頭的Mark Word要么偏向於其他線程,要么恢復到無鎖或者輕量級鎖。
(2)、分析
- 優點:加鎖和解鎖無需額外消耗
- 缺點:鎖進化時會帶來額外鎖撤銷的消耗
- 適用場景:只有一個線程訪問同步代碼塊
3、輕量級鎖
(1)、運行原理
(2)、分析
- 優點:競爭的線程不阻塞,也就是不涉及到用戶態與內核態的切換(Liunx),減少系統切換鎖帶來的開銷
- 缺點:如果長時間競爭不到鎖,自旋會消耗CPU
- 適用場景:追求響應時間、同步塊執行速度非常快
3、重量級鎖
它是傳統意義上的鎖,通過互斥量來實現同步,線程阻塞,等待Owner釋放鎖喚醒。
(2)、分析
- 優點:線程競爭不自旋,不消耗CPU
- 缺點:線程阻塞,響應時間慢
- 適用場景:追求吞吐量、同步塊執行時間較長
五、總結
Java的synchronized關鍵字可實現同步功能,在多個線程請求統一資源時,可以只允許一個線程訪問,在Owner釋放鎖之前其他線程都不能訪問。
synchronized的同步機制是通過競爭monitor實現的,多個競爭線程可通過隊列來協調。
每個Java對象的頭部都有關於鎖的標志位,這里存放了鎖的有關信息。為了提高效率,鎖有一個粗話過程,從輕到重依次是:無鎖狀態 ——> 偏向鎖 ——> 輕量級鎖 ——>重量級鎖。
推薦閱讀書籍:
《Java並發編程的藝術》
《深入理解Java虛擬機》