關於鎖的那點事兒

一 synchronized底層原理

咱們先經過反編譯下面的代碼來講明問題。html

public class SynchronizedDemo {
    public void method() {
        synchronized (this) {
            System.out.println("Method 1 start");
        }
    }
}

反編譯結果:java

關於這兩條指令的做用,咱們直接參考JVM規範中描述:安全

monitorenter :多線程

每一個對象有一個監視器鎖(monitor)。當monitor被佔用時就會處於鎖定狀態,線程執行monitorenter指令時嘗試獲取monitor的全部權,過程以下:併發

一、若是monitor的進入數爲0,則該線程進入monitor,而後將進入數設置爲1,該線程即爲monitor的全部者。app

二、若是線程已經佔有該monitor,只是從新進入,則進入monitor的進入數加1.jvm

3.若是其餘線程已經佔用了monitor,則該線程進入阻塞狀態,直到monitor的進入數爲0,再從新嘗試獲取monitor的全部權。函數

 

monitorexit:工具

執行monitorexit的線程必須是objectref所對應的monitor的全部者。性能

指令執行時,monitor的進入數減1,若是減1後進入數爲0,那線程退出monitor,再也不是這個monitor的全部者。其餘被這個monitor阻塞的線程能夠嘗試去獲取這個 monitor 的全部權。 

  經過這兩段描述,咱們應該能很清楚的看出Synchronized的實現原理,Synchronized的語義底層是經過一個monitor的對象來完成,其實wait/notify等方法也依賴於monitor對象,這就是爲何只有在同步的塊或者方法中才能調用wait/notify等方法,不然會拋出java.lang.IllegalMonitorStateException的異常的緣由。

 

 

咱們再看一下同步方法的反編譯結果

public class SynchronizedMethod {
    public synchronized void method() {
        System.out.println("Hello World!");
    }
}

反編譯結果:

 

從反編譯的結果來看,方法的同步並無經過指令monitorenter和monitorexit來完成(理論上其實也能夠經過這兩條指令來實現),不過相對於普通方法,其常量池中多了ACC_SYNCHRONIZED標示符。JVM就是根據該標示符來實現方法的同步的:當方法調用時,調用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設置,若是設置了,執行線程將先獲取monitor,獲取成功以後才能執行方法體,方法執行完後再釋放monitor。在方法執行期間,其餘任何線程都沒法再得到同一個monitor對象。 其實本質上沒有區別,只是方法的同步是一種隱式的方式來實現,無需經過字節碼來完成。

 

 

 二 鎖的狀態

上面介紹了synchronized用法和實現原理。咱們已經知道,synchronized是經過對象內部的一個叫作監視器鎖(monitor)來實現的。可是監視器鎖本質又是依賴於底層的操做系統的mutex lock來實現的。而操做系統實現線程之間的切換就須要從用戶態轉換到核心態,這個成本很是高,狀態之間的轉換須要相對比較長的時間,這就是synchronized效率低的緣由。所以,這種依賴於操做系統mutex lock所實現的鎖咱們稱爲"重量級鎖"。jdk對於synchronized作的種種優化,其核心都是爲了減小這種重量級鎖的使用。jdk1.6之後,爲了減小得到鎖和釋放鎖帶來的性能消耗,引入了"輕量級鎖"和"偏向鎖"

 

鎖的狀態總共有四種:無鎖狀態偏向鎖輕量級鎖重量級鎖。隨着鎖的競爭,鎖能夠從偏向鎖升級到輕量級鎖,再升級的重量級鎖(可是鎖的升級是單向的,也就是說只能從低到高升級,不會出現鎖的降級)。JDK 1.6中默認是開啓偏向鎖和輕量級鎖的,咱們也能夠經過-XX:-UseBiasedLocking來禁用偏向鎖。

(1)偏向鎖(自旋)

引入偏向鎖是爲了在無多線程競爭的狀況下儘可能減小沒必要要的輕量級鎖執行路徑,由於輕量級鎖的獲取及釋放依賴屢次CAS原子指令,而偏向鎖只須要在置換ThreadID的時候依賴一次CAS原子指令(因爲一旦出現多線程競爭的狀況就必須撤銷偏向鎖,因此偏向鎖的撤銷操做的性能損耗必須小於節省下來的CAS原子指令的性能消耗)。咱們知道,輕量級鎖是爲了在線程交替執行同步塊時提升性能,而偏向鎖則是在只有一個線程執行同步塊時進一步提升性能

