從青銅到王者的路線來聊聊Synchronized底層實現原理

一、引言

這篇文章碼了小編***個小時,點個贊不過分吧~~

文本內容有點多,如果有寫錯或者不好地方,還請多多指教~~~~~~~


Table of Contents

一、引言

二、倔強青銅

2.1 多線程一定快嗎?

2.2 上下文切換

2.3 測試上下文切換次數

2.4 Java內存模型

2.5 主內存與工作內存之間的數據交互過程

三、秩序白銀

3.1 多線程帶來的可見性問題

3.2 多線程帶來的原子性問題

3.3 多線程帶來的有序性問題

四、榮耀黃金

4.1 sync可重入特性

4.2 sync不可中斷特性 

4.3 反彙編學習sync原理

五、尊貴鉑金

5.1 montior 監視器鎖

5.2 monitor 競爭

5.3. monitor 等待

5.4 monitor 釋放

六、永恆鑽石

6.1 CAS 介紹

6.2 sync 鎖升級過程

6.3 對象的佈局

七、至尊星耀

7.1 偏向鎖

7.2 輕量級鎖

7.3 自旋鎖

7.4 消除鎖

7.5 鎖粗化

八、最強王者

終章:平時寫代碼如何對synchroized優化


二、倔強青銅

2.1 多線程一定快嗎?

我們先來看下面一段代碼,有兩個方法對各自a、b屬性進行累加操作,其中concurrency方法是採用多線程進行操作,結果如下:

/**
 * @Auther: IT賤男
 * @Date: 2020/3/9 10:37
 * @Description:
 */
public class ConcurrencyTest {

    // 累加次數
    private static final long count = 10000L;

    public static void main(String[] args) throws InterruptedException {
        concurrency();
        serial();
    }

    /**
     * 多線程累加
     *
     * @throws InterruptedException
     */
    private static void concurrency() throws InterruptedException {
        long start = System.currentTimeMillis();

        // 啓動新線程執行運行操作
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                int a = 0;
                for (int i = 0; i < count; i++) {
                    a += 5;
                }
            }
        });
        thread.start();

        int b = 0;
        for (int i = 0; i < count; i++) {
            b--;
        }
        // 等線程執行完
        thread.join();
        long end = System.currentTimeMillis() - start;
        System.out.println("concurrency 總共耗時" + end);
    }

    /**
     * 單線程累加
     */
    private static void serial() {
        long start = System.currentTimeMillis();
        int a = 0;
        for (int i = 0; i < count; i++) {
            a += 5;
        }
        int b = 0;
        for (int i = 0; i < count; i++) {
            b--;
        }
        long end = System.currentTimeMillis() - start;
        System.out.println("serial 總共耗時" + end);
    }
}

那這邊的答案是"不一定"的,小編測試了幾組數據如下(抽取部分結果):

多線程與單線程效率測試
循環次數 單線程執行 多線程執行 效率
1萬 0 1
1萬 0 0 相等
十萬 2 2 相等
十萬 1 1 相等

由以上的結果可以明確我們的答案是正確的,那爲什麼多線程在某些情況下會比單線程還要慢呢? 這是因爲多線程有創建和上下文切換的開銷。

2.2 上下文切換

那什麼是上下文切換呢?

目前來說即使是單核處理器也支持多線程執行代碼,CPU通過給個線程分配CPU時間片來實現這個機制。時間片是CPU分配給各個線程的時間,因爲時間片一般是幾十毫秒,所以CPU需要通過不停地切換線程來執行。 假設當我們線程A獲得CPU分配的時間片等於10毫秒,執行10毫秒之後,CPU需要切換到線程B去執行程序。等線程B的時間片執行完事了,又切回線程A繼續執行。

顯然易見,我們CPU相當於是循環的切換上下文,來達到同時執行的效果。當前執行完一個時間片後會切換下一個任務。但是在切換前會保存當前任務的狀態,方便下次切換會這個任務的時候,可以恢復這個任務之前的狀態。 所以任務從保存到再次被加載的過程就是一次上下文切換。

2.3 測試上下文切換次數

這裏我們需要使用一個命令叫做:"vmstat 1",這個命令是linux系統上的,可對操作系統的進程、虛擬內存、CPU活動進行監控。看下圖CS(Content Switch) 表示上下文切換的次數,從圖可見系統一般CS的值維持在600~800之間,當我們一直在運行ConcurrencyTest程序時,很明細發現CS飆升到1000以上。 

2.4 Java內存模型

在我們學習sync原理之前,我們需要搞清楚Java內存模型的一個概念知識。很重要、很重要、很重要

Java內存模型全稱:Java Memory Model ,簡稱Java內存模型或者JMM,Java線程之間的通信由JMM來控制,JMM決定一個線程對共享變量的寫入,何時對另外一個線程可見。我們由圖可見,線程之間的共享變量是存儲在主內存當中,每一個線程都有一個屬於自己的本地內存(也可以叫做工作內存),這個本地內存中存儲了主內存當中的共享變量。就相當於把主內存的共享變量copy了一份給自己。爲了提供效率,線程是不會直接與主內存進行打交道,而是通過本地內存來進行數據的讀取。

如果線程A與線程B之間要通信,需要經歷下面兩個步驟:

1 )線程A把本地內存A中更新過的共享變量,刷新到主內存當中去。

2 )線程B到主內存中重新讀取更新後的共享變量。

2.5 主內存與工作內存之間的數據交互過程

那麼主內存與工作內存之間的交互經過了哪些步驟呢?

lock(鎖定):作用於主內存的變量,把一個變量標識爲一條線程獨佔狀態。

unlock(解鎖):作用於主內存的變量,把一個處於鎖定狀態的變量釋放出來,釋放之後的變量纔可以被其他線程鎖定。

read(讀取):作用於主內存的變量,讀取主內存變量的值。

load(載入):作用於主內存的變量,把read操作從主內存中得到的變量值放入到線程本地內存的變量副本中。

use(使用):作用於工作內存的變量,把工作內存中的一個變量傳遞給執行引擎。

assign(賦值):作用域工作內存的變量,它把一個從執行引擎接收到的值賦值給工作內存的變量。

store(存儲):作用域工作內存的變量,把工作內存中的一個變量值傳輸到主內存中,以便隨後的write操作。

write(寫入):作用域工作內存的變量,把stroe操作從工作內存中一個變量的值傳送到主內存的變量中去。

上個筆記圖: 更加詳細的解釋如上幾個步驟

JMM是一種規範,其中定義幾條規則,小編挑選出相對本文比較重要的:

1、如果想要把一個變量從主內存複製到工作內存,就需要按照順序執行read和load操作,如果把變量從工作內存同步到主內存中,就要按照順序執行store和write操作。但Java內存模型只要求上述操作必須按照順序執行,而沒有保證必須是連續執行。

