天天看點

Java 并發程式設計synchornized和volatile的實作原理1. volatile的定義與實作原理2.synchronized的實作原理與應用 3.鎖的更新與對比4.鎖的優缺點對比5.volatile跟Synchronize的差別6.參考文章

目錄

1. volatile的定義與實作原理

2.synchronized的實作原理與應用

3.鎖的更新與對比

3.1.偏向鎖

3.2.輕量級鎖

 3.3重量級鎖

4.鎖的優缺點對比

5.volatile跟Synchronize的差別

6.參考文章

在并發程式設計過程中 經常要處理一些:可見性 、 原子性 、 有序性的問題。 

 “ 本文主要介紹synchronized和volatile到實作原理”    synchronized和volatile幾乎是java面試基礎部分必會,不會你就吃虧了,這一篇文章摘抄《Java并發程式設計的藝術》對于初學者來說看懂這篇可能有點困難,應該先弄明白synchronized和volatile後再看原理,看不會就多看幾遍,免得出了BAT的辦公大樓後後悔哦。

Java代碼在編譯後會變成Java位元組碼,位元組碼被類加載器加載到JVM裡,JVM執行位元組碼,最終需要轉化為彙編指令在CPU上執行,Java中所使用的并發機制依賴于JVM的實作和CPU的指令。本章我們将深入底層一起探索下Java并發機制的底層實作原理。

在多線程并發程式設計中synchronized和volatile都扮演着重要的角色,volatile是輕量級的synchronized,它在多處理器開發中保證了共享變量的“可見性”。可見性的意思是當一個線程修改一個共享變量時,另外一個線程能讀到這個修改的值。如果volatile變量修飾符使用恰當的話,它比synchronized的使用和執行成本更低,因為它不會引起線程上下文的切換和排程。本文将深入分析在硬體層面上Intel處理器是如何實作volatile的,通過深入分析幫助我們正确地使用volatile變量。

我們先從了解volatile的定義開始。

1. volatile的定義與實作原理

      Java語言規範第3版中對volatile的定義如下:Java程式設計語言允許線程通路共享變量,為了確定共享變量能被準确和一緻地更新,線程應該確定通過排他鎖單獨獲得這個變量。Java語言提供了volatile,在某些情況下比鎖要更加友善。如果一個字段被聲明成volatile,Java線程記憶體模型確定所有線程看到這個變量的值是一緻的。

在了解volatile實作原理之前,我們先來看下與其實作原理相關的CPU術語與說明

        如果對聲明了volatile的變量進行寫操作,JVM就會向處理器發送一條Lock字首的指令,将這個變量所在緩存行的資料寫回到系統記憶體。但是,就算寫回到記憶體,如果其他處理器緩存的值還是舊的,再執行計算操作就會有問題。是以,在多處理器下,為了保證各個處理器的緩存是一緻的,就會實作緩存一緻性協定,每個處理器通過嗅探在總線上傳播的資料來檢查自己緩存的值是不是過期了,當處理器發現自己緩存行對應的記憶體位址被修改,就會将目前處理器的緩存行設定成無效狀态,當處理器對這個資料進行修改操作的時候,會重新從系統記憶體中把資料讀到處理器緩存裡。

