從鎖升級(jí)的角度理解synchronized

前言

在 Java 中為保證線程安全,可以使用關(guān)鍵字 synchronized 保護(hù)代碼,在多個(gè)線程之間同時(shí)只能有一個(gè)線程執(zhí)行被保護(hù)的代碼。

synchronized 鎖的到底是什么?是對(duì)象,還是代碼塊呢?

保證線程安全已經(jīng)有了 synchronized 為什么又會(huì)出現(xiàn) Lock 呢,二者之間有什么區(qū)別呢?

synchronized 一定比 Lock 性能差嗎?

synchronized 的鎖升級(jí)過程是什么,偏向鎖,輕量級(jí)鎖,自旋鎖,重量級(jí)鎖怎么一步一步實(shí)現(xiàn)的?

synchronized 使用

1、用在靜態(tài)方法

public class SimpleUserSync {
    public static int a = 0;
    // 相當(dāng)于   synchronized (SimpleUserSync.class){a++;}
    public synchronized  static void addA_1() {
        a++;
    }
}

2、用在成員方法上

public class SimpleUserSync {
    public static int a = 0;
    // 相當(dāng)于   synchronized (this){a++;}
    public synchronized  void addA_1() {
        a++;
    }
}

3、用在代碼塊

private static final Object LOCK =new Object();
public static void addA_2() {
    synchronized (LOCK){
        a++;
    }
}

synchronized 原理

原理描述

如果對(duì)一項(xiàng)技術(shù)只停留在會(huì)用的階段是遠(yuǎn)遠(yuǎn)不夠的,原理性的知識(shí)可避免掉到坑里面去。

JDK 1.6 之前,還沒有進(jìn)行 synchronized 的優(yōu)化。那個(gè)時(shí)候 synchronized 只要申請(qǐng)鎖,java 進(jìn)程 就會(huì)從 用戶態(tài) 切換到 內(nèi)核態(tài),需要操作系統(tǒng)配合鎖定,這種切換相對(duì)來說比較占用系統(tǒng)資源。

Lock 的實(shí)現(xiàn)的思想是:線程基于 CAS 操作在 用戶態(tài) 自旋改變內(nèi)部的 state,操作成功即可獲取鎖,操作不成功,繼續(xù)自旋獲取直到成功(分配的 cpu 時(shí)間執(zhí)行完之后,再獲取到 cpu 資源,仍接著自旋獲取鎖)。這種實(shí)現(xiàn)方式在鎖競爭比較小的情況下,效率是比較高的。比起 用戶態(tài) 切換到 內(nèi)核態(tài),讓線程在哪里自旋一會(huì)效率是比較高的。如果一直自旋(比如說 1 分鐘)獲取不到鎖,那用戶態(tài) 切換到 內(nèi)核態(tài) 比你自旋一分鐘效率會(huì)高。

Lock 不一定比 synchronized 效率高,在鎖競爭的幾率極大的情況下,自旋消耗的資源遠(yuǎn)大于 用戶態(tài) 切換到 內(nèi)核態(tài)占用的資源。

JDK 1.6 對(duì) synchronized 做了優(yōu)化。在鎖競爭不大的情況下,使用 偏向鎖輕量級(jí)鎖,這樣只用在 用戶態(tài) 完成鎖的申請(qǐng)。當(dāng)鎖競爭的時(shí)候呢,會(huì)讓其自旋繼續(xù)獲取鎖,獲取 n 次還是沒有獲取到(自適應(yīng)自旋鎖),升級(jí)為 重量級(jí)鎖,用戶態(tài) 切換到 內(nèi)核態(tài),從系統(tǒng)層級(jí)獲取鎖。

鎖升級(jí)的宏觀表現(xiàn)大致是這個(gè)樣子。自適應(yīng)自旋鎖,自旋的次數(shù) n,是 JVM 根據(jù)算法收集其自旋多少次獲取鎖算出來的(JDK 1.6 之后),是一個(gè)預(yù)測值,隨著數(shù)據(jù)收集越來越多,它也越準(zhǔn)確。

synchronized 是通過鎖對(duì)象來實(shí)現(xiàn)的。因此了解一個(gè)對(duì)象的布局,對(duì)我們理解鎖的實(shí)現(xiàn)及升級(jí)是很有幫助的。

