1. 程式人生 > >深入理解java同步、鎖機制

深入理解java同步、鎖機制

本片文章嘗試從另一個層面來了解我們常見的同步(synchronized)和鎖(lock)機制。如果讀者想深入瞭解併發方面的知識推薦一本書《java併發程式設計實戰》,非常經典的一本書,英語水平好的同學也可以讀一讀《Concurrent programming in Java - design principles and patterns》由Doug Lea親自操刀,Doug Lea是併發方面的大神,jdk的併發包就是由他完成的。

我們都知道在java中被synchronized修飾的程式碼被稱為同步程式碼塊,同步程式碼塊意味著同一時刻只有一個執行緒執行,其他執行緒都被排斥在該同步塊之外,並且訪問也是按照某種順序執行的。實際上synchronized是基於監視器實現的,每一個例項和類都擁有一個監視器,通常我們說的“鎖”的動作就是獲取該監視器。因此通常我們講synchronized是基於JVM層面的,使用的是物件內建的鎖。靜態方法鎖住的是該class的監視器,例項方法鎖住的是對應例項的監視器。同步是使用monitorenter和monitorexit指令實現的,monitorenter嘗試獲取物件的鎖,如果該物件沒被鎖定或者當前執行緒已經獲取了鎖,則把鎖的計數器+1,同樣monitorexit把鎖的計數器-1。因此synchronized對於同一個執行緒是可重入的。

監視器支援兩種執行緒:互斥(sync)和協作。java通過物件的鎖實現對臨界區的互斥訪問,使用Object的wait(),notify(),notifyAll()方法來實現。

樂觀鎖和悲觀鎖

這兩個名字很多地方都出現過,所謂的樂觀鎖就是當去做某個修改或其他操作的時候它認為不會有其他執行緒來做同樣的操作(競爭),這是一種樂觀的態度,通常是基於CAS原子指令來實現的。關於CAS可以參見這篇文章java併發包的CAS操作,CAS通常不會將執行緒掛起,因此有時效能會好一些。(執行緒的切換是挺耗效能的一個操作)。

悲觀鎖,根據樂觀鎖的定義很容易理解悲觀鎖是認為肯定有其他執行緒來爭奪資源,因此不管到底會不會發生爭奪,悲觀鎖總是會先去鎖住資源。

以前的synchronized都是會阻塞執行緒的,就是說會發生上下文切換,從使用者態切換到核心態,由於這種方式有時候太耗費資源,因此後來又出現了自旋鎖,所謂自旋其實就是如果鎖已經被其他執行緒佔有,當前執行緒並不會掛起,而是做空操作,自旋其實從某種程度來說是樂觀鎖,因為它總是認為下次會得到鎖的。因此自旋鎖適合在競爭不激烈的情況下使用,據瞭解目前的jvm針對synchronized已經有了這方面的優化。

自旋的使用也是分場景的,有可能執行緒自旋很久也沒獲取到鎖,那麼CPU就白白被浪費了,還不如掛起執行緒,因此有出現了自適應的自旋鎖,它會更具歷史的自旋是否獲取到鎖的記錄來判斷自旋的時間或者是否需要自旋。

輕量級鎖

輕量級鎖的概念是相對需要互斥操作的重量級鎖而言,輕量級鎖的目的是減少多執行緒的互斥機率,並不是要代替互斥。要想了解輕量級鎖和後面講到的偏向鎖必須先了解下物件頭的記憶體佈局。下面這張圖就是Object Header的記憶體佈局:


初始都是01表示無鎖,00表示輕量級鎖,10表示重量級鎖等等。在程式碼進入同步塊的時候,如果此同步物件沒有被鎖定(鎖標誌位為“01”狀態),虛擬機器首先將在當前執行緒的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用於儲存鎖物件目前的Mark Word的拷貝(官方把這份拷貝加了一個Displaced字首,即Displaced Mark Word),然後虛擬機器嘗試利用CAS操作將物件的輕量級指標指向棧的lock record,如果更新成功當前執行緒獲取到鎖,並且標記為00輕量級鎖。如果這個更新操作失敗了,虛擬機器首先會檢查物件的Mark Word是否指向當前執行緒的棧幀,如果是就說明當前執行緒已經擁有了這個物件的鎖,那就可以直接進入同步塊繼續執行,否則說明這個鎖物件已經被其他執行緒搶佔了。如果有兩條以上的執行緒爭用同一個鎖,那輕量級鎖就不再有效,要膨脹為重量級鎖,鎖標誌的狀態值變為“10”,Mark Word中儲存的就是指向重量級鎖(互斥量)的指標,後面等待鎖的執行緒也要進入阻塞狀態。 

偏向鎖