下面來具體講解volatile的兩條實作原則。

       1)Lock字首指令會引起處理器緩存回寫到記憶體。Lock字首指令導緻在執行指令期間,聲言處理器的LOCK#信号。在多處理器環境中,LOCK#信号確定在聲言該信号期間,處理器可以獨占任何共享記憶體。但是,在最近的處理器裡,LOCK #信号一般不鎖總線,而是鎖緩存,畢竟鎖總線開銷的比較大。在8.1.4節有詳細說明鎖定操作對處理器緩存的影響,對于Intel486和Pentium處理器,在鎖操作時,總是在總線上聲言LOCK#信号。但在P6和目前的處理器中,如果通路的記憶體區域已經緩存在處理器内部,則不會聲言LOCK#信号。相反,它會鎖定這塊記憶體區域的緩存并回寫到記憶體,并使用緩存一緻性機制來確定修改的原子性,此操作被稱為“緩存鎖定”,緩存一緻性機制會阻止同時修改由兩個以上處理器緩存的記憶體區域資料。

        2)一個處理器的緩存回寫到記憶體會導緻其他處理器的緩存無效。IA-32處理器和Intel 64處理器使用MESI(修改、獨占、共享、無效)控制協定去維護内部緩存和其他處理器緩存的一緻性。在多核處理器系統中進行操作的時候,IA-32和Intel 64處理器能嗅探其他處理器通路系統記憶體和它們的内部緩存。處理器使用嗅探技術保證它的内部緩存、系統記憶體和其他處理器的緩存的資料在總線上保持一緻。例如,在Pentium和P6 family處理器中,如果通過嗅探一個處理器來檢測其他處理器打算寫記憶體位址,而這個位址目前處于共享狀态,那麼正在嗅探的處理器将使它的緩存行無效,在下次通路相同記憶體位址時,強制執行緩存行填充。

2.synchronized的實作原理與應用

        在多線程并發程式設計中synchronized一直是元老級角色,很多人都會稱呼它為重量級鎖。但是,随着Java SE 1.6對synchronized進行了各種優化之後,有些情況下它就并不那麼重了。本文詳細介紹Java SE 1.6中為了減少獲得鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級鎖,以及鎖的存儲結構和更新過程。     

        先來看下利用synchronized實作同步的基礎:Java中的每一個對象都可以作為鎖。具體表現為以下3種形式。

  • 普通同步方法,鎖是目前執行個體對象。
  • 靜态同步方法,鎖是目前類的Class對象。
  • 同步方法塊,鎖是Synchonized括号裡配置的對象。        

synchronized是用java的monitor機制來實作的,就是synchronized代碼塊或者方法進入及退出的時候會生成monitorenter跟monitorexit兩條指令。線程執行到monitorenter時會嘗試擷取對象所對應的monitor所有權,即嘗試擷取的對象的鎖;monitorexit即為釋放鎖。

以javap 反編譯一個帶有sychronized關鍵字的方法,輸出如下:

Java 并發程式設計synchornized和volatile的實作原理1. volatile的定義與實作原理2.synchronized的實作原理與應用 3.鎖的更新與對比4.鎖的優缺點對比5.volatile跟Synchronize的差別6.參考文章

關于這兩條指令的作用,我們直接參考JVM規範中描述:

monitorenter :

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
• If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
• If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
• If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.
           

這段話的大概意思為:

每個對象有一個螢幕鎖(monitor)。當monitor被占用時就會處于鎖定狀态,線程執行monitorenter指令時嘗試擷取monitor的所有權,過程如下:

1、如果monitor的進入數為0,則該線程進入monitor,然後将進入數設定為1,該線程即為monitor的所有者。

2、如果線程已經占有該monitor,隻是重新進入,則進入monitor的進入數加1.

3.如果其他線程已經占用了monitor,則該線程進入阻塞狀态,直到monitor的進入數為0,再重新嘗試擷取monitor的所有權。

monitorexit: 

The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.
The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.
           

這段話的大概意思為:

執行monitorexit的線程必須是objectref所對應的monitor的所有者。

指令執行時,monitor的進入數減1,如果減1後進入數為0,那線程退出monitor,不再是這個monitor的所有者。其他被這個monitor阻塞的線程可以嘗試去擷取這個 monitor 的所有權。 

3.鎖的更新與對比

        Java SE 1.6為了減少獲得鎖和釋放鎖帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”,在Java SE 1.6中,鎖一共有4種狀态,級别從低到高依次是:無鎖狀态、偏向鎖狀态、輕量級鎖狀态和重量級鎖狀态,這幾個狀态會随着競争情況逐漸更新。鎖可以更新但不能降級,意味着偏向鎖更新成輕量級鎖後不能降級成偏向鎖。這種鎖更新卻不能降級的政策,目的是為了提高獲得鎖和釋放鎖的效率,下文會詳細分析。