2、程序中如果有同步操作纔會有lock和unlock操作,一個變量在同一個時刻只允許一條線程對其進行lock操作,但lock操作可以被同一條線程重複執行多次,執行多次後,必須執行相對應次數但unlock操作,變量纔會被解鎖。lock和unlock必須成對出現。

3、如果對一個變量執行lock操作,將會清空工作內存中此變量的值,在執行引擎使用這個變量前需要重新執行load或者assign操作初始化變量但值。

4、java內存模型同步規則小編暫時提到這麼多,感興趣的小夥伴可以自行去了解一下

三、秩序白銀

3.1 多線程帶來的可見性問題

什麼是可見性問題呢?

所謂可見性:一個線程對主內存的修改可以及時被其他線程觀察到。

當一個共享屬性,被線程二修改了,但是線程一無法獲得最新的值,導致死循環。原因Java內存模型也說清楚了,線程是和本地內存做交互的。

1、線程一把falg屬性讀取到線程私有的本地內存中,值爲true。

2、線程二把falg屬性修改爲false,並且刷新到主內存當中,但是線程一它是不知道falg被修改了。

public class SyncExample5 {

    static boolean falg = true;

    // 鎖對象
    static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {

        // 線程一
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (falg) {
                    // 默認不可見,死循環,放開以下注釋即可解決不可見操作
                    
                    // 方式一,加上sycn操作即可解決可見性問題
                    // synchronized (lock){}

                    // 方式二, println 方法實現加上了同步機制,保證每次輸出都是最新值
                    // System.out.println(falg);
                }
            }
        }).start();

        // 睡眠兩秒
        Thread.sleep(2000L);

        // 線程二
        new Thread(new Runnable() {
            @Override
            public void run() {
                falg = false;
                System.out.println("falg 值已修改");
            }
        }).start();
    }
}

sync怎麼解決可見性問題呢?

這個就涉及到本地內存與工作內存交互的步驟了,還記得文本上面有講的8個步驟嗎?

如果程序中有加同步的機制,則會有Lock、Unlock操作,Lock操作會使本地內存中的屬性失效,從而去主內存中重新讀取數據。

3.2 多線程帶來的原子性問題

什麼是原子性問題呢?

所謂原子性:提供了互斥訪問,同一個時刻只能有一個線程來對它進行操作。

這裏一次任務累加1千次,同時啓動5個線程進行累加,最後的結果正常應該是5000纔對,但由於多線程會造成不一樣的結果。

public class SyncExample6 {

    static int index = 0;

    static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {

        // index 累加 1000次,使用lambda表達式
        Runnable task = () -> {
            // 不加sync則不能保證原子操作
            // synchronized (lock) {
                for (int i = 0; i < 1000; i++) {
                    index++;
                }
            // }
        };

        // 啓動五個線程來執行任務
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(task);
            thread.start();
        }

        // 爲了代碼直觀直接睡眠等待結果,實際需要調用線程的join方法等待線程結束
        Thread.sleep(2000L);
        System.out.println("index = " + index);
    }
}

我們使用java命令來編譯以上代碼:

javac SyncExample6.java 

javap -p -v SyncExample6.class ,這樣我們就能看到sync到底在底層做了什麼事。

編譯代碼之後找到“lambda$main$0”,因爲我們同步機制是寫在main方法中,用lambda表達式所寫。 

 private static void lambda$main$0();
    descriptor: ()V
    flags: ACC_PRIVATE, ACC_STATIC, ACC_SYNTHETIC
    Code:
      stack=2, locals=3, args_size=0
         0: iconst_0
         1: istore_0
         2: iload_0
         3: sipush        1000
         6: if_icmpge     39
         9: getstatic     #18                 // Field lock:Ljava/lang/Object;
        12: dup
        13: astore_1
        14: monitorenter
        15: getstatic     #14                 // Field index:I
        18: iconst_1
        19: iadd
        20: putstatic     #14                 // Field index:I
        23: aload_1
        24: monitorexit
        25: goto          33
        28: astore_2

造成原子性的問題的原因是什麼?

這個就涉及到文章一開始所講的上下文切換的知識點,index ++ 一共涉及到4條指令,如下

15: getstatic     #14  // 步驟一:獲取index值
18: iconst_1           // 步驟二:準備常量1
19: iadd               // 步驟三:相加操作
20: putstatic     #14  // 步驟四:重新賦值

以上這4條指令就是index ++ 的四個步驟,假設我們線程一進來,執行到步驟三,這個時候CPU切換線程。切換到線程二,線程二執行步驟一,這個時候index的值還是等於0,因爲線程一併沒有執行步驟四就被切換上下文了。 等線程二執行完成,又切回到線程一,線程一會接着執行步驟三,並不會重新獲取index的值,這就導致計算結果不正確了。

sync怎麼解決原子性問題呢?

  14: monitorenter
  15: getstatic     #14                 // Field index:I
  18: iconst_1
  19: iadd
  20: putstatic     #14                 // Field index:I
  23: aload_1
  24: monitorexit

當我們加上了sync同步機制之後, 會插入monitorenter、monitorexit兩條指令。 

又到了假設環節:假設線程一執行到步驟三,被切換到線程二,當我們線程二執行monitorenter這個指令會發現,這個對象已經被其他線程佔用了,所以就只能等待着不會進行操作。現在又切回到線程一,線程一操作完整個步驟執行monitorexit來釋放鎖。這個時候線程二纔可以獲得鎖。 這樣一操作就能保證同一個時刻只能有一個線程來對它進行操作,從而保證原子性。

monitorenter指令是在編譯後插入到同步代碼塊到開始位置,而monitorexit是插入到同步代碼塊結束位置和異常位置。JVM需要保障每個monitorenter必須有對應的monitorexit。任何一個對象都會有一個monitor來關聯,當且一個monitor被持有後,它就處理鎖定狀態。當線程執行到monitorenter指令的時候,將會嘗試獲取對象所對應的monitor的所有權,即嘗試獲取鎖對象。

3.3 多線程帶來的有序性問題

什麼是有序性問題呢?

有序性,指的是程序中代碼的執行順序,Java在編譯時和運行時會對代碼進行優化,會導致程序最終的執行順序不一定就是我們編寫代碼時的順序。

// 指定使用併發測試
@JCStressTest 
// 預測的結果與類型,附加描述信息,如果1,4 則是ok,如果結果有爲0也能勉強接受
@Outcome(id = {"1", "4"}, expect = Expect.ACCEPTABLE, desc = "ok") 
@Outcome(id = {"0"}, expect = Expect.ACCEPTABLE_INTERESTING, desc = "denger")
// 標註需要測試的類
@State
public class TestJMM {

    int num = 0;
    boolean ready = false;

    @Actor
    public void actor1(I_Result r) {
        if (ready) {
            r.r1 = num + num;
        } else {
            r.r1 = 1;
        }
    }

    @Actor
    public void actor2(I_Result r) {
        num = 2;
        ready = true;
    }
}