偏向鎖就是偏心的意思,當鎖被某個執行緒第一次獲取到得時候,會在物件頭記錄獲取到該鎖的執行緒id,以後每次該執行緒進入同步塊的時候都不需要加鎖,如果一旦有其他執行緒獲取到該鎖,則偏向鎖模式宣告失敗,鎖撤銷回未鎖定或輕量級鎖狀態。偏向鎖的作用就是完全消除鎖,連CAS操作都不做。

下面來看一下執行緒在進入同步塊和出同步塊的狀態轉換。

  當多個執行緒同時請求某個物件監視器時,物件監視器會設定幾種狀態用來區分請求的執行緒:

  • Contention List:所有請求鎖的執行緒將被首先放置到該競爭佇列
  • Entry List:Contention List中那些有資格成為候選人的執行緒被移到Entry List
  • Wait Set:那些呼叫wait方法被阻塞的執行緒被放置到Wait Set
  • OnDeck:任何時刻最多隻能有一個執行緒正在競爭鎖,該執行緒稱為OnDeck
  • Owner:獲得鎖的執行緒稱為Owner
  • !Owner:釋放鎖的執行緒
下面是一位網友畫得圖很形象:


新請求的執行緒會被放置到ContentionList中,當某個Owner釋放鎖的時候,如果EntryList是空則Owner會從ContentionList中移動執行緒到EntryList。顯然,ContentionList結構其實是個Lock-Free的佇列,因為只有Owner才會從ContentionList取節點。

EntryList與ContentionList邏輯上同屬等待佇列,ContentionList會被執行緒併發訪問,為了降低對ContentionList隊尾的爭用,而建立EntryList。Owner執行緒在unlock時會從ContentionList中遷移執行緒到EntryList,並會指定EntryList中的某個執行緒(一般為Head)為Ready(OnDeck)執行緒。Owner執行緒並不是把鎖傳遞給OnDeck執行緒,只是把競爭鎖的權利交給OnDeck,OnDeck執行緒需要重新競爭鎖。這樣做雖然犧牲了一定的公平性,但極大的提高了整體吞吐量,在Hotspot中把OnDeck的選擇行為稱之為“競爭切換”。

可重入鎖

可重入鎖的最大好處是可以避免思索,因為對於已經獲取到鎖的執行緒,不需要再一次去獲取鎖了,只需要將計數器+1即可,實際上synchronized也是可重入鎖的一種。但是本節我們要講的是併發包中的ReentrantLock及其實現。synchronized是JVM層面提供的鎖,而在java的語言層面jdk也為我們提供了非常優秀的鎖,這些鎖都在java.util.concurren包中。

先來看一下JVM提供的鎖和併發包中的鎖有哪些區別:

1.synchronized的加鎖和釋放都是由JVM提供,不需要我們關注,而lock的加鎖和釋放全部由我們去控制,通常釋放鎖的動作要在finally中實現。

2.synchronized只有一個狀態條件,也就是每個物件只有一個監視器,如果需要多個Condition的組合那麼synchronized是無法滿足的,而lock則提供了多條件的互斥,非常靈活。

3.ReentrantLock 擁有Synchronized相同的併發性和記憶體語義,此外還多了 鎖投票,定時鎖等候和中斷鎖等候。

在講解ReentrantLock之前,先來看下不AtomicInteger原始碼大體瞭解下它的實現原理。

/**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
     //該方法類似同步版本的i++,先將當前值+1,然後返回,
     //可以看到是一個for迴圈,只有當compareAndSet成功才會返回
     //那麼什麼時候成功呢?
    public final int incrementAndGet() {
        for (;;) {
            int current = get();//volatile型別的變數,因此每次獲取都是最新值
            int next = current + 1;//加1操作
            if (compareAndSet(current, next))//關鍵的是if中的方法
	    //如果compareAndSet成功,則整個加操作成功,如果失敗,則說明有其他執行緒已經修改了value
	    //那麼會進行下一輪的加1操作,直到成功
                return next;
        }
    }
/**
     * Gets the current value.
     *
     * @return the current value
     */
     //get方法很簡單,返回value,這個value是類的成員變數,並且是volatile的
    public final int get() {
        return value;
    }

    /**
     * Atomically sets the value to the given updated value
     * if the current value {@code ==} the expected value.
     *
     * @param expect the expected value
     * @param update the new value
     * @return true if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */
    public final boolean compareAndSet(int expect, int update) {
        //繼續跟蹤unsafe的方法,發現並沒提供,實際上該方法是個基於本地類庫的原子方法,使用一個指令即可完成操作。
	//如果記憶體中的值和預期的值相同,也就是沒有其他執行緒修改過該值,則更新該值為預期的值,返回成功,否則返回失敗
	return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
可以預見的是如果競爭非常激烈,則失敗的概率會大大增加,效能也會受到影響。實際上併發包中的鎖大多是基於CAS操作完成的,本節打算講解可重入鎖,但是需要了解的東西還非常多,只好重新寫一篇來介紹ReentrantLock了。