3.1.偏向鎖

        HotSpot 的作者經過研究發現,大多數情況下,鎖不僅不存在多線程競争,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價更低而引入了偏向鎖。當一個線程通路同步塊并擷取鎖時,會在對象頭和棧幀中的鎖記錄裡存儲鎖偏向的線程ID,以後該線程在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,隻需簡單地測試一下對象頭的Mark Word裡是否存儲着指向目前線程的偏向鎖。如果測試成功,表示線程已經獲得了鎖。如果測試失敗,則需要再測試一下Mark Word中偏向鎖的辨別是否設定成1(表示目前是偏向鎖):如果沒有設定,則使用CAS競争鎖;如果設定了,則嘗試使用CAS将對象頭的偏向鎖指向目前線程。

3.2.輕量級鎖

    (1)輕量級鎖加鎖

        線程在執行同步塊之前,JVM會先在目前線程的棧桢中建立用于存儲鎖記錄的空間,并将對象頭中的Mark Word複制到鎖記錄中,官方稱為Displaced Mark Word。然後線程嘗試使用CAS将對象頭中的Mark Word替換為指向鎖記錄的指針。如果成功,目前線程獲得鎖,如果失敗,表示其他線程競争鎖,目前線程便嘗試使用自旋來擷取鎖。

    (2)輕量級鎖解鎖

        輕量級解鎖時,會使用原子的CAS操作将Displaced Mark Word替換回到對象頭,如果成功,則表示沒有競争發生。如果失敗,表示目前鎖存在競争,鎖就會膨脹成重量級鎖。

兩個線程同時争奪鎖,導緻鎖膨脹的流程圖。

Java 并發程式設計synchornized和volatile的實作原理1. volatile的定義與實作原理2.synchronized的實作原理與應用 3.鎖的更新與對比4.鎖的優缺點對比5.volatile跟Synchronize的差別6.參考文章

       因為自旋會消耗CPU,為了避免無用的自旋(比如獲得鎖的線程被阻塞住了),一旦鎖更新成重量級鎖,就不會再恢複到輕量級鎖狀态。當鎖處于這個狀态下,其他線程試圖擷取鎖時,都會被阻塞住,當持有鎖的線程釋放鎖之後會喚醒這些線程,被喚醒的線程就會進行新一輪的奪鎖之争。

 3.3重量級鎖

  這個就是我們平常說的synchronized鎖,如果搶占不到,則線程阻塞,等待正在執行的線程結束後喚醒自己,然後重新開始競争。之是以說是重量級,是因為線程阻塞會讓出cpu資源,從核心态轉換為使用者态,然後執行的時候再次轉換為核心态,這個過程中,cpu要切換線程,看這個線程上次執行到哪兒了,這次應該從哪兒開始,相關的變量有哪些之類的,這就是所謂的執行上下文,這個上下文的切換對寶貴的cpu資源來說是“無用功”,因為這是在為執行做準備條件,如果cpu大量的時間用在這些“無用功”上,當然也就出活兒少,也就影響執行效率了。

synchronized就是這樣,預設開始是偏向鎖,有競争就逐漸更新,最終可能是重量級鎖的一個過程。鎖定的區域就是對象的Mark Word的内容。

4.鎖的優缺點對比

Java 并發程式設計synchornized和volatile的實作原理1. volatile的定義與實作原理2.synchronized的實作原理與應用 3.鎖的更新與對比4.鎖的優缺點對比5.volatile跟Synchronize的差別6.參考文章

5.volatile跟Synchronize的差別

  1. volatile隻能作用域變量,Synchronize可作用域變量、方法、類、同步代碼塊等;
  2. volatile隻能保證可見性和有序性,不能保證原子性,Synchronize三者都可以保證。
  3. volatile不會造成線程阻塞,Synchronize可能會造成線程阻塞。
  4. 在性能方面synchronized關鍵字是防止多個線程同時執行一段代碼,會影響程式執行效率,而volatile關鍵字在某些情況下性能要優于synchronized。

6.參考文章

synchronized的底層原理?

synchronized底層實作原理及鎖優化

Java并發程式設計:Synchronized及其實作原理

Synchronized關鍵字使用、底層原理、底層優化、ReenTrantLock的對比

繼續閱讀