Synchronized 精講

1.簡介

1.1 做用

併發場景中,保證同一時刻只有一個線程對有併發隱患的代碼進行操做java

1.2 錯誤案例

需求:兩個線程對 count 變量進行200000次循環增長,預期結果是400000次多線程

public class SynchronizedDemo implements Runnable {
    private static int count = 0;
    static SynchronizedDemo synchronizedInstance = new SynchronizedDemo();
    public static void main(String[] args) {
        Thread t1 = new Thread(synchronizedInstance);
        Thread t2 = new Thread(synchronizedInstance);
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
            System.out.println("count 最終的值爲: " + count);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    @Override
    public void run() {
        synchronized (this) {
            for (int i = 0; i < 200000; i++) {
                count++;
            }
        }
    }
}

結果 :顯然不等於400000次因此出現了運算錯誤
圖片併發

緣由:異步

count++;

該語句包含三個操做:ide

  1. 線程t一、t2 從主內存中獲取共享變量count的值,到本身的工做內存中
  2. 將本身的工做內存中的count值進行+1操做
  3. 將修改完的count變量的值存入到主內存中

圖片

注意:他們是將本身工做內存中的值進行改變刷回主內存,假設當前count的值爲8,t一、t2將count的值複製到本身的工做內存中進行修改,若是此時t1將count變成九、t2此時也將count的值變成9,當t一、t2兩個線程都將值刷回主內存的時候count值爲9,並非10,這個時候就會形成最後的結果和預期的不一致。性能

1.3 正確案例

  1. 代碼塊上加對象鎖 this
    @Override
    public void run() {
    synchronized (this) {
        for (int i = 0; i < 200000; i++) {
            count++;
        }
    }
    }
  2. 在普通方法上加鎖
    @Override
    public synchronized void run() {
    for (int i = 0; i < 200000; i++) {
            count++;
    }
    }
  3. 加.class鎖
    @Override
    public void run() {
    for (int i = 0; i < 200000; i++) {
        synchronized (SynchronizedDemo.class) {
            count++;
        }
    }
    }

