關於Lock、synchronized、volatile原理及區別

1.volatile

大多數人都知道volatile一個是保證多線程併發時的內存的可見性,還有一個就是禁止指令重排序,那麼什麼是內存的可見性呢?JMM模型規範了所有的變量(這裏指分配對象之類的共享變量),必須通過主內存與線程工作內存通信。

但是這裏會存在一個問題,如果多線程併發的情況下,有兩個線程同時對a進行加一操作了,在沒有正確同步的情況下,那麼有可能就會出現a=2情況,產生這種情況是因爲JMM規範線程讀取變量的時候都必須經過主內存,然後存儲到本地的副本中,如果線程1讀取a=1到自己的線程副本中,進行a+1操作,但是此時還沒有把數據寫回主內存,線程2也可開始了從主內存讀取a=1的值,那麼這種情況下,兩個線程的累加並不是我們預期的a=3值。那麼這裏我們是不是直接用volatile修飾這個變量就會讓a++操作併發安全了呢?這裏其實還是埋了個坑,我還是需要介紹一下指令重排序的概念,我們java的字節碼文件經過JVM加載執行最後都會編譯成機器語言如彙編,我們方法的代碼執行會變成一條條指令,但是硬件層在某些時候爲了提高CPU效率會對一些沒有數據依賴的指令進行重排序,就比如a=1,b=2.如果根據程序次序規則的happens-before關係,a=1的指令會先於b=2的指令執行,但是CPU有可能將先將b=2執行了,而且這裏JMM規範對這種情況並沒有要求,JMM規範裏在程序處理結果跟順序執行一致的情況下,並沒有要求A happens-before B,就一定要A先執行,它只要求A執行的結果對B是可見的,也就是不影響最後程序的結果它都是允許的。

如上圖是一個DCL單例創建,如果不適用volatile修飾,我們獲取初始化一半的對象,這裏因爲new SingletonDemo()創建對象時不是一個原子操作,當JVM遇到new指令時,假設步驟爲1.在主內開闢對象空間,2.執行對象初始化操作,3.將對象地址賦值給棧內存的變量instance.因爲我們使用的是synchronized,代碼進入臨界區後,JMM規範允許臨界區的代碼重排序,所以這裏有可能將步驟3執行在步驟2的前面。這樣就有可能在併發的情況下,其他線程讀取到了未初始化完畢的對象。那麼如果用volatile修飾instance變量禁止指令重排序後,我們就可以保證單例的併發安全了。

這裏補充一點,被volatile修飾的變量,底層編譯成彙編的時候,會多一個Lock前綴指令,該指令主要作用是,將寫緩衝區刷回內存,還有就是如果支持緩存鎖定,就鎖定該內存的地址或者鎖總線,讓內存中的變量變成某個cpu獨享狀態,然後通過MESI緩存一致性來保證線程間的內存可見性,還有它禁止了內存屏障內的指令重排序。其實這些就是JMM爲了保證votatile的語義的硬件實現。

 

2.synchronized

JVM實現的鎖,當我們對共享資源進行修改時,爲了保證併發安全性,通俗的說,就是我們對共享資源上一把鎖,只有拿到鎖的線程才能進入,然後線程出來之後,還要上交這把鎖。關於JVM如何實現的呢?

synchronized修飾成員方法時,鎖的對象就是this,修飾類方法時,鎖的對象就是this.getClass(),修飾代碼塊時,鎖的對象就是()裏的對象,然後我們所有的對象其實都是有一個ObjectMonitor對象關聯的,當我們線程要獲取鎖時,就是獲取這個對象,那麼當多個線程爭取monitor對象時,只會有一個線程成功,其他競爭失敗的對象會進入一個entry-list隊列裏阻塞等待搶鎖成功的線程釋放鎖,如果持有鎖的線程調用wait()方法,那麼它將進入一個wait set的隊裏並且釋放鎖,等待其他線程對它的notify.這裏其實JVM還有實現,釋放鎖後對entry-list隊列裏的阻塞線程進行喚醒操作。因爲java線程是需要操作系統轉入內核進行喚醒和上下文切換的,所以才說synchronized是重量級鎖

但是從JDK1.6之後,synchronized進行了多種優化操作,其中就是偏向鎖,輕量級鎖。偏向鎖就是通過設置對象的markwold裏的鎖標誌位,以及記錄當前獲取鎖的線程ID,通過CAS操作替換markwold,如果替換成功則獲取鎖。輕量級鎖其實實現就是自旋,當一個線程獲取鎖時,會判斷前面線程是否通過自旋獲取過鎖,如果前面的線程通過自旋成功獲取鎖,那麼這裏也會通過自旋等待來獲取鎖,這裏的缺點就是自旋空轉浪費CPU處理器時間,如果應用程序中都是存在併發而且執行鎖操作時間較長的情況下的,關閉自旋操作可能效率更高。

 

3.Lock

Lock鎖時JDK層面實現的,通過AQS同步器實現的。底層原理是通過硬件提供的原子操作compareAndswap指令實現的一種lock free(無鎖化),它有reentrantLock(重入鎖),ReentrantReadWriteLock(讀寫鎖)。其實都是通過基礎的同步器AQS實現的。

AQS採用了模板設計,封裝了等待線程入隊列以及出隊列的操作,讀寫鎖對信號量設計的更巧妙。採用了高16位代表讀鎖獲取的次數,低16位代表寫鎖獲取的次數,來進行一個讀寫分離。

 

4.它們的區別

volatile是輕量級的鎖,它能保證變量的內存可見性以及禁止重排序,Lock是JDK層面通過循環cas操作的lock free的鎖,這裏需要注意應用場景,如果資源一定會存在併發的情況下,而且處理也比較耗時的話,不建議使用Lock接口,使用JVM原生的synchronized效率應該更好些,synchronized是JVM底層實現的鎖,雖然做過一系列優化,儘管也會使用cas以及自旋做一些優化鎖,但是一旦膨脹爲重量級鎖之後,是無法降級的,也就是必須通過系統的阻塞與喚醒線程,是比較消耗資源的行爲了。所以我們在鎖選型的時候,應該側重各自的優缺點以及應用場景去實現。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章