深入瞭解java同步、鎖緊機構

來源:互聯網
上載者:User

標籤:

該薄膜還具有從本文試圖一個高度來認識我們共同的同步(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通常不會將線程掛起,因此有時效能會好一些。(線程的切換是挺耗效能的一個操作)。

悲觀鎖,依據樂觀鎖的定義非常easy理解悲觀鎖是覺得肯定有其它線程來爭奪資源,因此無論究竟會不會發生爭奪。悲觀鎖總是會先去鎖住資源。

曾經的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方法很easy,返回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該。

著作權聲明:本文部落格原創文章,部落格,未經同意,不得轉載。

深入瞭解java同步、鎖緊機構

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.