是時候貼一段代碼湊文章字數了,這裏代碼用了Jcstress高併發測試框架,目的是爲了能夠演示有序性所導致到問題。

小夥伴可以先仔細看以上代碼,假設actor1、actor2 各有一個線程進來,想想 r.r1 的值會產生幾種情況。 

小編告訴你吧,其實答案有三種,分別是:1、4、0

出現1的情況:

1)假設 actor1先獲得執行權,ready = false ,則 r.r1 = 1;

2)假設 actor2先獲得執行權,執行到num = 2, 線程切換到actor1,ready還是爲false,r.r1 = 1;

出現4的情況:

1)假設actor2先獲得執行權,執行完,此時ready = true,num = 2 ,等到在執行actor1時,結果爲4;

出現0的情況:

1)這裏就是重點了,假設actor2獲得執行權,由於指令重排序導致actor2代碼順序更換。

這個時候執行到ready = true,線程切換到actor1,這個時候ready已經等於true了,但是num還是0,所以就出現了0的情況。

    @Actor
    public void actor2(I_Result r) {
        // 由於指令重排序,導致下面代碼更換了順序,如下:
        ready = true;
        num = 2;
    }

我們用壓測來執行以下代碼吧,使用maven 執行 clean install,會生成一個jar包,直接用命令啓動jar包就行了,Jcstress使用方式小編就不多說了,感興趣的小夥伴可以自行學習下, 執行的結果也符合我們預期的值。

sync怎麼解決有序性問題呢?

這個時候只需要在actor1和actor2分別加上鎖操作,由於它們的鎖對象都是同一個,哪怕由於指令重排序執行到actor2的ready = true,這個時候線程切換到actor1,但是有加鎖所以actor1也只能等着。 等到actor2 把 num = 2 執行完,actor1 纔可以拿到鎖對象。

// 指定使用併發測試
@JCStressTest
// 預測的結果與類型,附加描述信息
@Outcome(id = {"1"}, expect = Expect.ACCEPTABLE, desc = "ok")
// 因爲sync解決有序性問題,不會有0的出現,爲了方便觀察結果,我們把4設置成能勉強接受的值
@Outcome(id = {"4"}, expect = Expect.ACCEPTABLE_INTERESTING, desc = "denger")
@Outcome(id = {"0"}, expect = Expect.ACCEPTABLE_INTERESTING, desc = "denger")
// 標註需要測試的類
@State
public class TestJMM {

    int num = 0;
    boolean ready = false;

    Object lock = new Object();

    @Actor
    public void actor1(I_Result r) {
        synchronized (lock) {
            if (ready) {
                r.r1 = num + num;
            } else {
                r.r1 = 1;
            }
        }
    }

    @Actor
    public void actor2(I_Result r) {
        synchronized (lock) {
            num = 2;
            ready = true;
        }
    }
}

測試結果如下:

四、榮耀黃金

4.1 sync可重入特性

什麼是可重入呢?

即一個線程可以多次執行synchronzied重複獲取同一把鎖。 sync底層鎖對象中包含了一個計數器(recursions 變量),會記錄線程獲得了幾次鎖。 當我們同一個線程獲得了鎖,計數器則會+1,執行完同步代碼塊,計數器-1。 直到計數器的數量爲0,就釋放這個鎖對象。

public class SyncExample8 {

    public static void main(String[] args) {
        new MyThread().start();
    }

}

class MyThread extends Thread {
    @Override
    public void run() {
        synchronized (MyThread.class) {
            System.out.println(getName() + "進入了同步代碼塊1");
            synchronized (MyThread.class) {
                System.out.println(getName() + "進入了同步代碼塊2");
            }
        }
    }
}

運行結果如下,我們可以很明細的看出在輸出“同步代碼塊1”之後,不需要等待鎖釋放,即可進入第二個同步代碼塊。這樣的一個特性可以避免死鎖的發生,也可以更好的封裝代碼(即:同步代碼塊中的代碼,可以分成多個方法來寫)。  

輸入結果如下:

Thread-0進入了同步代碼塊1
Thread-0進入了同步代碼塊2

4.2 sync不可中斷特性 

不可中斷只指,線程二在等待線程一釋放鎖的時候,是不可被中斷的。

當一個線程獲得鎖之後,另外一個線程一直處於堵塞或者等待狀態,前一個線程不釋放鎖,後一個線程會一直被阻塞或等待,所以sync是不可中斷鎖。

public class SyncExample9 {

    private static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        Runnable run = () -> {
            synchronized (lock) {
                String name = Thread.currentThread().getName();
                System.out.println(name + "進入同步代碼塊");
                try {
                    // 讓線程一持有鎖
                    Thread.sleep(888888L);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        };

        // 創建線程一先執行同步代碼快
        Thread t1 = new Thread(run);
        t1.start();

        // 主線程睡眠一下,保證上面線程先執行
        Thread.sleep(1000L);

        // 後開啓線程取執
        Thread t2 = new Thread(run);
        t2.start();

        System.out.println("開始中斷線程二");
        // 強行線程二中斷
        t2.interrupt();
        System.out.println("線程一狀態" + t1.getState());
        System.out.println("線程二狀態" + t2.getState());

    }

}

當我們線程一進入同步代碼之後,一直持有鎖,並且睡眠了(也證實了sleep方法睡眠不會釋放鎖對象)。

此時線程二啓動去嘗試獲取鎖,獲取失敗之後就變成堵塞狀態,哪怕我們強行中斷線程二,最後看到線程二的狀態仍是堵塞的。

Thread-0進入同步代碼塊
開始中斷線程二
線程一狀態TIMED_WAITING
線程二狀態BLOCKED

4.3 反彙編學習sync原理

使用javap反彙編java代碼,引入monitor概念。

public class SyncExample10 {

    private static Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        synchronized (lock) {
            System.out.println("1");
        }
    }

    public synchronized void test() {
        System.out.println("1");
    }

}

我們使用javac、javap 兩個命令對SyncExample10來進行編譯

javac SyncExample10.java 

javap -v -p  SyncExample10.class  

編譯後的指令就如下啦,我們主要看main方法裏面的內容,着重看 monitorenter、monitorexit 兩個指令

 public static void main(java.lang.String[]) throws java.lang.InterruptedException;
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: getstatic     #2                 
         3: dup
         4: astore_1
         5: monitorenter    // 這裏
         6: getstatic     #3                 
         9: ldc           #4                
        11: invokevirtual #5                  
        14: aload_1
        15: monitorexit  // 這裏
        16: goto          24
        19: astore_2
        20: aload_1
        21: monitorexit  // 這裏
        22: aload_2
        23: athrow
        24: return

monitorenter 指令

當我們進入同步代碼塊的時候會先執行monitorenter指令,每一個對象都會和一個monitor監視器關聯,監視器被佔用時會被鎖住,其他線程無法來獲取該monitor。當其他線程執行monitorente指令時,它會嘗試去獲取當前對象對應的monitor的所有權。