(2)輕量級鎖

「輕量級」是相對於使用操做系統互斥量來實現的傳統鎖而言的。可是,首先須要強調一點的是,輕量級鎖並非用來代替重量級鎖的,它的本意是在沒有多線程競爭的前提下,減小傳統的重量級鎖使用產生的性能消耗。在解釋輕量級鎖的執行過程以前,先明白一點,輕量級鎖所適應的場景是線程交替執行同步塊的狀況,若是存在同一時間訪問同一鎖的狀況,就會致使輕量級鎖膨脹爲重量級鎖。

 

輕量級鎖也是一種多線程優化,它與偏向鎖的區別在於,輕量級鎖是經過CAS來避免進入開銷較大的互斥操做,而偏向鎖是在無競爭場景下徹底消除同步,連CAS也不執行

 

 

 

三 其餘優化

一、適應性自旋(Adaptive Spinning):從輕量級鎖獲取的流程中咱們知道當線程在獲取輕量級鎖的過程當中執行CAS操做失敗時,是要經過自旋來獲取重量級鎖的。問題在於,自旋是須要消耗CPU的,若是一直獲取不到鎖的話,那該線程就一直處在自旋狀態,白白浪費CPU資源。解決這個問題最簡單的辦法就是指定自旋的次數,例如讓其循環10次,若是還沒獲取到鎖就進入阻塞狀態。可是JDK採用了更聰明的方式——適應性自旋,簡單來講就是線程若是自旋成功了,則下次自旋的次數會更多,若是自旋失敗了,則自旋的次數就會減小。

二、鎖粗化(Lock Coarsening):鎖粗化的概念應該比較好理解,就是將屢次鏈接在一塊兒的加鎖、解鎖操做合併爲一次,將多個連續的鎖擴展成一個範圍更大的鎖。舉個例子:

public class StringBufferTest {
    StringBuffer stringBuffer = new StringBuffer();

    public void append(){
        stringBuffer.append("a");
        stringBuffer.append("b");
        stringBuffer.append("c");
    }
}

  這裏每次調用stringBuffer.append方法都須要加鎖和解鎖,若是虛擬機檢測到有一系列連串的對同一個對象加鎖和解鎖操做,就會將其合併成一次範圍更大的加鎖和解鎖操做,即在第一次append方法時進行加鎖,最後一次append方法結束後進行解鎖。

 三、鎖消除(Lock Elimination):鎖消除即刪除沒必要要的加鎖操做。根據代碼逃逸技術,若是判斷到一段代碼中,堆上的數據不會逃逸出當前線程,那麼能夠認爲這段代碼是線程安全的,沒必要要加鎖。看下面這段程序:

public class SynchronizedTest02 {

    public static void main(String[] args) {
        SynchronizedTest02 test02 = new SynchronizedTest02();
        //啓動預熱
        for (int i = 0; i < 10000; i++) {
            i++;
        }
        long start = System.currentTimeMillis();
        for (int i = 0; i < 100000000; i++) {
            test02.append("abc", "def");
        }
        System.out.println("Time=" + (System.currentTimeMillis() - start));
    }

    public void append(String str1, String str2) {
        StringBuffer sb = new StringBuffer();
        sb.append(str1).append(str2);
    }
}

雖然StringBuffer的append是一個同步方法,可是這段程序中的StringBuffer屬於一個局部變量,而且不會從該方法中逃逸出去,因此其實這過程是線程安全的,能夠將鎖消除。

爲了儘可能減小其餘因素的影響,這裏禁用了偏向鎖(-XX:-UseBiasedLocking)。經過上面程序,能夠看出消除鎖之後性能仍是有比較大提高的。

 

 

四 總結

本文重點介紹了JDk中採用輕量級鎖和偏向鎖等對Synchronized的優化,可是這兩種鎖也不是徹底沒缺點的,好比競爭比較激烈的時候,不但沒法提高效率,反而會下降效率,由於多了一個鎖升級的過程,這個時候就須要經過-XX:-UseBiasedLocking來禁用偏向鎖。下面是這幾種鎖的對比:

 