對(duì)象布局

<img src="http://oss.mflyyou.cn/blog/20200613211643.png?author=zhangpanqin" alt="image-20200613211643599" style="zoom: 25%;" />

對(duì)象填充,是將一個(gè)對(duì)象大小不足 8 個(gè)字節(jié)的倍數(shù)時(shí),使用 0 填充補(bǔ)齊,為了更高效效率的讀取數(shù)據(jù),64 java 虛擬機(jī),一次讀取是 64 bit(8 字節(jié))。

對(duì)象頭(Object Header)

在64位JVM上有一個(gè)壓縮指針選項(xiàng)-XX:+UseCompressedOops,默認(rèn)是開啟的。開啟之后 Class Pointer 部分就會(huì)壓縮為4字節(jié),對(duì)象頭大小為 12 字節(jié)

Mark Word

圖來自馬士兵教育多線程公開課

偏向鎖位鎖標(biāo)志位 是鎖升級(jí)過程中承擔(dān)重要的角色。

Jol 查看對(duì)象信息

我們可以使用 jol 查看一個(gè)對(duì)象的對(duì)象頭信息,已達(dá)到觀測鎖升級(jí)的過程

jol 官方示例

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.10</version>
</dependency>
public class JOLSample_01_Basic {
    public static void main(String[] args) throws Exception {
        out.println(ClassLayout.parseInstance(new JOLSample_01_Basic.A()).toPrintable());
    }

    public static class A {
        boolean f;
        int a;
    }

}
image-20200613214753341

鎖升級(jí)過程

圖來自馬士兵教育多線程公開課

<font color=red>偏向鎖是默認(rèn)開啟的,但是有個(gè)延遲時(shí)間</font>

# 查看偏向鎖配置的默認(rèn)參數(shù)
java -XX:+PrintFlagsInitial | grep -i biased

# 偏向鎖啟動(dòng)的延遲,Java 虛擬機(jī)啟動(dòng) 4 秒之后,創(chuàng)建的對(duì)象才是匿名偏向,否則是普通對(duì)象
#intx BiasedLockingStartupDelay                 = 4000                                {product}
# 默認(rèn)開啟偏向鎖
#bool UseBiasedLocking                          = true                                {product}

<font color=red>鎖升級(jí)之后,用戶線程不能降級(jí)。GC 線程可以降級(jí)</font>

普通對(duì)象到輕量級(jí)鎖

public class JOLSample_12_ThinLocking {
    public static void main(String[] args) throws Exception {
        A a = new A();
        ClassLayout layout = ClassLayout.parseInstance(a);
        out.println("**** 對(duì)象創(chuàng)建,沒有經(jīng)過鎖競爭");
        out.println(layout.toPrintable());
        synchronized (a) {
            out.println("**** 獲取到鎖");
            out.println(layout.toPrintable());
        }
        out.println("**** 鎖釋放");
        out.println(layout.toPrintable());
    }

    public static class A {
    }
}

因?yàn)槠蜴i的延遲,創(chuàng)建的對(duì)象為普通對(duì)象(偏向鎖位 0,鎖標(biāo)志位 01),獲取鎖的時(shí)候,無鎖(偏向鎖位 0,鎖標(biāo)志位 01) 升級(jí)為 輕量級(jí)鎖(偏向鎖位 0,鎖標(biāo)志位 00),釋放鎖之后,對(duì)象的鎖信息(偏向鎖位 0,鎖標(biāo)志位 01)

<img src="http://oss.mflyyou.cn/blog/20200613221547.png?author=zhangpanqin" alt="image-20200613221547109" style="zoom: 33%;" />

synchronized (a) 的時(shí)候,由 aMark Word 中鎖偏向 0,鎖標(biāo)志位 01 知道鎖要升級(jí)為輕量級(jí)鎖。java 虛擬機(jī)會(huì)在當(dāng)前的線程的棧幀中建立一個(gè)鎖記錄(Lock Record)空間,Lock Record 儲(chǔ)存鎖對(duì)象的 Mark World拷貝和當(dāng)前鎖對(duì)象的指針。