monitor裏面有兩個很重要成員變量:

owner: 當一個線程獲取到該對象的鎖,就把線程當前賦值給owner。 

recursions:會記錄線程擁有鎖的次數,重複獲取鎖當前變量也會+1,當一個線程擁有monitor後,其他線程只能等待。

monitorenter執行流程如下:

1)若monitor的進入次數爲0時,線程可以進入monitor,並將monitor進入的次數(recursions)+1,當前線程成爲montiro的owner(所有者);

2)若線程已擁有monitor的所有權,允許它重入monitor,進入一次次數+1 (可重複特性);

3)若其他線程已經佔有monitor,那麼當前嘗試獲取monitor的線程會被阻塞,一直到monitor進入次數爲變0,才能重新被再次獲取。

monitorexit 指令

既然我們同步代碼塊進入時計數器會執行+1操作,那麼我們退出的時候,計數器當然要執行-1;

要注意,能夠執行monitorexit指令的線程,一定是擁有當前對象的monitor所有權的線程。 當我們執行monitorexit指令計數器減到爲0時,當前線程就不再擁有monitor所有權。其他被阻塞的線程即可再一次去嘗試獲取這個monitor的所有權。

大家仔細看看上面編譯出來的指令,其實monitoreexit是有兩個的,爲什麼呢?

因爲需要保證如果同步代碼塊執行拋出了異常,則也需要釋放鎖對象。等到下次面試官問你,synchronized如果拋異常了,會不會釋放鎖對象,答案是:會的。

ACC_SYNCHRONIZED 修飾

剛剛我們所看到的是mian方法中同步代碼塊所編譯後的指令,以下是同步方法編譯後指令

可以看到同步方法在反彙編後,會增加ACC_SYNCHRONIZED修飾,會隱式調用monitorenter、mointorexit,在執行同步方法前會調用monitorenter,在方法結束之後會調用monitorexit。

 public synchronized void test();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #4                  // String 1
         5: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 19: 0
        line 20: 8

五、尊貴鉑金

5.1 montior 監視器鎖

剛剛上文有提到每一個對象都會和一個monitor監視器關聯,真正的鎖都是靠monitor監視器來完成,

那monitor到底是個啥玩意呢? 小編偷偷告訴你,其實monitor是用C++所寫。

http://hg.openjdk.java.net/jdk8/jdk8/hotspot/ 網址都給你們找好了,點擊左邊zip、gz下載都行。 網速不好的同學可以在網上“hotspot 源碼下載” ,下載之後文件如下圖:

下載之後爲了方便瀏覽,小編建議你們可以去下載一個CLion工具來看代碼,或者直接用文本編輯器打開也行。

java對象怎麼和monitor關聯的呢?

這裏就牽扯到另外一個知識點,我們每一個對象在內存中分爲三塊區域:對象頭(Header)、實例數據(Instance Data)、對齊填充(Padding)。而這個對象頭就包含了一個monitor的引用地址,指向了一個具體的monitor對象。

monitor裏面包含了什麼?

我們先找到monitor對象對應的源文件:/src/share/vm/runtime/objectMonitor.hpp,往下翻可以看到ObjectMonitor的構造方法,裏面有一系列成員屬性。

ObjectMonitor() {
    _header       = NULL;
    _count        = 0;
    _waiters      = 0,
    _recursions   = 0;        // 記錄線程的重入次數
    _object       = NULL;    
    _owner        = NULL;     // 標識擁有該monitor的線程
    _WaitSet      = NULL;     // 存儲正處於wait狀態的線程
    _WaitSetLock  = 0 ;   
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;    // 存放競爭失敗線程的單向鏈表
    FreeNext      = NULL ;
    _EntryList    = NULL ;    // 存儲等待鎖block狀態的線程
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
}

挑幾個比較重要的來說一下:

_recursions:這個在上文講monitorenter指令的時候有提到,就是記錄線程線程獲取鎖的次數,獲取到鎖該屬性則會+1,退出同步代碼塊則-1;

_owner:當一個線程獲得了monitor的所有權,則該對象會保存到_owner中。

_WaitSet:當線程入wait狀態,則會存儲到_WaitSet當中。

 _cxq :當線程之間開始競爭鎖,如果鎖競爭失敗後,則會加入_cxq鏈表中。

_EntryList:當新線程進來嘗試去獲取鎖對象,又沒有獲取到對象的時候,則會存儲到_EntryList當中。

5.2 monitor 競爭

什麼情況下會競爭?

當多個線程執行同步代碼塊的時候,這個時候就會出現鎖競爭。

當線程執行同步代碼塊時,先執行monitorenter指令, 這個時候會調用interpreterRuntime.cpp中的函數

源文件如下:src/share/vm/interpreter/interpreterRuntime.cpp,搜索:monitorenter

IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
  // 代碼省略

  // 是否用偏向鎖
  if (UseBiasedLocking) {
    // Retry fast entry if bias is revoked to avoid unnecessary inflation
    ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
  } else {
      // 重量級鎖
    ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
  }
  
  // 代碼省略
IRT_END

線程之間如何競爭鎖的?

對於重量級鎖,monitorenter函數中會調用 :ObjectSynchronizer::slow_enter,

最終調用到這個函數上:ObjectMonitor::enter,源碼位於:/src/share/vm/runtime/objectMonitor.cpp

void ATTR ObjectMonitor::enter(TRAPS) {
  // The following code is ordered to check the most common cases first
  // and to reduce RTS->RTO cache line upgrades on SPARC and IA32 processors.
  Thread * const Self = THREAD ;
  void * cur ;

  // 1、通過CAS操作嘗試把monitor的_owner設置成當前線程
  cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
  if (cur == NULL) {
     assert (_recursions == 0   , "invariant") ;
     assert (_owner      == Self, "invariant") ;
     return ;
  }

  // 2、重入鎖
  if (cur == Self) {
     // 重入鎖計數器也需要+1
     _recursions ++ ;
     return ;
  }

  // 3、如果是當前線程第一次進入該monitor
  if (Self->is_lock_owned ((address)cur)) {
    assert (_recursions == 0, "internal state error");
    // 計數器+1
    _recursions = 1 ;
    // 把當前線程設置賦值給_owner
    _owner = Self ;
    OwnerIsThread = 1 ;
    return ;
  }
 
   // TODO-FIXME: change the following for(;;) loop to straight-line code.
   for (;;) {
      jt->set_suspend_equivalent();

      // 4、獲取鎖失敗,則等待鎖釋放
      EnterI (THREAD) ;

      if (!ExitSuspendEquivalent(jt)) break ;

  
      _recursions = 0 ;
      _succ = NULL ;
      exit (false, Self) ;

      jt->java_suspend_self();
    }
}

此處省略了鎖的自旋優化等操作,文章後面會講到 

以上代碼具體的操作流程如下:

1)通過CAS嘗試把monitor的_owner屬性設置爲當前線程