參考: http://www.cnblogs.com/paddix/p/5405678.html

 

 

五 其餘問題

1 synchronized 和ReentrantLock區別

實現上,synchronized是jvm層面實現的,能夠經過一些監控工具監控synchronized的鎖定,並且在代碼執行出現異常時jvm會自動釋放鎖定。可是ReentrantLock則不行,徹底是經過jdk實現的,須要程序保證鎖必定會釋放,必須講unLock放在finally{}中

功能上,ReentrantLock有鎖投票,定時鎖,中斷鎖,公平非公平鎖等額外功能

(1) 定時鎖

a) lock(), 若是獲取了鎖當即返回,若是別的線程持有鎖,當前線程則一直處於休眠狀態,直到獲取鎖

b) tryLock(), 若是獲取了鎖當即返回true,若是別的線程正持有鎖,當即返回false

c) tryLock(long timeout,TimeUnit unit), 若是獲取了鎖定當即返回true,若是別的線程正持有鎖,等待給定的時間,在等待的過程當中,獲取鎖定返回true,若是等待超時,返回false

(2) 中斷鎖

lockInterruptibly若是獲取了鎖定當即返回,若是沒有獲取鎖定,當前線程處於休眠狀態,直到得到鎖定,或者當前線程被別的線程中斷

(3) 非公平鎖

 reenTrantLock能夠指定是公平鎖仍是非公平鎖。而synchronized只能是非公平鎖。所謂的公平鎖就是先等待的線程先得到鎖。 

性能上,在synchronized優化之前,synchronized的性能是比ReenTrantLock差不少的,( 由於採用的是cpu悲觀鎖,即線程得到是獨佔鎖,獨佔鎖意味着其餘線程只能依靠阻塞來等待線程釋放鎖,而在cpu轉換線程阻塞時會引發線程上下文切換,當有不少線程競爭鎖的時候,會引發cpu頻繁的上下文切換致使效率很低。)可是自從java1.6之後,synchronized引入了偏向鎖,輕量級鎖, 鎖消除,鎖粗化,適應性自旋等,二者的性能就差很少了,在兩種方法均可用的狀況下,官方甚至建議使用synchronized,其實synchronized的優化我感受就借鑑了ReenTrantLock中的CAS技術。都是試圖在用戶態就把加鎖問題解決,避免進入內核態的線程阻塞。

另外,ReentrantLock提供了一個Condition類,用來實現分組喚醒一批線程,而不是像synchronized要麼隨機喚醒一個線程要麼喚醒所有線程。

 

2 synchronized修飾方法和修飾代碼塊時有何不一樣

持有鎖的對象不一樣:

  1. 修飾方法時:this引用的當前實例持有鎖
  2. 修飾代碼塊時:要指定一個對象,該對象持有鎖

 有一個類這樣定義:

public class SynchronizedTest
{
    public synchronized void method1(){}
    public synchronized void method2(){}
    public static synchronized void method3(){}
    public static synchronized void method4(){}
}

那麼,有SynchronizedTest的兩個實例a和b,對於一下的幾個選項有哪些能被一個以上的線程同時訪問呢?

A. a.method1() vs. a.method2()   instance_a instance_a
B. a.method1() vs. b.method1()    instance_a instance_b
C. a.method3() vs. b.method4()  Synchronized.class Synchronized.class
D. a.method3() vs. b.method3()  Synchronized.class Synchronized.class
E. a.method1() vs. a.method3()   instance_a Synchronized.class

答案是什麼呢?BE

 

3 悲觀鎖和樂觀鎖

悲觀鎖: 悲觀鎖假設在最壞的狀況下,而且確保其餘線程不會干擾(獲取正確的鎖)的狀況下才能執行下去。常見實現如獨佔鎖等。安全性更高,但在中低併發程度下效率低

樂觀鎖: 樂觀鎖接觸衝突檢查機制來判斷更新過程當中是否存在其餘線程的干擾,若是存在,這個操做將失敗,而且能夠重試(也能夠不重試)。常見實現如CAS等。樂觀鎖削弱了一致性,但中低併發程序下的效率大大提升。

 