java 虛擬機(jī),使用 CAS 將 a 的 Mark Word(62 位) 指向當(dāng)前線程(main 線程)中 Lock Record 指針,CAS 操作成功,將 a 的鎖標(biāo)志位變?yōu)?00。

<img src="http://oss.mflyyou.cn/blog/20200613224235.png?author=zhangpanqin" alt="image-20200613224235023" style="zoom:50%;" />

CAS 操作失敗。會(huì)依據(jù) a 對(duì)象 Mark Word 判斷是否指向當(dāng)前線程的棧幀,如果是,說明當(dāng)前線程已經(jīng)擁有鎖了,直接進(jìn)入代碼塊執(zhí)行(可重入鎖)。

如果 a 對(duì)象的 Mark Word判斷是另外一個(gè)線程擁有所,會(huì)升級(jí)鎖,鎖標(biāo)志位改為 (10)。

輕量級(jí)鎖解鎖,就是將 Lock Record 中的 a 的 mark word 拷貝,通過 CAS 替換 a 對(duì)象頭中的 mark word ,替換成功解鎖順利完成。

偏向鎖

偏向鎖是比輕量級(jí)鎖更輕量的鎖。輕量級(jí)鎖,每次獲取鎖的時(shí)候,都會(huì)使用 CAS 判斷是否可以加鎖,不管有沒有別的線程競爭。

偏向鎖呢,比如說 T 線程獲取到了 a 對(duì)象的偏向鎖,a 的 Mark Word 會(huì)記錄當(dāng)前 T 線程的 id ,當(dāng)下次獲取鎖的時(shí)候。T 線程再獲取 a 鎖的時(shí)候,只需要判斷 a 的 Mark Word 中的偏向鎖位和當(dāng)前持有 a 鎖的線程 id,而不再需要通過 CAS 操作獲取偏向鎖了。

延遲 6 秒創(chuàng)建 a 對(duì)象,這時(shí)已經(jīng)過了偏向鎖延遲的時(shí)間,創(chuàng)建的對(duì)象為可偏向?qū)ο蟆?/p>

public class JOLSample_13_BiasedLocking {
    public static void main(String[] args) throws Exception {
        TimeUnit.SECONDS.sleep(6);
        final A a = new A();
        ClassLayout layout = ClassLayout.parseInstance(a);
        out.println("**** Fresh object");
        out.println(layout.toPrintable());
        synchronized (a) {
            out.println("**** With the lock");
            out.println(layout.toPrintable());
        }
        out.println("**** After the lock");
        out.println(layout.toPrintable());
    }
    public static class A {
        // no fields
    }
}

<img src="http://oss.mflyyou.cn/blog/20200613231613.png?author=zhangpanqin" alt="image-20200613231613645" style="zoom: 33%;" />

重量級(jí)鎖

寫了一個(gè) demo ,驗(yàn)證 偏向鎖,輕量級(jí)鎖,重量級(jí)鎖的逐漸升級(jí)過程。