2)如果之前設置的owner等於當前線程,說明當前線程再次進入monitor,即重入鎖,執行_recursions ++ ; 記錄重入次數。

3)如果當前線程是第一次進入monitor,設置_recursions = 1,_owner = 當前線程,該線程成功獲得鎖並返回。

4、如果獲取鎖失敗,等待鎖釋放

5.3. monitor 等待

上文有提到,如果鎖競爭失敗後,會調用EnterI (THREAD) 函數,還是在objectMonitor.cpp源碼中搜索:::EnterI

以下代碼小編省略了部分:

void ATTR ObjectMonitor::EnterI (TRAPS) {
    Thread * Self = THREAD ;
    assert (Self->is_Java_thread(), "invariant") ;
    assert (((JavaThread *) Self)->thread_state() == _thread_blocked   , "invariant") ;

    // 嘗試獲取鎖
    if (TryLock (Self) > 0) {
        assert (_succ != Self              , "invariant") ;
        assert (_owner == Self             , "invariant") ;
        assert (_Responsible != Self       , "invariant") ;
        return ;
    }

    // 自旋操作嘗試獲取鎖
    if (TrySpin (Self) > 0) {
        assert (_owner == Self        , "invariant") ;
        assert (_succ != Self         , "invariant") ;
        assert (_Responsible != Self  , "invariant") ;
        return ;
    }

    // 當前線程封裝成ObjectWaiter對象node,狀態設置成ObjectWaiter::TS_CXQ
    ObjectWaiter node(Self) ;
    Self->_ParkEvent->reset() ;
    node._prev   = (ObjectWaiter *) 0xBAD ;
    node.TState  = ObjectWaiter::TS_CXQ ;

    // 通過CAS把node節點push到_cxq隊列中
    ObjectWaiter * nxt ;
    for (;;) {
        node._next = nxt = _cxq ;
        if (Atomic::cmpxchg_ptr (&node, &_cxq, nxt) == nxt) break ;

        // Interference - the CAS failed because _cxq changed.  Just retry.
        // As an optional optimization we retry the lock.
        // 再次嘗試獲取鎖
        if (TryLock (Self) > 0) {
            assert (_succ != Self         , "invariant") ;
            assert (_owner == Self        , "invariant") ;
            assert (_Responsible != Self  , "invariant") ;
            return ;
        }
    }

    // 掛起線程
    for (;;) {
        // 掛起之前再次嘗試獲取鎖
        if (TryLock (Self) > 0) break ;
        assert (_owner != Self, "invariant") ;

        if ((SyncFlags & 2) && _Responsible == NULL) {
           Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ;
        }

        // park self
        if (_Responsible == Self || (SyncFlags & 1)) {
            TEVENT (Inflated enter - park TIMED) ;
            Self->_ParkEvent->park ((jlong) RecheckInterval) ;
            // Increase the RecheckInterval, but clamp the value.
            RecheckInterval *= 8 ;
            if (RecheckInterval > 1000) RecheckInterval = 1000 ;
        } else {
            TEVENT (Inflated enter - park UNTIMED) ;
            // 通過park將當前線程掛起,等待鎖釋放
            Self->_ParkEvent->park() ;
        }
        // 嘗試獲取鎖
        if (TryLock(Self) > 0) break ;
    }

    return ;
}

以上代碼具體流程概括如下: 

1)進入EnterI後,先會再次嘗試獲取鎖對象

2)把當前線程封裝成ObjectWaiter對象node,狀態設置成ObjectWaiter::TS_CXQ ;

3)在for循環中,通過CAS把node節點push到_cxq(上文有提到這個屬性)列表中,同一時刻可能有多個線程把自己到node節點push到_cxq列表中。

4)node節點push到_cxq 列表之後,通過自旋嘗試獲取鎖,如果還是沒有獲取到鎖,則通過park將當前線程掛起,等待喚醒。

5)當前線程被喚醒時,會從掛起到點繼續執行,通過TryLock再次嘗試鎖。

5.4 monitor 釋放

什麼時候會釋放monitor?

當線程執行完同步代碼塊時,調用monitorexit指令釋放鎖,這個時候鎖就會被釋放。

還是在objectMonitor.cpp源碼中搜索:::exit

釋放monitor過程是什麼?

exit函數代碼如下,當然小編也有大部分的刪減,留下比較主要的代碼部分。

void ATTR ObjectMonitor::exit(bool not_suspended, TRAPS) {

   // 判斷計數器,不等於0則執行-1
   if (_recursions != 0) {
     _recursions--;        // this is simple recursive enter
     TEVENT (Inflated exit - recursive) ;
     return ;
   }

   // w = 最後被喚醒的線程
   ObjectWaiter * w = NULL ;
   int QMode = Knob_QMode ;
    
   // QMode == 2,會繞過EntryList隊列,從cxq隊列中獲取線程用於競爭鎖
   if (QMode == 2 && _cxq != NULL) {
    w = _cxq ;
    assert (w != NULL, "invariant") ;
    assert (w->TState == ObjectWaiter::TS_CXQ, "Invariant") ;
    // 喚醒線程
    ExitEpilog (Self, w) ;
    return ;
   }
    
  // QMode還有還好幾種策略,小編就不一一列舉了

  // 最後拿到了要被喚醒的線程
  w = _EntryList  ;
  if (w != NULL) {
    guarantee (w->TState == ObjectWaiter::TS_ENTER, "invariant") ;
    // 喚醒線程
    ExitEpilog (Self, w) ;
    return ;
  }


}

觀察以上代碼,都需要調用ExitEpilog函數來喚醒線程, 還是在objectMonitor.cpp源碼中搜索:::ExitEpilog

void ObjectMonitor::ExitEpilog (Thread * Self, ObjectWaiter * Wakee) {
   assert (_owner == Self, "invariant") ;

   _succ = Knob_SuccEnabled ? Wakee->_thread : NULL ;
   ParkEvent * Trigger = Wakee->_event ;

   Wakee  = NULL ;

   // Drop the lock
   OrderAccess::release_store_ptr (&_owner, NULL) ;
   OrderAccess::fence() ;                               // ST _owner vs LD in unpark()

   if (SafepointSynchronize::do_call_back()) {
      TEVENT (unpark before SAFEPOINT) ;
   }

   DTRACE_MONITOR_PROBE(contended__exit, this, object(), Self);
   
   // 最重要的時候這裏,調用unpark來進行喚醒
   Trigger->unpark() ;

   // Maintain stats and report events to JVMTI
   if (ObjectMonitor::_sync_Parks != NULL) {
      ObjectMonitor::_sync_Parks->inc() ;
   }
}

以上代碼具體流程概括如下: 

1)退出同步代碼塊時會讓_recursions - 1,當_recursions的值等於0的時候,說明線程釋放了鎖。

2)根據不同的策略(由QMode來指定),最終獲取到需要被喚醒的線程(代碼中是:w)