4 異常時是否釋放鎖?同步是否具有繼承性?

當一個線程執行的代碼出現異常時,其所持有的鎖會自動釋放。同步不具備繼承性(聲明爲synchronized的父類方法A,在子類中重寫以後並不具有synchronized的特性)

 

5 是否瞭解自旋鎖

自旋鎖也常常用於線程(進程)之間的同步。通常來講,咱們知道在線程A得到普通鎖以後,若是再有線程B試圖得到鎖,那麼這個線程將會掛起(阻塞)。那麼咱們來考慮這樣一種case: 若是兩個線程資源競爭不是特別激烈,而處理器阻塞一個線程引發的線程上下文切換的代價高於等待資源的代價的時候(鎖的已保持者保持鎖時間比較短),那麼線程b能夠不放棄cpu時間片,而是在"原地"盲等,直到鎖的持有者釋放了該鎖,這就是自選鎖的原理,能夠自旋鎖是一種非阻塞鎖

自旋鎖可能的問題:

1 過多佔據cpu的時間: 若是鎖的當前持有者長時間不釋放該鎖,那麼等待過程將長時間地佔據cpu時間片,致使cpu資源浪費。所以能夠設定一個超時時間,過時等待着放棄cpu時間片

2 誤用有死鎖風險: 當一個線程連續兩次得到自旋鎖(如遞歸),那麼第一次這個線程得到了該鎖,當第二次試圖加鎖的時候,檢測到鎖已被佔用(被本身),那麼這時線程會一直等待本身釋放該鎖,而不能繼續執行形成死鎖。所以遞歸程序使用自旋鎖應該遵循如下原則: 遞歸決不能在持有自旋鎖時調用它本身,也決不能在遞歸調用時試圖得到相同的自旋鎖。

 

6 volatile變量和atomic變量有什麼不一樣

volatile變量能夠確保先行關係,如寫操做會發生在後續的讀操做以前,但它並不能保證原子性。例如volatile修飾count變量那麼count++操做就不是原子性的。而AtomicInteger類提供的atomic方法可讓這種操做具備原子性,如getAndIncrement()方法會原子性地進行增量操做把當前值+1。

 

7 公平鎖和非公平鎖

 在Java的ReentrantLock構造函數中提供了兩種鎖:建立公平鎖和非公平鎖(默認)。代碼以下:

public ReentrantLock() {
       sync = new NonfairSync();
}

 public ReentrantLock(boolean fair) {
       sync = fair ? new FairSync() : new NonfairSync();
}

在公平的鎖上, 線程按照他們發出請求的順序獲取鎖. 但在非公平鎖上,則容許'插隊': 當一個線程請求非公平鎖時,若是在發出請求的同時該鎖變成可用狀態,那麼這個線程會跳過隊列中全部的等待線程而得到鎖。非公平鎖提倡插隊行爲,可是沒法防止某個線程在合適的時候進行插隊。

在公平的鎖中,若是有另外一個線程持有鎖或者有其餘線程在等待隊列中等待這個鎖,那麼新發出的請求的線程將被放入到隊列中。而非公平鎖上,只有當鎖被某個線程持有時,新發出請求的線程纔會被放入隊列中。

非公平鎖性能高於公平鎖性能的緣由:

在恢復一個被掛起的線程與該線程真正運行之間存在着嚴重的延遲。

假設線程A持有一個鎖,而且線程B請求這個鎖。因爲鎖被A持有,所以B將被掛起。當A釋放鎖時,B將被喚醒,所以B會再次嘗試獲取這個鎖。與此同時,若是線程C也請求這個鎖,那麼C極可能會在B被徹底喚醒以前得到、使用以及釋放這個鎖。這樣就是一種共贏的局面:B得到鎖的時刻並無推遲,C更早的得到了鎖,而且吞吐量也提升了。

當持有鎖的時間相對較長或者請求鎖的平均時間間隔較長,應該使用公平鎖。在這些狀況下,插隊帶來的吞吐量提高(當鎖處於可用狀態時,線程卻還處於被喚醒的過程當中)可能不會出現。

相關文章
相關標籤/搜索