public class JOLSample_14_FatLocking {
    public static void main(String[] args) throws Exception {
        // 延遲六秒執(zhí)行例子,創(chuàng)建的 a 為可偏向?qū)ο?        TimeUnit.SECONDS.sleep(6);
        final A a = new A();
        ClassLayout layout = ClassLayout.parseInstance(a);
        out.println("**** 查看初始化 a 的對(duì)象頭");
        out.println(layout.toPrintable());
        // 這里模擬獲取鎖,當(dāng)前獲取到的鎖為 偏向鎖
        Thread t = new Thread(() -> {
            synchronized (a) {
            }
        });
        t.start();
        // 阻塞等待獲取 t 線程完成
        t.join();
        out.println("**** t 線程獲得鎖之后");
        out.println(layout.toPrintable());
        final Thread t2 = new Thread(() -> {
            synchronized (a) {
                // a 的存在兩個(gè)想成競爭鎖,鎖升級(jí)為輕量級(jí)鎖
                out.println("**** t2 第二次獲取鎖");
                out.println(layout.toPrintable());
                try {
                    TimeUnit.SECONDS.sleep(3);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        // 開啟 t3 線程模擬競爭,t3 會(huì)自旋獲得鎖,由于 t2 阻塞了 3 秒,t3 自旋是得不到鎖的,鎖升級(jí)為重量級(jí)鎖
        final Thread t3 = new Thread(() -> {
            synchronized (a) {
                out.println("**** t3 不停獲取鎖");
                out.println(layout.toPrintable());
            }
        });
        t2.start();
        // 為了 t2 先獲得鎖,這里阻塞 10ms ,再開啟 t3 線程
        TimeUnit.MILLISECONDS.sleep(10);
        t3.start();t2.join();t3.join();
        // 驗(yàn)證 gc 可以使鎖降級(jí)
        System.gc();
        out.println("**** After System.gc()");
        out.println(layout.toPrintable());
    }
    public static class A {}
}
**** 查看初始化 a 的對(duì)象頭
com.fly.blog.sync.JOLSample_14_FatLocking$A object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      
**** t 線程獲得鎖之后
com.fly.blog.sync.JOLSample_14_FatLocking$A object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 f0 52 d3 (00000101 11110000 01010010 11010011) (-749539323)

**** t2 第二次獲取鎖
com.fly.blog.sync.JOLSample_14_FatLocking$A object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           f8 38 c3 10 (11111000 00111000 11000011 00010000) (281229560)
      
**** t3 不停獲取鎖
com.fly.blog.sync.JOLSample_14_FatLocking$A object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           5a 1b 82 d2 (01011010 00011011 10000010 11010010) (-763225254)
      
**** After System.gc()
com.fly.blog.sync.JOLSample_14_FatLocking$A object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           09 00 00 00 (00001001 00000000 00000000 00000000) (9)

觀察各階段對(duì)象頭中的 偏向鎖位鎖標(biāo)志位 ??梢钥吹芥i在不斷升級(jí)。然后看到 gc 之后,又變成了無鎖。

t2 線程持有鎖 a輕量級(jí)鎖 的時(shí)候,t3 也在獲得 a 的 輕量級(jí)鎖CAS 修改 a 的 Mark Word 為 t3 所有失敗。導(dǎo)致了鎖升級(jí)為重量級(jí)鎖,設(shè)置 a 的鎖標(biāo)志位為 10,并且將 Mark Word 指針指向一個(gè) monitor對(duì)象,并將當(dāng)前線程阻塞,將當(dāng)前線程放入到 _EntryList 隊(duì)列中。當(dāng) t2 執(zhí)行完之后,它解鎖的時(shí)候發(fā)現(xiàn)當(dāng)前鎖已經(jīng)升級(jí)為重量級(jí)鎖,釋放鎖的時(shí)候,會(huì)喚醒 _EntryList 的線程,讓它們?nèi)?a 鎖。

class ObjectMonitor() {
    _owner        = NULL; // 持有這把鎖監(jiān)視器線程
    _WaitSet      = NULL; // 處于wait狀態(tài)的線程,會(huì)被加入到_WaitSet
    _EntryList    = NULL ; // 處于等待鎖block狀態(tài)的線程,會(huì)被加入到該列表
}

Java Language Specification

Java Language Specification    https://docs.oracle.com/javase/specs/jls/se8/html/index.html
Every object, in addition to having an associated monitor, has an associated wait set. A wait set is a set of threads.

When an object is first created, its wait set is empty. Elementary actions that add threads to and remove threads from wait sets are atomic. Wait sets are manipulated solely through the methods Object.wait, Object.notify, and Object.notifyAll.

調(diào)用對(duì)象的 Object.wait 方法,該線程會(huì)釋放鎖,并將當(dāng)前線程放入到 monitor 的 _WaitSet 隊(duì)列中,等某個(gè)線程調(diào)用 Object.notify, and Object.notifyAll,實(shí)際就是喚醒 _WaitSet 中的線程。


本文由 張攀欽的博客 http://www.mflyyou.cn/ 創(chuàng)作。 可自由轉(zhuǎn)載、引用,但需署名作者且注明文章出處。

如轉(zhuǎn)載至微信公眾號(hào),請(qǐng)?jiān)谖哪┨砑幼髡吖娞?hào)二維碼。微信公眾號(hào)名稱:Mflyyou

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容