3)最後調用ExitEpilog函數中,最終由unpark來執行喚醒操作。

六、永恆鑽石

6.1 CAS 介紹

CAS的英文單詞CompareAndSwap的縮寫,比較並替換。CAS需要有3個操作數:內存地址V、舊的預期值A、即將要更新的目標值B。

CAS指令執行時,當內存地址V的值與預期值A相等時,將目標值B保存到內存當中,否則就什麼都不做。 整個比較並替換的操作是一個原子操作。

CAS是樂觀鎖技術,當多個線程嘗試使用CAS同時更新同一個變量時,只有其中一個線程能更新變量的值,而其他線程都失敗,失敗的線程並不會掛起,而是被告知這次競爭失敗,並可以再次嘗試。

優點:可以避免優先級倒置和死鎖等危險,競爭比較便宜,協調發生在更細的力度級別,允許更高程度的並行機制等等。

缺點:

1、循環時間長開銷很大,如果CAS失敗,會一直進行嘗試,如果CAS長時間一直不成功,可能會給CPU帶來很大的開銷。

2、只能保證一個共享的原子操作,我們可以使用循環CAS的方式來保證原子操作,但是對多個共享變量操作時,循環CAS就無法保證操作的原子性,這個時候就可以用鎖來保證原子性。

3、ABA問題,如果內存地址V初次讀取的值是A,並且在準備賦值的時候檢查仍然爲A,那我們就能說它的值沒有被其他線程改變過嗎?

如果在這段期間它的值曾被改成了B,後來又被改回A,那CAS就會誤認爲它從來沒有被改變過,這個漏洞稱之爲CAS操作的ABA問題。Java併發包爲了解決這個問題,提供了一個帶有標記的原子引用類 “AtomicStampendReference”,它可以通過控制變量值的版本來保證CAS的正確性。

因此,在使用CAS前要考慮清楚“ABA”問題是否會影響程序併發性的正確性,如果需要解決ABA問題,改用傳統的互斥同步可能比原子類更高效