    輸出結果:
    在這裏插入圖片描述優化

後文詳細講解四種加 synchronized 的方式this


2.用法

2.1 對象鎖

2.1.1 方法鎖操作系統

修飾普通方法默認鎖對象爲this當前實例對象線程

public synchronized void method() ;在普通方法上面加synchronized

public class SynchronizedDemo3 implements Runnable {
    static SynchronizedDemo3 synchronizedDemo3 = new SynchronizedDemo3();
    public synchronized void method() {
        System.out.println("線程名稱" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("線程名稱" + Thread.currentThread().getName() + "運行完成");
    }
    @Override
    public void run() {
        method();
    }
    public static void main(String[] args) {
        Thread t1 = new Thread(synchronizedDemo3);
        t1.setName("我是線程 t1");
        Thread t2 = new Thread(synchronizedDemo3);
        t2.setName("我是線程 t2");
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

輸出結果: 線程 t1 和線程 t2 執行過程是順序執行的
圖片

2.1.2 同步代碼塊

  1. 代碼示例:沒有加鎖而定義的兩個線程執行的狀況

在這裏插入圖片描述

輸出結果:線程 t1 和線程 t2 交叉執行造成了亂序

圖片


  1. 代碼示例:加Synchronized 鎖而定義的兩個線程執行的狀況,鎖對象的是this(當前對象)

圖片

輸出結果:線程 t1 和線程 t2 執行過程是順序執行的

圖片


  1. 代碼示例:加Synchronized 鎖而定義的兩個線程執行的狀況,鎖對象的是自定義對象

在這裏插入圖片描述

輸出結果:線程 t1 和線程 t2 執行造成了順序,這種狀況下和this沒有什麼區別,可是若是是多個同步代碼塊的話就須要進行自定義對象鎖

圖片


  1. 代碼示例:多個同步代碼塊使用自定義對象鎖,(兩個自定義對象鎖對應兩個同步代碼塊)

在這裏插入圖片描述

輸出結果:輸出順序線程t1 和線程t2 代碼進行了交叉執行,出現了亂序

在這裏插入圖片描述


  1. 代碼示例:多個同步代碼塊使用自定義對象鎖,(一個自定義對象鎖對應兩個同步代碼塊)

圖片

輸出結果:線程 t1 和線程 t2 執行造成了順序

圖片

2.2 類鎖

特色:類鎖只能在同一時間被一個對象擁有(不管有多少個實例想訪問也是一個對象持有它)

2.2.1 synchronized修飾靜態的方法

  1. 代碼示例: synchronized 加在普通方法上面

    public class SynchronizedDemo4 implements Runnable {
    private static SynchronizedDemo4 synchronizedInstance1 = new SynchronizedDemo4();
    private static SynchronizedDemo4 synchronizedInstance2 = new SynchronizedDemo4();
    public synchronized void method() {
        System.out.println("線程名稱" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("線程名稱" + Thread.currentThread().getName() + "運行完成");
    }
    @Override
    public void run() {
        method();
    }
    public static void main(String[] args) {
        Thread t1 = new Thread(synchronizedInstance1);
        t1.setName("我是線程 t1");
        Thread t2 = new Thread(synchronizedInstance2);
        t2.setName("我是線程 t2");
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    }

    輸出結果:輸出順序線程t1 和線程t2 代碼進行了交叉執行,出現了亂序
    圖片

  2. 代碼示例: synchronized 加在靜態方法上面

public static synchronized void method();使用方式

public class SynchronizedDemo4 implements Runnable {
    private static SynchronizedDemo4 synchronizedInstance1 = new SynchronizedDemo4();
    private static SynchronizedDemo4 synchronizedInstance2 = new SynchronizedDemo4();
    public static synchronized void method() {
        System.out.println("線程名稱" + Thread.currentThread().getName());
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("線程名稱" + Thread.currentThread().getName() + "運行完成");
    }
    @Override
    public void run() {
        method();
    }
    public static void main(String[] args) {
        Thread t1 = new Thread(synchronizedInstance1);
        t1.setName("我是線程 t1");
        Thread t2 = new Thread(synchronizedInstance2);
        t2.setName("我是線程 t2");
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

輸出結果:線程 t1 和線程 t2 執行造成了順序
圖片

2.2.2 指定鎖對象爲Class對象

  1. 代碼示例:synchronized 加.class

*synchronized (**SynchronizedDemo5.class**)*

public class SynchronizedDemo5 implements Runnable {
    private static SynchronizedDemo5 synchronizedInstance1 = new SynchronizedDemo5();
    private static SynchronizedDemo5 synchronizedInstance2 = new SynchronizedDemo5();
    void method() {
        synchronized (SynchronizedDemo5.class) { //類鎖只有一把
            System.out.println("線程名稱" + Thread.currentThread().getName());
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("線程名稱" + Thread.currentThread().getName() + "運行完成");
        }
    }
    @Override
    public void run() {
        method();
    }
    public static void main(String[] args) {
        Thread t1 = new Thread(synchronizedInstance1);
        t1.setName("我是線程 t1");
        Thread t2 = new Thread(synchronizedInstance2);
        t2.setName("我是線程 t2");
        t1.start();
        t2.start();
        try {
            t1.join();
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

輸出結果: 線程 t1 和線程 t2 執行造成了順序
圖片

3.性質

3.1 可重入性也叫遞歸鎖

就是說你已經獲取了一把鎖,等想要再次請求的時候不須要釋放這把鎖和其餘線程一塊兒競爭該鎖,能夠直接使用該鎖

好處:避免死鎖

粒度:線程而非調用

3.2案例證實可重入性

  1. 證實同一個方法是可重入

代碼實例:

package synchronizedPage;
public class SynchronizedDemo6 {
    int count = 0;
    public static void main(String[] args) {
        SynchronizedDemo6 synchronizedDemo6 = new SynchronizedDemo6();
        synchronizedDemo6.method();
    }
    private synchronized void method() {
        System.out.println(count);
        if (count == 0) {
            count++;
            method();
        }
    }
}

輸出結果:
在這裏插入圖片描述

  1. 證實可重入不要求是同一個方法

代碼實例:

package synchronizedPage;
public class SynchronizedDemo7 {
    private synchronized void method1() {
        System.out.println("method1");
        method2();
    }
    private synchronized void method2() {
        System.out.println("method2");
    }
    public static void main(String[] args) {
        SynchronizedDemo7 synchronizedDemo7 = new SynchronizedDemo7();
        synchronizedDemo7.method1();
    }
}

輸出結果:
在這裏插入圖片描述

  1. 證實可重入不要求是同一個類中的

代碼實例:

package synchronizedPage;
public class SynchronizedDemo8 {
    public synchronized void doSomething() {
        System.out.println("我是父類方法");
    }
}
class childrenClass extends SynchronizedDemo8{
    public synchronized void doSomething() {
        System.out.println("我是子類方法");
        super.doSomething();
    }
    public static void main(String[] args) {
        childrenClass childrenClass = new childrenClass();
        childrenClass.doSomething();
    }
}

輸出結果:
圖片

3.3 不可中斷

當A線程持有這把鎖時,B線程若是也想要A線程持有的鎖時只能等待,A永遠不釋放的話,那麼B線程永遠的等待下去。

4.底層原理實現

4.1 加鎖和釋放鎖的原理

  • synchronized加在代碼塊上
    public void test() {
    synchronized(this){
    count++;
    }
    }

    利用 javap -verbose 類的名字查看編譯後的文件
    在這裏插入圖片描述

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

  1. 若是monitor的進入數爲0,則該線程進入monitor,而後將進入數設置爲1,該線程即爲monitor的全部者
  2. 若是線程已經佔有該monitor,只是從新進入,則進入monitor的進入數加1【可重入性質
  3. 若是其餘線程已經佔用了monitor,則該線程進入阻塞狀態,直到monitor的進入數爲0,再從新嘗試獲取monitor的全部權

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

monitorexit指令出現了兩次,第1次爲同步正常退出釋放鎖;第2次爲發生異步退出釋放鎖

  • synchronized加在方法上(不管時普通方法仍是靜態方法)
    public synchronized void test() {
    count++;
    }

    利用 javap -verbose 類的名字查看編譯後的文件
    圖片

方法的同步並無經過指令monitorentermonitorexit來完成,不過相對於普通方法,其常量池中多了ACC_SYNCHRONIZED標示符。JVM就是根據該標示符來實現方法的同步的:

當方法調用時,調用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標誌是否被設置,若是設置了,執行線程將先獲取monitor,獲取成功以後才能執行方法體,方法執行完後再釋放monitor。在方法執行期間,其餘任何線程都沒法再得到同一個monitor對象,其實底層仍是monitor對象鎖。

5.Java虛擬機對synchronized的優化

從JDK6開始,就對synchronized的實現機制進行了較大調整,包括使用JDK5引進的CAS自旋以外,還增長了自適應的CAS自旋、鎖消除、鎖粗化、偏向鎖、輕量級鎖這些優化策略。因此synchronized關鍵字的優化使得性能極大提升,同時語義清晰、操做簡單、無需手動關閉,因此推薦在容許的狀況下儘可能使用此關鍵字,同時在性能上此關鍵字還有優化的空間。

5.1 鎖主要存在的四種狀態

無鎖狀態、偏向鎖狀態、輕量級鎖狀態、重量級鎖狀態

鎖的膨脹過程

無鎖狀態 -> 偏向鎖 -> 輕量級鎖 -> 重量級鎖

只能從低到高升級,不會出現鎖的降級

5.2 自旋鎖

所謂自旋鎖,就是指當一個線程嘗試獲取某個鎖時,若是該鎖已被其餘線程佔用,就一直循環檢測鎖是否被釋放,而不是進入線程掛起或睡眠狀態。(減小線程切換)

使用場景: 自旋鎖適用於鎖保護的臨界區很小的狀況,臨界區很小的話,鎖佔用的時間就很短。

缺點:雖然它能夠避免線程切換帶來的開銷,可是它佔用了CPU處理器的時間。若是持有鎖的線程很快就釋放了鎖,那麼自旋的效率就很是好,反之,自旋的線程就會白白消耗掉處理的資源,它不會作任何有意義的工做,因此增長了適應性自選鎖

5.3 適應性自旋鎖

所謂自適應就意味着自旋的次數再也不是固定的,它是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。

線程若是自旋成功了,那麼下次自旋的次數會更加多,由於上次成功了,那麼這次自旋也頗有可能會再次成功,那麼它就會容許自旋等待持續的次數更多。反之,不多可以成功,那麼之後自旋的次數會減小甚至省略掉自旋過程,以避免浪費處理器資源。

5.4 鎖消除

爲了保證數據的完整性,在進行操做時須要對這部分操做進行同步控制,可是在有些狀況下,JVM檢測到不可能存在共享數據競爭,這是JVM會對這些同步鎖進行鎖消除。做爲寫程序的人應該會知道哪裏存在數據競爭,不可能隨便的加鎖。

5.5 鎖粗化

將多個連續的加鎖、解鎖操做鏈接在一塊兒,擴展成一個範圍更大的鎖。雖然咱們平時倡導把加鎖的片斷儘可能小爲了增長併發效率和性能。可是若是一系列的連續加鎖解鎖操做,可能會致使沒必要要的性能損耗,因此引入鎖粗化。

5.6 偏向鎖

在大多數狀況下,鎖不只不存在多線程競爭,並且老是由同一線程屢次得到,爲了讓線程得到鎖的代價更低,引進了偏向鎖。偏向鎖是在單線程執行代碼塊時使用的機制,若是在多線程併發的環境下(即線程A還沒有執行完同步代碼塊,線程B發起了申請鎖的申請),則必定會轉化爲輕量級鎖或者重量級鎖。

引入偏向鎖主要目的是:爲了在沒有多線程競爭的狀況下儘可能減小沒必要要的輕量級鎖執行路徑。由於輕量級鎖的加鎖解鎖操做是須要依賴屢次CAS原子指令的,而偏向鎖只須要在置換ThreadID的時候依賴一次CAS原子指令。

當一個線程訪問同步塊並獲取鎖時,會在對象頭和棧幀中的鎖記錄裏存儲鎖偏向的線程ID,之後該線程進入和退出同步塊時不須要花費CAS操做來爭奪鎖資源,只須要檢查是否爲偏向鎖、鎖標識爲以及ThreadID便可,處理流程以下:

  1. 暫停擁有偏向鎖的線程
  2. 判斷鎖對象是否還處於被鎖定狀態,否,則恢復到無鎖狀態(01),以容許其他線程競爭。是,則掛起持有鎖的當前線程,並將指向當前線程的鎖記錄地址的指針放入對象頭,升級爲輕量級鎖狀態(00),而後恢復持有鎖的當前線程,進入輕量級鎖的競爭模式

偏向鎖的獲取和撤銷流程:

圖片

5.7 輕量級鎖

引入輕量級鎖的主要目的是在沒有多線程競爭的前提下,減小傳統的重量級鎖使用操做系統互斥量產生的性能消耗。當關閉偏向鎖功能或者多個線程競爭偏向鎖致使偏向鎖升級爲輕量級鎖,則會嘗試獲取輕量級鎖,其步驟以下:

  1. 在線程進入同步塊時,若是同步對象鎖狀態爲無鎖狀態(鎖標誌位爲「01」狀態,是否爲偏向鎖爲「0」),虛擬機首先將在當前線程的棧幀中創建一個名爲鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的Mark Word的複製
  2. 拷貝對象頭中的Mark Word複製到鎖記錄(Lock Record)中。
  3. 拷貝成功後,虛擬機將使用CAS操做嘗試將對象Mark Word中的Lock Word更新爲指向當前線程Lock Record的指針,並將Lock record裏的owner指針指向object mark word。若是更新成功,則執行步驟(4),不然執行步驟(5)。
  4. 若是這個更新動做成功了,那麼當前線程就擁有了該對象的鎖,而且對象Mark Word的鎖標誌位設置爲「00」,即表示此對象處於輕量級鎖定狀態
  5. 若是這個更新操做失敗了,虛擬機首先會檢查對象Mark Word中的Lock Word是否指向當前線程的棧幀,若是是,就說明當前線程已經擁有了這個對象的鎖,那就能夠直接進入同步塊繼續執行。不然說明多個線程競爭鎖,進入自旋執行(3),若自旋結束時仍未得到鎖,輕量級鎖就要膨脹爲重量級鎖,鎖標誌的狀態值變爲「10」,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,當前線程以及後面等待鎖的線程也要進入阻塞狀態。

圖片

輕量級鎖的釋放也是經過CAS操做來進行的,主要步驟以下

  1. 經過CAS操做嘗試把線程中複製的Displaced Mark Word對象替換當前的Mark Word
  2. 若是替換成功,整個同步過程就完成了,恢復到無鎖狀態(01)
  3. 若是替換失敗,說明有其餘線程嘗試過獲取該鎖(此時鎖已膨脹),那就要在釋放鎖的同時,喚醒被掛起的線程

問題:

  1. 爲何升級爲輕量鎖時要把對象頭裏的Mark Word複製到線程棧的鎖記錄中呢?

由於在申請對象鎖時須要以該值做爲CAS的比較條件,同時在升級到重量級鎖時,能經過這個比較斷定是否在持有鎖的過程當中此鎖被其餘線程申請過,若是被其餘線程申請了,則在釋放鎖的時候要喚醒被掛起的線程。

  1. 爲何會嘗試CAS不成功以及什麼狀況下會不成功?
  2. CAS自己是不帶鎖機制的,其是經過比較來操做得。假設以下場景:線程A和線程B都在對象頭裏的鎖標識爲無鎖狀態進入,那麼如線程A先更新對象頭爲其鎖記錄指針成功以後,線程B再用CAS去更新,就會發現此時的對象頭已經不是其操做前的對象了,因此CAS會失敗。也就是說,只有兩個線程併發申請鎖的時候會發生CAS失敗。
  3. 此時線程B進行CAS自旋,等待對象頭的鎖標識從新變回無鎖狀態或對象頭內容等於對象,這也就意味着線程A執行結束,此時線程B的CAS操做終於成功了,因而線程B得到了鎖以及執行同步代碼的權限。若是線程A的執行時間較長,線程B通過若干次CAS時鐘沒有成功,則鎖膨脹爲重量級鎖,即線程B被掛起阻塞、等待從新調度

    5.8 重量級鎖

Synchronized是經過對象內部的一個叫作監視器鎖(Monitor)來實現的。可是監視器鎖本質又是依賴於底層的操做系統的Mutex Lock來實現的而操做系統實現線程之間的切換這就須要從用戶態轉換到核心態,這個成本很是高,性能消耗特別嚴重。 所以,這種依賴於操做系統Mutex Lock所實現的鎖咱們稱之爲 「重量級鎖」。

6. 缺點

  1. 效率低
    • 鎖的釋放狀況少
    • 試圖獲取鎖時不能設定超時
    • 不能中斷一個正在試圖得到鎖的線程
  2. 不夠靈活
    • 加鎖和釋放鎖的時候單一,每一個鎖僅有一個單一條件
  3. 不知道是否成功獲取鎖
相關文章
相關標籤/搜索