介紹完CAS,那麼肯定就多多少少介紹以下實現原理,我們以AtomicInteger爲例,它是JDK中提供能夠保障原子性操作的類。

    /**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }

我們點進去看它裏面的方法,拿incrementAndGet方法爲例子,這個方法是在原有值的基礎上進行+1操作,它的實現調用Unfafe類的方法,我們再點進去看。

   public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

Unfafe類使Java擁有了像C語言的指針一樣操作內存空間的能力,同時也帶來了指針問題,過度的使用Unsafe類會使得出錯的機率變大。因此Java官方不建議使用的,Unsafe對象也不能直接調用,只能通過放射來獲取。

小編這裏說一下getandAddInt方法的執行流程,

var1:傳進來的是this,也就是AtomicInteger實例對象;

var2:偏移量,通過結合var1就能夠獲得在內存中的最新值;

var4:要進行累加的值,也就是 1 ;

先通過var1+var2 獲取到內存中最新的值,然後再調用compareAndSwapInt方法,這個方法又會通過var1+var2參數獲取內存中最新的值,與var5的值進行比較,如果比較成功,這把var5+var4的結果更新到內存中去。如果不成功,則繼續循環操作。也就是我們剛剛介紹CAS所說,比較並替換。

6.2 sync 鎖升級過程

在JDK1.5以前,sync是一個重量級的鎖,在1.6以後,對sync做了大量的各種優化,包含偏向鎖、輕量級鎖、適應性自旋、鎖消除、鎖粗化等等,這些技術都是爲了線程之間更加高效的共享數據,以及解決競爭問題,從而達到程序的執行效率。

當然鎖肯定升級的過程:無鎖 —— 偏向鎖 —— 輕量級鎖 —— 重量級鎖。

每個不同的鎖都有不同的使用藏場景,在瞭解各種鎖的特性之前,我們還需要搞清楚對象在內存中的佈局!

6.3 對象的佈局

我們每一個對象在內存中分爲三塊區域:對象頭(Header)、實例數據(Instance Data)、對齊填充(Padding)。

對象頭:

當一個線程嘗試訪問sync修飾的代碼塊時,它先要獲得鎖,這個鎖對象是存在對象頭中的。

以Hotspot虛擬機爲例,對象頭裏面主要包含了Mark Word(字段標記)、Klass Pointer (指針類型),如果對象是數組類型,還包含了數組的長度。

怎麼又扯到Hotspot虛擬機呢? 小夥伴可以這樣理解,JVM可以理解爲一套規範,而Hotspot是具體的虛擬機產品。 就好比如你們要找女朋友、或者男朋友,既然找朋友是不是就要有一定的要求或者規範,JVM就可以看作這個規範,而Hotspot就是具體的男朋友或者女朋友了。

你不信? System.out.println(System.getProperties());  運行這個代碼吧,找找你們java.vm.name等於什麼。

java.vm.name=Java HotSpot(TM) 64-Bit Server VM

Mark Word :裏默認存儲對象的HashCode、分代年齡和鎖位標記。 這個也是sync鎖實現的重要部分了,在運行期間,Mark Word 裏存儲的數據會隨着鎖標位置的變化而變化。 在64位虛擬機下,Mark Word是64bit大小的,其存儲結構如圖:

Mark Word 64位虛擬機存儲結構
鎖狀態 25 bit 31 bit 1 bit 4 bit 1 bit 2 bit
    cms_free 分代年齡 偏向鎖 鎖位標識
無鎖 unused HashCode     0 01
偏向鎖 ThreadID(54bit)、Epoch(2bit)     1 01
輕量級鎖 指向佔中鎖記錄的指針       00
重量級 指向互斥量(重量級鎖)的指針       10

以上這個表格數據不能亂來對不對,我們可以查看源碼:src/share/vm/oops/markOop.hpp

裏面註釋寫的很清楚了,對照以下注釋反映出上面的表格,更加直觀。

//  32 bits:
//  --------
//             hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
//             JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)
//             size:32 ------------------------------------------>| (CMS free block)
//             PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
//
//  64 bits:
//  --------
//  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
//  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
//  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
//  size:64 ----------------------------------------------------->| (CMS free block)

Klass Pointer :用於存儲對象的類型指針,該指針指向它的類元數據,JVM通過這個指針確定是哪個對象的實例。  

對象頭 = Mark Word + Klass Point 在未開啓指針壓縮對情況下所佔大小:

以64位系統爲例:Mark Word = 8 bytes,指針類型 = 8 bytes ,對象頭 = 16 bytes = 128bits;

實例數據:

類中定義的成員變量

對齊填充: 

對齊填充並不是必然存在的,也沒有什麼特殊的意義,它僅僅只是佔位符的作用。由於HotPort VM的自動內存管理系統要求對象起始地址必須是8字節的整倍數,當對象的實例數據部分沒有對齊時,就需要通過對齊填充來不補齊。

 

說了這麼多,都是概念性的東西,說誰不會說對不對,接下來我們嘗試在把一個對象在內存中都佈局輸出看下:

先引入這個jar包,它能夠提供我們想要看到的東西,使用方式如下:

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.10</version>
</dependency>
public class SyncExample4 {

    static Apple apple = new Apple();

    public static void main(String[] args) {
        // 這裏使用ClassLayout來查看
        System.out.println(ClassLayout.parseInstance(apple).toPrintable());
    }
}

class Apple {
    private int count;
    private boolean isMax;

}

以下內容就是我們Java對象內存分佈所查看到的內容,我們能直接看到內容有object header 翻譯過來就是對象頭呀, 再往下看就是loss due to the next object alignment,這個就是對齊填充,由於Apple 有一個boolean的屬性,佔了一個字節,所以計算機爲了提高執行效率和GC垃圾回收的效率,進行了7個字節的填充(這裏涉及到CPU運行小編就不多扯了)。

com.example.concurrency.sync.Apple object internals:
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4           (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4           (object header)                           43 c0 00 f8 (01000011 11000000 00000000 11111000) (-134168509)
     12     4       int Apple.count                               0
     16     1   boolean Apple.isMax                               false
     17     7           (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total

看到這裏我們確實能夠確定對象頭的存在,那麼對象頭裏面不是說用31 bit存儲了HashCode嗎? 怎麼沒看見 

我們再來執行一段代碼, 計算一下apple的HashCode是多少,看運行結果可知,本次運行apple的HashCode是7ea987ac,我們再看看對應VALUE值也發生了改變。這裏有一個概念,由於存在大小端存儲方式,我們需要從後往前看。 

public class SyncExample4 {

    static Apple apple = new Apple();

    public static void main(String[] args) {
        // 查看HashCode
        System.out.println(Integer.toHexString(apple.hashCode()));
        System.out.println(ClassLayout.parseInstance(apple).toPrintable());
    }
}

class Apple {
    private int count;
    private boolean isMax;
}
7ea987ac
# WARNING: Unable to attach Serviceability Agent. You can try again with escalated privileges. Two options: a) use -Djol.tryWithSudo=true to try with sudo; b) echo 0 | sudo tee /proc/sys/kernel/yama/ptrace_scope
com.example.concurrency.sync.Apple object internals:
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           01 ac 87 a9 (00000001 10101100 10000111 10101001) (-1450726399)
      4     4           (object header)                           7e 00 00 00 (01111110 00000000 00000000 00000000) (126)
      8     4           (object header)                           43 c0 00 f8 (01000011 11000000 00000000 11111000) (-134168509)
     12     4       int Apple.count                               0
     16     1   boolean Apple.isMax                               false
     17     7           (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total

細心一點的小夥伴就會發現,上文不是說了對象頭一共佔了16個字節嗎? 這裏三個object header 才12個字節也不對呀?

這裏JVM默認會開啓指針壓縮,我們可以通過參數把它關掉:

在打印看結果,就是16個字節。

 OFFSET  SIZE                                        TYPE DESCRIPTION                               VALUE
      0     4                                             (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4                                             (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4                                             (object header)                           80 68 f5 1f (10000000 01101000 11110101 00011111) (536176768)
     12     4                                             (object header)                           02 00 00 00 (00000010 00000000 00000000 00000000) (2)

最後總結以下: Java對象有三個部分組成:對象頭、實例數據、對齊填充,其中對象頭又包含Mark Word、Klass Pointer(如果對象是數組類型,還包含了數組的長度)。

七、至尊星耀

Mark Word 64位虛擬機存儲結構
鎖狀態 25 bit 31 bit 1 bit 4 bit 1 bit 2 bit
    cms_free 分代年齡 偏向鎖 鎖位標識
無鎖 unused HashCode     0 01
偏向鎖 ThreadID(54bit)、Epoch(2bit)     1 01
輕量級鎖 指向佔中鎖記錄的指針       00
重量級 指向互斥量(重量級鎖)的指針       10

7.1 偏向鎖

偏向鎖的原理

在大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一個線程多次獲得。爲了讓線程獲得的鎖的代價更低,從而引入偏向鎖的。

我們對照Mark Word存儲結構來看,當一個線程訪問同步代碼快之後,會把Mark Word中的偏向鎖標識由0改爲1,並且存儲當前線程的ID,以後該線程進入和退出同步代碼的的時候,則不需要進行CAS操作來加鎖和解鎖。只需要簡單的測試一下對象頭裏是否存儲着指向當先線程的偏向鎖,如果結果成功,表示線程已經獲得了鎖。如果失敗,需要再查看Mark Word中的偏向鎖標識是否設置成1,如果沒有,則使用CAS競爭鎖。

我們可以使用代碼來觀察下:

偏向鎖在Java 6 和Java 7中默認是開啓的,但是他在應用程序啓動幾秒鐘之後才激活,我們需要先來關閉延遲啓動。

public class SyncExample4 {

    public static void main(String[] args) {
        Apple apple = new Apple();
        apple.start();
    }
}

class Apple extends Thread {

    private Object lock = new Object();

    @Override
    public void run() {
        synchronized (lock) {
            System.out.println(ClassLayout.parseInstance(lock).toPrintable());
        }
    }
}
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 d8 86 22 (00000101 11011000 10000110 00100010) (579262469)
      4     4        (object header)                           9c 7f 00 00 (10011100 01111111 00000000 00000000) (32668)
      8     4        (object header)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

 由於大小端存儲,原本偏向鎖和鎖位標識是在最後的,現在我們需要看最前8位數:00000101

 第一個1 代表是偏向鎖,並且鎖標識01,和我們的表格也能夠對應上。

偏向鎖的撤銷

偏向鎖使用了一種等到競爭出現了才釋放鎖的機制,所以當其他線程來進行爭奪鎖的時候,持有偏向鎖的線程纔會釋放鎖。但是偏向鎖的撤銷的時候,需要等到一個全局安全點,也就是在這個時間點上沒有正在執行的字節碼。 它首先會暫停所有線程(包括擁有偏向鎖的線程),然後在判斷當前是不是偏向鎖,如果偏向鎖標識等於1,就撤銷回0;

偏向鎖的好處

偏向鎖的好處也很顯而易見,只有同一個線程來訪問同步代碼塊的時候,效率是很高的,只需要判斷當先線程和Mark Word裏面存儲的線程是否是一致就行了。如果程序中大多數的鎖都是不同的線程來進行訪問,那麼這個時候偏向鎖就是多餘的了。

我們可以通過JVM參數來關閉偏向鎖:-XX:-UseBiasedLocking

7.2 輕量級鎖

什麼是輕量級鎖

輕量級鎖是在JDK6中加入的新型鎖機制,引入輕量級鎖的目的是爲了,在多線程交替執行同步代碼塊的情況下,儘量避免重量級鎖引起的性能消耗,但是如果多線程在同一時刻進入臨界區,會導致輕量級鎖膨脹升級爲重量級鎖,所以輕量級鎖的出現並非代替重量級鎖。

棧楨

我們在JVM虛擬中,有堆和棧,而在棧中還包含了我們對象的各種方法,一個方法就相當於一個“棧楨”。其中方法中也是可以存儲內容的,其中就包含了Displaced Mark Word,這個有什麼作用呢? 接着往下看

 

輕量級鎖原理

線程在執行同步代碼快之前,JVM會現在當前線程的棧楨中創建用於存儲鎖記錄的空間,並將對象頭中的Mark Word 複製到鎖記錄當中。這個就是我們剛剛所說Displaced Mark Word了。JVM利用CAS操作嘗試將對象的Mark Word更新爲指向鎖記錄的指針。如果成功,當先線程獲得鎖並且將鎖位標識改爲00,如果失敗了則需要判斷當前對象的Mark Word是否指向當前線程的指針,如果是則表示當線程已經持有對象的鎖,執行同步代碼快。如果不是隻能說明該鎖對象被其他線程佔用,這時的輕量級需要膨脹到重量級鎖,鎖位標識改爲10,後面的線程進入阻塞狀態。

輕量級鎖的釋放

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

7.3 自旋鎖

自旋鎖是在JDK1.4中就已經引入了,默認是關閉的,在JDK1.6中默認幾句開啓了。

爲什麼要用自旋鎖呢?自旋鎖通俗易懂的來說,就是循環去獲取鎖。 因爲在我們鎖升級的過程中,如果線程競爭鎖失敗,就立即被掛起,然後等待被喚醒,其實這個時候性能開銷是比較大的。可能線程還正在被掛起的時候,鎖已經被釋放掉了,所以就有了自旋鎖的操作。

當線程競爭鎖失敗之後,先自旋來嘗試獲取鎖,如果鎖被佔用的時間很短,自旋等待的效果就非常好。反之,如果鎖被佔用的時間很長,那麼自旋的線程只會拜拜消耗處理器資源,而不會有任何的作用。自旋默認的默認值是10次,可使用參數-XX:PreBlockSpin來更改。

適應性自選鎖

由於我們自旋鎖可能迴帶來一定的性能消耗,但是我們又不清楚設置自旋次數多少合適,所以這個時候適應性自選鎖就來了。適應性自選就意味着自旋的時間不再固定了,而是由前一次在同一個鎖的自旋時間及所得擁有者的狀態來決定。假設在同一個同步代碼塊上自旋10次就能獲得鎖,那麼虛擬機就會認爲這次也能夠獲得鎖,還允許自旋的時間稍微長一點。 那麼再假設一個同步代碼塊從來都沒有自旋成功過,那麼虛擬機就可能省略自旋的過程,以免浪費性能。

光說還不如來點實際的代碼,源碼路徑:src/share/vm/runtime/objectMonitor.cpp ,搜索::TrySpin_VaryDuration

int ObjectMonitor::TrySpin_VaryDuration (Thread * Self) {
 // 固定自旋次數
    int ctr = Knob_FixedSpin ;
    if (ctr != 0) {
        while (--ctr >= 0) {
            if (TryLock (Self) > 0) return 1 ;
            SpinPause () ;
        }
        return 0 ;
    }

    // 適應式自旋
    for (ctr = Knob_PreSpin + 1; --ctr >= 0 ; ) {
      if (TryLock(Self) > 0) {
        // 成功後,修改自旋的時間
        int x = _SpinDuration ;
        if (x < Knob_SpinLimit) {
           if (x < Knob_Poverty) x = Knob_Poverty ;
           _SpinDuration = x + Knob_BonusB ;
        }
        return 1 ;
      }
      SpinPause () ;
    }
}

7.4 消除鎖

我們先來看以下代碼:

 public String getContent() {
        return new StringBuffer().append("a").append("b").append("c").toString();
    }
 @Override
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }

StringBuffer中的append是同步的,但是我們這個getContent這個方法,每次都是新new一個對象來進行操作。所以不同的線程進來,鎖住的對象也是不同的,所以就根本不會造成線程上的問題。 這個時候虛擬機即使編譯器(JIT)在運行時,對一些代碼上的要求同步,但是被檢測到不可能存在共享數據競爭的鎖進行消除,這個就是鎖消除。

7.5 鎖粗化

什麼是鎖粗化呢? JVM會探測一連串細小的操作都是用同一個對象加鎖,將同步代碼塊的範圍放大,放到這串操作的外面,這樣只需要加一次鎖即可。

 public static void main(String[] args) {
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < 100; i++) {
            sb.append("a");
        }
    }
 @Override
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }

看上面代碼,StringBuffer的append的方法裏面是有加同步關鍵字的,然而我們在外面循環了100次,就要進入鎖和退出鎖各100次,所以這個時候JVM就會把鎖粗化。 把append方法同步關鍵字去掉,擴大在外面來,就只需要進入和退出1次即可。

 public static void main(String[] args) {

        StringBuffer sb = new StringBuffer();
        synchronized (sb) {
            for (int i = 0; i < 100; i++) {
                sb.append("a");
            }
        }
    }

 

八、最強王者

終章:平時寫代碼如何對synchroized優化

終於打上王者了,不要以爲打上王者就行啦,還有一些日常操作我們還需要注意到的。

減少sync的同步代碼塊的範圍:

同步代碼塊精簡,執行就會更快,可能輕量級鎖、自旋鎖就搞定了,不會升級爲重量級鎖。

   public static void main(String[] args) {

        StringBuffer sb = new StringBuffer();
        synchronized (sb) {
            System.out.println("a");
        }
    }

降低sync鎖的粒度:

鎖的對象也是有講究的,假設test01和02本身沒有任何業務相關的代碼,但是鎖的對象越是同一個,這樣豈不是併發效率就很低了。

public class SyncExample4 {

    public void test01(){
        synchronized (SyncExample4.class){}
    }
    public void test02(){
        synchronized (SyncExample4.class){}
    }
}

讀寫分離:

我們儘量可以做到,讀的時候不加鎖,寫入和刪除的時候加鎖,這樣就可以保證多個線程同時來讀取數據。

舉個例子:

HashTable容器競爭激烈的併發環境下,效率低是因爲多個線程競爭同一把鎖,假如容器有多把鎖,每一把鎖用於鎖住容器中一部分數據,那麼多線程訪問容器裏面不同的數據段的數據時,線程間不會存在鎖競爭,從而有效提高併發訪問率。這就是ConcurrentHashMap的鎖分段技術,將數據分成一段一段的存儲,然後把每一段數據分配一把鎖,當一個線程佔用鎖訪問其中一段數據的時候,其他段段數據也能被其他線程訪問。

 

 

小編我終於寫完了,溫馨提示光看一遍印象不會特別深刻,最好能夠實際動手操作以下,看下源碼如何實現之類的。

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