【Java并發(fā)004】原理層面:synchronized關(guān)鍵字全解析

一、前言

synchronized關(guān)鍵字在需要原子性、可見性和有序性這三種特性的時候都可以作為其中一種解決方案,看起來是“萬能”的。的確,大部分并發(fā)控制操作都能使用synchronized來完成。在多線程并發(fā)編程中Synchronized一直是元老級角色,很多人都會稱呼它為重量級鎖,但是隨著Java SE1.6對Synchronized進行了各種優(yōu)化之后,有些情況下它并不那么重了,本文詳細介紹了Java SE1.6中為了減少獲得鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級鎖,以及鎖的存儲結(jié)構(gòu)和升級過程。

面試官問題:為什么說jdk6之后synchronized不再那么重量了?
JDK1.6之前的內(nèi)建鎖,即synchronized,jdk1.6之后,內(nèi)建鎖=無鎖+偏向鎖+輕量級鎖+重量級鎖,并加入鎖升級機制,所以說,jdk6以后,synchronized不再那么重量了,其實,不是synchronized不再那么重量,而是一定要不得已才使用synchronized。
小結(jié):對于為什么說jdk6之后synchronized不再那么重量了,解釋是,jdk6以前,只有無鎖+重量級鎖兩種狀態(tài),加鎖就是重量級鎖,阻塞調(diào)用,jdk6以后,程序員再使用synchronized的時候,底層有無鎖+偏向鎖+輕量級鎖+重量級鎖,四種狀態(tài),一定要不得已才使用synchronized,不那么重量的了,當然對于程序員的使用synchonized來說,是透明的,所以說jdk6之后synchronized不再那么重量了。

二、synchronized的使用

2.1 synchronized五種情況

image.png

五種情況主要是方法級別鎖和代碼級別鎖,還有就是鎖對象的不同,方法級別鎖和代碼級別鎖很好理解,看代碼就懂,鎖對象不同是什么意思?

第一種情況和第三種情況,形成鎖競爭:普通方法和代碼塊中使用this是同一個監(jiān)視器(鎖),即某個具體調(diào)用該代碼的對象

第二種情況和第四種情況,形成競爭:靜態(tài)方法和代碼塊中使用該類的class對象是同一個監(jiān)視器,任何該類的對象調(diào)用該段代碼時都是在爭奪同一個監(jiān)視器的鎖定

小結(jié):同步兩因素:第一多線程,第二鎖對象

  1. 多線程:就是因為存在不止一個線程才能形成競爭,只有一個線程你和誰競爭,所以一旦設(shè)置同步,多線程必不可少;
  2. 鎖對象:鎖對象表示的是競爭的對象,即多個線程之間競爭什么,競爭的對象相同才能形成競爭,競爭的對象不同是無法形成競爭的,舉一反三,比如非同步方式(沒有synchronized修飾),就啥都不競爭,和同步原子沒半毛錢關(guān)系,代碼中通過對競爭的資源加鎖解鎖形成原子操作,所以一旦涉及同步,鎖對象必不可少。

2.2 synchronized五種情況的使用

2.2.1 synchronized五種情況的使用

public class Synchronized {
    //synchronized關(guān)鍵字可放于方法返回值前任意位置,本示例應(yīng)當注意到sleep()不會釋放對監(jiān)視器的鎖定
    //實例方法
    public synchronized void instanceMethod() {
        for (int i = 0; i < 5; i++) {
            System.out.println("instanceMethod");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    //靜態(tài)方法
    public synchronized static void staticMethod() {
        for (int i = 0; i < 5; i++) {
            System.out.println("staticMethod");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void thisMethod() {
        //this對象
        synchronized (this) {
            for (int i = 0; i < 5; i++) {
                System.out.println("thisMethod");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public void classMethod() {
        //class對象
        synchronized (Synchronized.class) {
            for (int i = 0; i < 5; i++) {
                System.out.println("classMethod");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public void anyObject() {
        //任意對象
        synchronized ("anything") {
            for (int i = 0; i < 5; i++) {
                System.out.println("anyObject");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

2.2.2 第一種情況和第三種情況

普通方法和代碼塊中使用this是同一個監(jiān)視器(鎖),即某個具體調(diào)用該代碼的對象

   public static void main(String[] args) {
        Synchronized syn = new Synchronized();
        for (int i = 0; i < 10; i++) {
            new Thread() {
                @Override
                public void run() {
                    syn.thisMethod();
                }
            }.start();
            new Thread() {
                @Override
                public void run() {
                    syn.instanceMethod();
                }
            }.start();
        }
    }

我們會發(fā)現(xiàn)輸出結(jié)果總是以5個為最小單位交替出現(xiàn),證明sychronized(this)和在實例方法上使用synchronized使用的是同一監(jiān)視器。如果去掉任一方法上的synchronized或者全部去掉,則會出現(xiàn)instanceMethod和thisMethod無規(guī)律的交替輸出。

2.2.3 第二種情況和第四種情況

靜態(tài)方法和代碼塊中使用該類的class對象是同一個監(jiān)視器,任何該類的對象調(diào)用該段代碼時都是在爭奪同一個監(jiān)視器的鎖定

public static void main(String[] args) {
    for (int i = 0; i < 10; i++) {
        Synchronized syn = new Synchronized();
        new Thread() {
            @Override
            public void run() {
                syn.staticMethod();
            }
        }.start();
        new Thread() {
            @Override
            public void run() {
                syn.classMethod();
            }
        }.start();

    }
}

輸出以5個為最小單位交替出現(xiàn),證明兩段代碼是同一把鎖,如果去掉任一synchronnized則會無規(guī)律交替出現(xiàn)。

三、synchronized源碼解析

3.1 對象頭 + Mark Word

3.1.1 同步鎖對象的對象頭(對象頭=Mark Word + Class Metadata Address + Array length,三個每一個占一個字寬)

線程同步兩要素=多線程+鎖對象,對于鎖對象,這里介紹其對象頭

對象頭=Mark Word + Class Metadata Address + Array length,三個每一個占一個字寬。
Markdown存放對象的hashCode或鎖信息;
Class Metadata Address存放存儲到對象類型數(shù)據(jù)的指針;
ArrayLength,數(shù)組類型特有,存放數(shù)組類型長度。
如果對象是數(shù)組類型,則虛擬機用3個字寬(Word)存儲對象頭;
如果對象是非數(shù)組類型,則用2字寬存儲對象頭。

在32位虛擬機中,1字寬等于4字節(jié),即32bit,64位虛擬機中,1字寬=8字節(jié)=64bit,如表所示:

長度 內(nèi)容 說明
32/64bit Mark Word 存儲對象的hashCode或鎖信息等
32/64bit Class Metadata Address 存儲到對象類型數(shù)據(jù)的指針
32/64bit Array length 數(shù)組的長度(如果當前對象是數(shù)組)

3.1.2 對象頭的Mark Word(無鎖狀態(tài) + 四種數(shù)據(jù))

Mark Word無鎖狀態(tài),如下:

鎖狀態(tài) 25bit 4bit 1bit是否偏向鎖 2bit鎖標志位
無鎖狀態(tài) 對象的hashCode 對象分代年齡 0 01

Mark Word可能變化為存儲以下4種數(shù)據(jù),如表所示:

image.png

3.1.3 小結(jié):對象頭 + Mark Word

對象頭三個部分:
對象頭=Mark Word + Class Metadata Address + Array length,三個每一個占一個字寬。
Markdown存放對象的hashCode或鎖信息;(無鎖狀態(tài):29bit hashcode+ 3bit lock)
Class Metadata Address存放存儲到對象類型數(shù)據(jù)的指針;
ArrayLength,數(shù)組類型特有,存放數(shù)組類型長度。

小結(jié):五種狀態(tài):

  1. 無鎖狀態(tài):
    hashcode 哈希碼 29bit
    biased_lock: 偏向鎖標識位,1bit
    lock: 鎖狀態(tài)標識位,2bit 01

  2. 偏向鎖(線程id 23 + 偏向時間戳 2 + 分代年齡 4 = 29 + lock 3):
    JavaThread: 保存持有偏向鎖的線程ID,23bit
    epoch: 保存偏向時間戳,2bit(到40,升級為輕量級鎖)
    age: 保存對象的分代年齡,4bit
    biased_lock: 偏向鎖標識位,1bit
    lock: 鎖狀態(tài)標識位,2bit 01
    其中,線程id 23 + 偏向時間戳 2 + 分代年齡 4 都是偏向鎖特有,后兩個都是涉及時間

  3. 輕量鎖和重量鎖(30+2):
    ptr: monitor的指針(就是鎖指針,同步代碼塊的鎖由monitorenter和monitorexit完成,同步方法的鎖由ACC_SYNCHRONIZED修飾,底層是一致的),30bit
    lock: 鎖狀態(tài)標識位,2bit 00 10

  4. GC標志(30+2)
    空,30bit
    lock: 鎖狀態(tài)標識位,2bit 11

記憶方法:偏向鎖和無鎖一起記憶,其他三個,輕量級鎖和重量級鎖一起記憶。

3.2 javap命令:從.java文件到.class文件,synchronized代碼級別和方法級別的區(qū)別(代碼級別:monitorenter+monitorexit,方法級別:ACC_SYNCHRONIZED)

金手指:JVM第二篇就用過這個javap命令

3.2.1 synchronized代碼級別鎖底層:monitorenter+monitorexit

我們寫個demo看下,使用javap命令,查看JVM底層是怎么實現(xiàn)synchronized

public class TestSynMethod1 {
    synchronized void hello() {

    }

    public static void main(String[] args) {
        String anything = "anything";
        synchronized (anything) {   // 任意字符串作為鎖對象
            System.out.println("hello word");
        }
    }
}

線程同步兩要素:多線程和對象鎖,這里只有main線程,對象鎖是可以隨便找一個字符串變量,因為這里只要測試一個synchronized關(guān)鍵字底層是如何保證原子性的,synchronized關(guān)鍵字底層實現(xiàn),不用模擬多線程對競爭資源爭奪。

同步塊的jvm實現(xiàn),可以看到它通過monitorenter和monitorexit實現(xiàn)鎖的獲取和釋放。通過圖片中的注解可以很好的解釋synchronized的特性2,當代碼段執(zhí)行結(jié)束或出現(xiàn)異常后會自動釋放對監(jiān)視器的鎖定。

在這里插入圖片描述

3.2.2 synchronized方法級別鎖底層:ACC_SYNCHRONIZED

如果synchronized在方法上,那就沒有上面兩個指令,取而代之的是有一個ACC_SYNCHRONIZED修飾,表示方法加鎖了。然后可以在常量池中獲取到鎖對象,實際實現(xiàn)原理和同步塊一致,后面也會驗證這一點

在這里插入圖片描述

辨析方法級別鎖和代碼級別鎖

synchronized代碼級別鎖底層:monitorenter+monitorexit
synchronized方法級別鎖底層:ACC_SYNCHRONIZED
聯(lián)系:如果synchronized在方法上,底層使用ACC_SYNCHRONIZED修飾該方法,然后在常量池中獲取到鎖對象,實際實現(xiàn)原理和同步塊一致

四、原理:鎖升級整個流程(偏向鎖獲取 + 偏向鎖撤銷 + 輕量鎖加鎖 + 輕量鎖解鎖)

1、只能升級不能降級:目的是為了提高 獲得鎖和釋放鎖的效率
2、升級順序:無鎖狀態(tài) 0 01、偏向鎖狀態(tài)101、輕量級鎖狀 態(tài)000和重量級鎖狀態(tài)010

4.1 鎖升級

Java SE 1.6為了減少獲得鎖和釋放鎖帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”,在Java SE 1.6中,鎖一共有4種狀態(tài),級別從低到高依次是:無鎖狀態(tài)、偏向鎖狀態(tài)、輕量級鎖狀 態(tài)和重量級鎖狀態(tài),這幾個狀態(tài)會隨著競爭情況逐漸升級。鎖可以升級但不能降級,意味著偏向鎖升級成輕量級鎖后不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是為了提高 獲得鎖和釋放鎖的效率。

4.2 偏向鎖(偏向鎖獲取 + 偏向鎖撤銷)

4.2.1 偏向鎖獲取過程(兩個IF判斷步驟)

  1. 偏向鎖的引入是適應(yīng)Java并發(fā)的實際需求:大多數(shù)情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,所以,Java并發(fā)中為了讓線程獲得鎖的代價更低而引入了偏向鎖。

  2. 當一個線程訪問同步塊并獲取到同步鎖的時候,會在同步鎖對象的對象頭和棧幀中的鎖記錄Lock Record里存儲鎖偏向的線程ID(金手指:偏向鎖對象頭的mark word:線程id 23 + 偏向時間戳 2 + 分代年齡 4 = 29 + lock 3,和無鎖一起記憶),以后該線程再次進入和退出的時候,同步塊時不需要進行CAS操作來加鎖和解鎖,只需簡單地測試一下當前同步鎖對象的對象頭的Mark Word里是否存儲著指向當前線程的偏向鎖。

tip: 檢測的對象為thread id
當一個線程獲取偏向鎖的時候,會將這個線程的thread id寫到同步鎖對象的對象頭的mark word里面去,所以判斷的時候,根據(jù)threadid判斷。
偏向鎖檢測的對象為線程id,這也就是為什么偏向鎖需要23位線程id,其他四種情況的mark down都不需要線程id,無鎖是29位字節(jié)碼,輕量級鎖和重量級鎖是30位的monitor鎖指針,gc標志是30位的空。

  1. 如果測試成功,表示線程已經(jīng)獲得了鎖;如果測試失敗,則需要再測試一下Mark Word中偏向鎖的標識是否設(shè)置成0?
    如果偏向標志為0,則將它設(shè)置為1,更新自己的線程ID到對象頭的mark word字段中;
    如果偏向標志為1,表示此時偏向鎖已經(jīng)被別的線程獲取,則不斷嘗試使用CAS獲取偏向鎖或者將偏向鎖撤銷,在不斷CAS自旋過程中,大概率會升級為輕量級鎖。

面試官:偏向鎖獲取的整個過程可以用下圖來小結(jié)

在這里插入圖片描述
  1. 當一個線程第一次訪問同步代碼塊并獲取鎖時(使用cas操作獲取到鎖),會在同步鎖對象的對象頭的棧幀中的鎖記錄lock record中記錄存儲偏向鎖的線程ID。以后該線程再次進入同步塊時不再需要CAS來加鎖和解鎖,只需簡單測試一下對象頭的mark word中偏向鎖線程ID是否是當前線程ID(所以說,對于一個線程來說,偏向鎖的獲取只需要第一次使用CAS操作,該線程后面的直接判斷即可,最樂觀,最高效,加鎖和解鎖不需要額外的消耗,和執(zhí)行非同步方法比僅存在納秒級的差距);

  2. 如果成功,表示線程已獲取到鎖直接進入代碼塊運行;如果測試失敗(要么無鎖,要么就是有其他的線程,偏向鎖不適合多線程,多次cas操作自旋(如上圖),大概率會變成輕量級鎖),檢查當前偏向鎖字段是否為0?

  3. 如果為0,表示是001,無鎖,將偏向鎖字段設(shè)置為1,并且更新自己的線程ID到同步鎖對象的對象頭的mark word字段當中(下面我們可以在程序中打印這個對象的對象頭 good,用來查看鎖升級的過程);如果為1,表示此時偏向鎖已經(jīng)被別的線程獲取,則此線程不斷嘗試使用CAS獲取偏向鎖或者將偏向鎖撤銷,升級為輕量級鎖(升級概率較大,偏向時間戳epoche默認達到40升級為輕量級鎖)。

后面的測試類2中,main線程兩次偏向鎖,第一次需要cas,第二次直接判斷就好了,很方便快捷。

問題:上圖中,測試失敗,不斷自旋,然后兩條路,升級為輕量級鎖和撤銷偏向鎖是不同的兩條路?
回答:嗯嗯,這個圖告訴我們,多個線程下,偏向鎖只有兩個歸宿,鎖競爭失敗方撤銷偏向鎖或者將鎖更新為輕量級鎖

金手指:關(guān)于獲取偏向鎖,就是兩個對于同步鎖對象的判斷,同步鎖對象中是否存儲當前線程id?是的話直接執(zhí)行同步代碼塊,不是的話判斷同步鎖對象中偏向標志是否為0,為0表示當前無鎖,直接設(shè)置偏向標志為1和線程id即可,不為0表示已經(jīng)有其他線程持有偏向鎖,然后自旋,自旋兩個結(jié)果,鎖競爭失敗方撤銷偏向鎖或者將鎖更新為輕量級鎖。

4.2.2 偏向鎖撤銷過程(四個步驟)

偏向鎖釋放的時機:偏向鎖使用了一種等到競爭出現(xiàn)才釋放鎖的機制,所以當其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程才會釋放鎖。

偏向鎖撤銷的兩個階段(考慮撤銷 + 執(zhí)行撤銷):
1、考慮撤銷:等待有線程嘗試競爭偏向鎖,才會考慮撤銷。
2、執(zhí)行撤銷:考慮撤銷完畢后,如果是確定要撤銷,一定要等到JVM的safepoint點,才會執(zhí)行撤銷,因為這里沒有正在執(zhí)行的字節(jié)碼。
tip:這里是第一點和第二點使用了考慮撤銷和執(zhí)行撤銷,有先后順序,考慮撤銷并決定撤銷后才會在safepoint執(zhí)行撤銷偏向鎖,因為這里沒有正在執(zhí)行的字節(jié)碼。

偏向鎖撤銷流程:

  1. 首先暫停擁有偏向鎖的線程,然后檢查持有偏向鎖的線程是否活著;
  2. 如果線程活動狀態(tài),則將同步鎖對象頭設(shè)置成無鎖狀態(tài);
  3. 如果線程非活動狀態(tài),擁有偏向鎖的棧會被執(zhí)行,遍歷偏向?qū)ο蟮逆i記錄lock record,棧中的鎖記錄lock record和同步鎖對象的對象頭的Mark Word要么重新偏向于其他線程,要么恢復(fù)到無鎖或者標記對象不適合作為偏向鎖;
  4. 最后喚醒暫停的線程。

下圖中的線程1演示了偏向鎖初始化的流程,線程2演示了偏向鎖撤銷的流程。一個圖概括了偏向鎖的獲取和撤銷:

在這里插入圖片描述

對于上圖中概念的解釋:

  1. 第一個判斷,同步鎖對象的thread id是否為當前線程id?返回為false,第一次一定是沒有;
  2. 第二個判斷,當前偏向字段是否為0?返回true,表示無鎖,將threadid1設(shè)置到同步鎖對象的對象頭中的mark word(存放哈希碼和鎖對象)中,threadid1設(shè)置到鎖對象成功,進入偏向鎖狀態(tài);
    此時,同步鎖對象的對象頭中的mark word存放者thread id1,末尾三位為101,表示進入偏向鎖狀態(tài);
  3. 執(zhí)行同步代碼塊:就是synchronized代碼塊;
  4. 當線程1正在執(zhí)行同步代碼塊的時候,如果線程2要訪問同步代碼塊,同樣經(jīng)過兩個條件判斷;
  5. 第一個判斷:同步鎖對象的thread id是否為當前線程id?返回為false,同步鎖對象的thread id一定不是thread id2;
  6. 第二個判斷:當前偏向字段是否為0?返回false,當前鎖偏向字段為1,表示已有線程占用偏向鎖;
  7. 線程2自旋,使用cas想要將同步鎖對象的mark word中的thread id設(shè)置為自己的,但是沒有成功(因為線程1還沒執(zhí)行完成,其實,多個線程下,偏向鎖只有兩個歸宿,鎖競爭失敗方撤銷偏向鎖或者將鎖更新為輕量級鎖),這里要驗證撤銷偏向鎖,所以這里線程2發(fā)起撤銷偏向鎖
  8. 撤銷偏向鎖第一步:首先暫停擁有偏向鎖的線程,這里是暫停線程1,如上圖,線程1被暫停,然后檢查持有偏向鎖的線程是否活著;
  9. 撤銷偏向鎖第二步:如果持有偏向鎖的線程1 活動狀態(tài),則將同步鎖對象頭設(shè)置成無鎖狀態(tài);
  10. 撤銷偏向鎖第三步:如果持有偏向鎖的線程1 非活動狀態(tài),擁有偏向鎖的棧會被執(zhí)行,遍歷偏向?qū)ο蟮逆i記錄,棧中的鎖記錄lock record和同步鎖對象的對象頭的Mark Word要么重新偏向于其他線程,要么恢復(fù)到無鎖或者標記對象不適合作為偏向鎖(三種);
  11. 撤銷偏向鎖第四步:最后喚醒暫停的線程。
  12. 這里,線程1還活著,恢復(fù)到無鎖 001,最后喚醒被暫停的線程1,就是上圖的恢復(fù)線程。

小結(jié):這里,撤銷偏向鎖四步驟,先第一步,線程1被暫停,檢查線程1是否活動狀態(tài);然后第二步,線程1 活動狀態(tài),將同步鎖對象頭設(shè)置為無鎖狀態(tài);最后第四步,喚醒被暫停的線程1,恢復(fù)線程。
注意1:考慮撤銷:撤銷偏向鎖觸發(fā)條件是等到有競爭才撤銷/釋放偏向鎖,這是考慮撤銷。
注意2:執(zhí)行撤銷:考慮撤銷并確定撤銷后,才會執(zhí)行撤銷,而且要等待JVM safepoint點執(zhí)行撤銷,因為這里沒有正在執(zhí)行的字節(jié)碼。
注意3:撤銷/釋放偏向鎖的持有偏向鎖的線程1,不是線程2,線程2啥都沒有,撤銷啥。

最后再上一張圖補一下(獲取偏向鎖 + 撤銷/釋放偏向鎖)

在這里插入圖片描述

4.2.3 附:關(guān)閉偏向鎖

偏向鎖在Java 6和Java 7里是默認啟用的,但是它在應(yīng)用程序啟動幾秒鐘之后才激活,如有必要可以使用JVM參數(shù)來關(guān)閉延遲:-XX:BiasedLockingStartupDelay=0。如果你確定應(yīng)用程序里所有的鎖通常情況下處于競爭狀態(tài),可以通過JVM參數(shù)關(guān)閉偏向鎖:-XX:UseBiasedLocking=false,那么程序默認會進入輕量級鎖狀態(tài)。

小結(jié):關(guān)閉偏向鎖注意兩個:
JVM參數(shù)設(shè)置: XX:BiasedLockingStartupDelay=0 關(guān)閉偏向鎖延遲
JVM參數(shù)設(shè)置:XX:UseBiasedLocking=false 關(guān)閉偏向鎖,直接進入輕量級鎖

4.3 輕量級鎖(輕量級鎖加鎖 + 輕量級鎖解鎖)

4.3.1 引入兩個新概念:Lock Record + Displaced Mark Word

上面關(guān)于偏向鎖,我們在介紹了對象頭和Mark Word之后,都是拿同步鎖對象來研究的,比如獲取偏向鎖的兩個判斷,同步鎖對象中的thread id是否是當前線程?同步鎖對象中的偏向字段是否為0?無鎖情況下直接更新同步鎖對象的 thread id 和 設(shè)置偏向字段為1。

這里介紹輕量級鎖,引入一個新的名詞 Lock Record 鎖記錄 和一個新的動詞 Displaced Mark Word。

Displaced Mark Word定義:線程在執(zhí)行同步塊之前,JVM會先在當前線程的棧楨(方法調(diào)用棧)中創(chuàng)建用于存儲鎖記錄的空間,并將對象頭中的Mark Word復(fù)制到鎖記錄中,官方稱為Displaced Mark Word。

在代碼進入同步塊的時候,如果同步對象鎖狀態(tài)為無鎖狀態(tài)(鎖標志位為“01”狀態(tài),是否為偏向鎖為“0”),虛擬機首先將在當前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用于存儲鎖對象目前的Mark Word的拷貝,這個動作稱之為 Displaced Mark Word。

tip1:Displaced Mark Word 是一個動作,發(fā)生在線程執(zhí)行同步塊之前。
tip2:Lock Record 是一個名詞,是當前線程的幀棧中的一個空間名,用來存放對象的對象頭的mark word。

4.3.2 輕量級鎖加鎖(三步驟)

接上面的,thread2獲得偏向鎖失敗,只能自旋,只有兩個歸宿,要么讓 thread1 撤銷/釋放偏向鎖,要么thread1 升級輕量級鎖。

流程(偏向鎖升級為輕量級鎖,即輕量級鎖加鎖):

  1. 線程嘗試使用CAS操作,將同步鎖對象頭中的Mark Word替換為指向鎖記錄的指針。
  2. 如果成功,當前線程獲得鎖,輕量級鎖加鎖成功。
  3. 如果失敗,表示其他線程已經(jīng)競爭到輕量級鎖(金手指:這里thread1成功,獲得到鎖,thread2失敗,表示thread1已經(jīng)成功得到鎖,只能自旋),當前線程便嘗試使用自旋來獲取鎖。

偏向鎖加鎖和輕量級鎖加鎖:
偏向鎖和輕量級鎖,鎖競爭失敗方都是cas自旋獲取鎖,修改mark word,如果加鎖失敗,也都是自旋。

4.3.3 輕量級鎖解鎖(三步驟)

流程(輕量級鎖解鎖):

  1. 同步代碼塊執(zhí)行完成,輕量級解鎖時,會使用原子的CAS操作將Displaced Mark Word替換回到對象頭;
  2. 如果成功,則表示沒有競爭發(fā)生,輕量級鎖解鎖成功;
  3. 如果失敗,表示當前鎖存在競爭(金手指:下圖中,線程1執(zhí)行完代碼塊,輕量級鎖釋放失敗,因為thread2在競爭鎖),鎖就會膨脹成重量級鎖(下圖中markword第四個010)。

輕量級鎖加鎖 + 輕量級鎖解鎖:

在這里插入圖片描述

上圖中同步鎖對象頭中的mark word,一共涉及三個,從上到下,無鎖、輕量級鎖、重量級鎖

無論是偏向鎖還是輕量鎖,鎖競爭失敗方都會自旋,這里講輕量級鎖自旋,因為自旋會消耗CPU,為了避免無用的自旋(比如獲得鎖的線程被阻塞住了,繼續(xù)自旋嘗試就是浪費,不會成功的),所以規(guī)定,一旦鎖升級成重量級鎖,就不會再恢復(fù)到輕量級鎖狀態(tài)。當鎖處于這個狀態(tài)下,其他線程試圖獲取鎖時, 都會被阻塞住,當持有鎖的線程釋放鎖之后會喚醒這些線程,被喚醒的線程就會進行新一輪 的奪鎖之爭。

小結(jié):
輕量級鎖:多個線程在不同時間段請求同一把鎖,也就是基本不存在鎖競爭。針對此種情況,JVM采用輕量級鎖來避免線程的阻塞以及喚醒。
只要在同一時間內(nèi)有線程去競爭鎖,那么線程執(zhí)行一次CAS操作,然后發(fā)現(xiàn)已經(jīng)被別的線程搶占,直接升級為重量級鎖,不在進行CAS操作,避免無用自旋(將鎖升級為重量鎖、阻塞自旋線程這兩招就是為了避免無用自旋獲取輕量鎖)

輕量鎖加鎖 + 輕量鎖解鎖

在這里插入圖片描述

輕量鎖加鎖
線程在執(zhí)行同步代碼塊之前,JVM先在當前線程的棧幀中創(chuàng)建用于存儲鎖記錄的空間,并將對象頭的mark word字段直接復(fù)制到此空間中。然后線程嘗試使用CAS將對象頭的mark word替換為指向鎖記錄的指針(指當前線程),如果成功表示獲取到輕量級鎖。如果失敗,表示其他線程競爭輕量級鎖,當前線程便使用自旋來不斷嘗試。

輕量鎖釋放0
解鎖時,會使用CAS將復(fù)制的mark word替換回對象頭,如果成功,表示沒有競爭發(fā)生,正常解鎖。如果失敗,表示當前鎖存在競爭,進一步膨脹為重量級鎖(下圖中markword第四個010)。

4.3.4 對比:偏向鎖、輕量級、重量級鎖

優(yōu)點 缺點 適用場景
偏向鎖 加鎖和解鎖不需要額外的消耗,和執(zhí)行非同步方法比僅存在納秒級的差距。 如果線程間存在鎖競爭,會帶來額外的鎖撤銷的消耗。 最樂觀的鎖,適用于一個線程訪問同步代碼塊,偏向鎖不適用與多個線程,因為多個線程競爭同步資源,一定會有失敗,失敗了就是偏向鎖撤銷,然后不但自旋,到達閾值epoche=40,就升級輕量級鎖
輕量級鎖 競爭的線程不會阻塞,不斷自旋,提高了程序的響應(yīng)速度。 如果始終得不到鎖競爭的線程使用自旋會消耗CPU。 響應(yīng)時間很短,同步塊執(zhí)行速度非常快,適用于多個線程在不同時間段申請同一把鎖
重量級鎖 線程競爭不使用自旋,不會消耗CPU。 線程阻塞,響應(yīng)時間緩慢。 追求吞吐量。同步塊執(zhí)行速度較長。

重量級鎖會阻塞,喚醒請求加鎖的線程(這就是為了將鎖升級重量級鎖的原因,避免輕量鎖無效自旋浪費CPU)。針對的是多個線程同一個時刻競爭同一把鎖的情況,JVM采用自適應(yīng)自旋,來避免線程在面對非常小的同步塊時,仍會被阻塞以及喚醒。

輕量級鎖采用CAS操作,將鎖對象的標記字段替換為指向線程的指針,存儲著鎖對象原本的標記字段。針對的是多個線程在不同時間段申請同一把鎖的情況。

偏向鎖只會在第一次請求時采用CAS操作,在鎖對象的mark word字段中記錄下當前線程ID,此后運行中持有偏向鎖的線程不再有加鎖過程。針對的是鎖僅會被同一線程持有

偏向鎖是一個線程,多個線程大概率升級為輕量級鎖,輕量級鎖和重量級鎖都是多個線程。
既然是一個線程,偏向鎖有什么用?因為對象一開始就是偏向鎖,遇到synchronized大概率變?yōu)檩p量級鎖,你也可以一開始就是輕量級鎖

4.3.5 同步鎖對象的對象頭中的mark word變化(獲取偏向鎖+撤銷偏向鎖+升級為輕量鎖之前+升級為輕量鎖/輕量鎖加鎖+輕量鎖解鎖)

同步鎖對象的對象頭中的mark word

  1. 第一次獲得偏向鎖的時候,會將thread id放到棧幀中的鎖記錄Lock Record和同步鎖對象的對象頭中,其余次只要對比就好
  2. 撤銷偏向鎖的時候,如果持有偏向鎖的線程不存活,棧中的鎖記錄lock record和同步鎖對象的對象頭的Mark Word要么重新偏向于其他線程,要么恢復(fù)到無鎖或者標記對象不適合作為偏向鎖(三種)
  3. 升級為輕量級鎖之前,線程在執(zhí)行同步塊之前,JVM會先在當前線程的棧楨中創(chuàng)建用于存儲鎖記錄的空間,并將對象頭中的Mark Word復(fù)制到剛剛新建的鎖記錄空間中;
  4. 輕量級鎖加鎖的時候,線程嘗試使用CAS將對象頭中的Mark Word替換為指向鎖記錄的指針,表示輕量級鎖加鎖成功;
  5. 輕量級鎖解鎖的時候,使用原子的CAS操作將Displaced Mark Word替換回到對象頭,如果成功,解鎖成功,如果失敗,升級為重量級鎖。

4.4 重量級鎖(重量級鎖加鎖 + 重量級鎖解鎖)(了解即可)

4.4.1 重量級鎖的底層支持

4.4.1.1 ObjectMonitor類

基于HotSpot實現(xiàn)的JVM中,關(guān)于synchronized鎖的實現(xiàn)是靠ObjectMonitor(對象監(jiān)視器)實現(xiàn)的,當多個線程同時請求一個對象監(jiān)視器(請求同一個鎖)時,對象監(jiān)視器將設(shè)置幾個狀態(tài)以用于區(qū)分調(diào)用線程。

底層ObjectMonitor類的常用屬性(獲取重量鎖 + 釋放重量鎖 用到):

屬性 意義
_header MarkOop對象頭,對象頭
_waiters 等待線程數(shù),int
_recursions synchronized可重入鎖,記錄重入次數(shù),int
_owner 指向獲得ObjectMonitor的線程,指針,重要
_WaitSet 用于線程通信的集合,調(diào)用了java中的wait()方法會被放入其中,Set 等待集合
_cxq_EntryList 用于線程同步的集合,多個線程嘗試獲取鎖時,List

4.4.1.2 節(jié)點ObjectWaiter類(雙鏈表,鏈表節(jié)點中存放線程)

重量鎖的并發(fā)競爭狀態(tài)維護就是依靠三個隊列來實現(xiàn)的,分別是 _WaitSet、_cxq | _EntryList 。這三個隊列都是由ObjectWaiter類 實現(xiàn)的,其實就是雙向鏈表實現(xiàn)(金手指:有_prev和_next指針),鏈表中每一個節(jié)點存放線程。

對于 _WaitSet、_cxq | _EntryList ,都是雙鏈表,但是實現(xiàn)的功能不同。

_cxq | _EntryList 是用來實現(xiàn)線程同步的,

_WaitSet 是用來實現(xiàn)線程通信的,底層是等待隊列,對應(yīng)的Object類的 wait()/notify()/notifyAll() 方法,進入 _WaitSet 是失去參與同步鎖競爭,彈出 _WaitSet 是有機會參與同步鎖競爭。

4.4.1.3 線程同步:宏觀synchronized 與 底層鏈表 _cxq|_EntryList

獲取重量鎖本質(zhì)是修改_owner指針:當synchronized是重量鎖的時候,線程獲取鎖底層實現(xiàn)就是改變_owner指針,讓他指向自己。

在這里插入圖片描述

對于上圖中概念的解釋:
Contention List:首先將鎖定線程的所有請求放入競爭隊列
OnDeck:任何時候只有一個線程是最具競爭力的,該線程稱為OnDeck(由系統(tǒng)調(diào)度策略決定)

對于上圖中流程的解釋:

  1. Contention List 包括 _cxq和_EntryList,用戶實現(xiàn)線程同步,將鎖定線程的所有請求放入競爭隊列,接收多個線程;
  2. 選擇出最有競爭力的線程(由系統(tǒng)調(diào)度策略決定,所以說synchronized是非公平的獨占鎖),該線程稱為 OnDeck ,修改 _owner 指針,指向這個 OnDeck 線程(即獲得ObjectMonitor的線程)
  3. 線程執(zhí)行過程中,如果有線程通信的要求,調(diào)用notify()釋放一個 _WaitSet 集合中的線程(即ObjectWaiter對象),放到_EntryList中,允許其參與下一次同步鎖競爭。

4.4.1.4 線程通信:宏觀的wait()/notify()/notifyAll()與底層鏈表_WaitSet

宏觀Object類的wait()/notify()/notifyAll()方法,其實是調(diào)用內(nèi)核的方法實現(xiàn)的,他們的邏輯是:

  1. 調(diào)用wait()的線程加入_WaitSet中,即失去競爭同步鎖的機會;
  2. 調(diào)用notify()喚醒_WaitSet鏈表節(jié)點中的線程,就可以重新得到競爭同步鎖的機會。
  3. notify和notifyAll不同在于前者只喚醒一個線程,后者喚醒所有隊列中的線程。
  4. 值得注意的是notify并不會立即釋放鎖,而是等到同步代碼執(zhí)行完畢。

4.4.2 重量鎖加鎖(ObjectMonitor類的enter()方法)

4.4.2.1 重量鎖的獲取過程,ObjectMonitor::enter

  1. 設(shè)置_owner字段,CAS操作成功表示獲取鎖 :通過CAS嘗試把monitor的_owner字段設(shè)置為當前線程;
  2. 設(shè)置_recursions字段,記錄重入次數(shù) :如果設(shè)置之前的_owner指向當前線程,說明當前線程再次進入monitor,即重入鎖,執(zhí)行_recursions ++ ,記錄重入的次數(shù);
  3. 查看當前線程的鎖記錄空間中的Displaced Mark Word,即是否是該鎖的輕量級鎖持有者,如果是則是第一次加重量級鎖,設(shè)置_recursions為1,_owner為當前線程,該線程成功獲得鎖并返回;
  4. 如果獲取鎖失敗,則等待鎖的釋放。

4.4.2.2 ObjectMonitor::EnterI方法,自旋等待鎖釋放(核心:for循環(huán))

monitor競爭失敗的線程,通過自旋執(zhí)行ObjectMonitor::EnterI方法等待鎖的釋放,EnterI方法的部分邏輯實現(xiàn)如下:

  1. 當前線程被封裝成ObjectWaiter對象node,狀態(tài)設(shè)置成ObjectWaiter::TS_CXQ;
  2. 自旋CAS將當前節(jié)點使用頭插法加入cxq隊列
  3. node節(jié)點push到_cxq列表如果失敗了,再嘗試獲取一次鎖(因為此時同時線程加入,可以減少競爭),如果還是沒有獲取到鎖,則通過park將當前線程掛起,等待被喚醒。

當被系統(tǒng)喚醒時,繼續(xù)從掛起的地方開始執(zhí)行下一次循環(huán)也就是繼續(xù)自旋嘗試獲取鎖。如果經(jīng)過一定時間獲取失敗繼續(xù)掛起。

4.4.3 重量鎖解鎖(ObjectMonitor類的exit()方法)

當某個持有鎖的線程執(zhí)行完同步代碼塊時,會進行鎖的釋放。在HotSpot中,通過改變ObjectMonitor的值來實現(xiàn),并通知被阻塞的線程,具體實現(xiàn)位于ObjectMonitor::exit方法中。

  1. 初始化ObjectMonitor的屬性值,如果是重入鎖遞歸次數(shù)減一,等待下次調(diào)用此方法,直到為0,該鎖被釋放完畢。
  2. 根據(jù)不同的策略(由QMode指定),從cxq或EntryList中獲取頭節(jié)點,通過ObjectMonitor::ExitEpilog方法喚醒該節(jié)點封裝的線程,喚醒操作最終由unpark完成。

4.4.4 Object對象(即任意對象)調(diào)用hashCode()、wait()方法會使鎖直接升級為重量級鎖

調(diào)用hashCode()、wait()方法會使鎖直接升級為重量級鎖(在看jvm源碼注釋時看到的),下面測試一下。

4.4.4.1 調(diào)用wait()直接升級為重量級鎖

構(gòu)造demo的想法:要使用wait(),就要配合notify()/notifyAll()喚醒,使用一個線程是做不到的,一定要使用兩個線程。

調(diào)用wait方法

public class TestWait {

    public static void main(String[] args) throws InterruptedException {
        TimeUnit.SECONDS.sleep(5);
        O object = new O();

        Thread thread1 = new Thread() {
            @Override
            public void run() {
                synchronized (object) {    // 因為thread2釋放鎖,所以這里thread1可以成功
                    System.out.println("thread1獲取鎖成功,開始執(zhí)行,因為thread1調(diào)用了wait()方法,直接升級為重量級鎖");
                    System.out.println("2\n" + ClassLayout.parseInstance(object).toPrintable());
                    object.notify();
                }
            }
        };

        Thread thread2 = new Thread() {
            @Override
            public void run() {
                synchronized (object) {
                    System.out.println("thread2 獲取偏向鎖成功開始執(zhí)行");
                    System.out.println("1\n" + ClassLayout.parseInstance(object).toPrintable());   //打印對象頭
                    try {
                        object.wait();  // 阻塞thread2,并將object鎖對象變?yōu)橹亓考夋i,同時thread2釋放鎖,給機會thread1
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        thread2.start();   // 先啟動thread2

        //讓thread1執(zhí)行完同步代碼塊中方法。
        TimeUnit.SECONDS.sleep(3);   
        thread1.start();
    }
}

測試結(jié)果

在這里插入圖片描述

面試語言組織:調(diào)用wait()直接將偏向鎖升級為重量級鎖,構(gòu)造demo很簡單,新建兩個線程,休眠5秒,啟動第一個線程,打印對象頭為偏向鎖,調(diào)用wait()進入阻塞狀態(tài),釋放鎖,將鎖直接設(shè)置為重量級鎖,啟動第二個線程打印對象頭就可以知道

4.4.4.2 調(diào)用hashCode()直接升級為重量級鎖

構(gòu)造demo想法:調(diào)用hashcode只需要一個線程就好了,直接使用main線程,看代碼

調(diào)用hashCode()

public class TestLightweightLock {
    public static void main(String[] args) throws InterruptedException {
        TimeUnit.SECONDS.sleep(5);
        O object = new O();
        synchronized (object) {   // objetc作為同步鎖對象了,這句必須有,否則打印看不到鎖升級
            System.out.println("thread1 獲取偏向鎖成功,開始執(zhí)行代碼");
            System.out.println(ClassLayout.parseInstance(object).toPrintable());
            object.hashCode();
            try {
                //等待對象頭信息改變
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("hashCode() 調(diào)用后");
            System.out.println(ClassLayout.parseInstance(object).toPrintable());
        }
    }
}

面試語言組織:調(diào)用hashcode()直接將偏向鎖升級為重量級鎖,構(gòu)造demo很簡單,新建一個線程,休眠5秒,啟動線程,打印對象頭為偏向鎖,調(diào)用hashcode(),打印對象頭為重量級鎖。

測試結(jié)果

在這里插入圖片描述

4.4.5 鎖的降級(重量級鎖降級為輕量級鎖)

鎖也可以降級,在安全點判斷是否有線程嘗試獲取此鎖,如果沒有進行鎖降級(重量級鎖降級為輕量級鎖,和之前在書中看到的鎖只能升級不同,可能理解的意思不一樣)。

測試代碼如下,順便測試了一下重量級鎖升級

public class TestMonitor {
    public static void main(String[] args) throws InterruptedException {
        TimeUnit.SECONDS.sleep(5);
        O object = new O();
        Thread thread1 = new Thread() {
            @Override
            public void run() {
                synchronized (object) {  // object作為同步鎖對象
                    System.out.println("thread1 獲得偏向鎖");
                    System.out.println(ClassLayout.parseInstance(object).toPrintable());
                    try {
                        //讓線程晚點兒死亡,造成鎖的競爭
                        TimeUnit.SECONDS.sleep(6);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("thread2 獲取鎖失敗導致鎖升級,此時thread1還在執(zhí)行");
                    System.out.println(ClassLayout.parseInstance(object).toPrintable());
                }
            }
        };
        Thread thread2 = new Thread() {
            @Override
            public void run() {
                synchronized (object) {
                    System.out.println("thread2 獲取偏向鎖失敗,最終升級為重量級鎖,等待thread1執(zhí)行完畢,獲取重量鎖成功");
                    System.out.println(ClassLayout.parseInstance(object).toPrintable());
                    try {
                        TimeUnit.SECONDS.sleep(3);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        thread1.start();   // 啟動thread1
        //對象頭打印需要時間,先讓thread1獲取偏向鎖
        TimeUnit.SECONDS.sleep(5);
        //thread2去獲取鎖,因為t1一直在占用,導致最終升級為重量級鎖
        thread2.start();
        
        //確保t1和t2執(zhí)行結(jié)束
        thread1.join();
        thread2.join();
        TimeUnit.SECONDS.sleep(1);
       

        Thread t3 = new Thread(() -> {
            synchronized (object) {
                System.out.println("再次獲取");
                System.out.println(ClassLayout.parseInstance(object).toPrintable());
            }
        });
        t3.start();
    }
}

測試結(jié)果

在這里插入圖片描述
在這里插入圖片描述

t1和t2由于爭搶導致鎖升級為重量級鎖,等待它們執(zhí)行完畢,啟動t3獲取同一個鎖發(fā)現(xiàn)又降級為輕量級鎖。

五、實踐:鎖升級整個流程(偏向鎖獲取 + 偏向鎖撤銷 + 輕量鎖加鎖 + 輕量鎖解鎖)

5.1 實踐:驗證偏向鎖默認開啟,但是有4s啟動延遲

理論:java6以后默認開啟偏向鎖,但是偏向鎖要在應(yīng)用程序啟動幾秒鐘之后才激活。

使用JOL工具類,打印對象頭

添加maven依賴

<dependency>
  <groupId>org.openjdk.jol</groupId>
  <artifactId>jol-core</artifactId>
  <version>0.8</version>
</dependency>

新建O類和TestInitial類測試,設(shè)置啟動參數(shù)-XX:+PrintFlagsFinal

class O {
    int a = 1;
}
public class TestInitial {
    public static void main(String[] args) {
        O object = new O();
        //打印對象頭  object header = mark word 無鎖存放hashcode,有鎖存放鎖信息 +   Class Metadata Address 對象類型數(shù)據(jù)指針 + array length 數(shù)組長度
        System.out.println(ClassLayout.parseInstance(object).toPrintable());
    }
}

運行結(jié)果(很長,不完成粘貼,只要分析部分):

在這里插入圖片描述

結(jié)果如下,重點關(guān)注紅框內(nèi)的內(nèi)容,偏向標志位true,表示默認是開啟偏向鎖的,第一個驗證完成


在這里插入圖片描述
在這里插入圖片描述

0 1 2 3 表示48=32bit Markdown 16進制表示未 01 00 00 00 二進制表示為后面,紅框中最后三位 0 01 表示無鎖
4 5 6 7 表示4
8=32bit
8 9 12 11 表示4*8=32bit Array length 數(shù)組長度
12 13 14 15 表示int類型 4 *8 =32bit 4個字節(jié) int O.a value為1

com.O 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)                           c2 c0 00 20 (11000010 11000000 00000000 00100000) (536920258)
     12     4    int O.a                                       1
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

為什么打印出來的對象頭的第一個字節(jié)末尾三位是001,未加鎖?

我們的偏向鎖明明是開啟的,這是因為由4s中的延時開啟,這一設(shè)計的目的是因為程序在啟動初期需要初始化大量類,此時會發(fā)生大量鎖競爭,如果開啟偏向鎖,在沖突時鎖撤銷要耗費大量時間。

在這里插入圖片描述

修改TestInitial程序,第一行添加延時5s

class O {
    int a = 1;
}

public class TestInitial {
    public static void main(String[] args) throws Exception {
        TimeUnit.SECONDS.sleep(5);
        O object = new O();
        //打印對象頭  object header = mark word 無鎖存放hashcode,有鎖存放鎖信息 +   Class Metadata Address 對象類型數(shù)據(jù)指針 + array length 數(shù)組長度
        System.out.println(ClassLayout.parseInstance(object).toPrintable());
    }
}

測試結(jié)果如下


在這里插入圖片描述

如上測試結(jié)果中:

0 1 2 3 表示48=32bit Markdown 16進制表示未 05 00 00 00 二進制表示為后面,紅框中最后三位 101 表示偏向鎖
4 5 6 7 表示4
8=32bit
8 9 12 11 表示4*8=32bit Array length 數(shù)組長度
12 13 14 15 表示int類型 4 *8 =32bit 4個字節(jié) int O.a value為1

這里打印的是對象頭,因為沒有synchronized,所以根本就不訪問同步代碼塊,object對象根本就沒有成為同步鎖對象,只是一個普通的Object對象(后面的程序,訪問synchronized同步塊并將object作為同步鎖對象并打印其對象頭),所以object對象頭里面的就是 00000000 00000000 00000000 00000101 線程id為全0,因為object沒有作為同步鎖對象,main線程不需要訪問同步代碼塊

可以發(fā)現(xiàn)過了偏向鎖延時啟動時間后,我們再創(chuàng)建對象,對象頭鎖狀態(tài)變成了偏向鎖

解釋:
剛才是001,對象頭鎖狀態(tài)變成了無鎖,
現(xiàn)在延遲5s,對象頭鎖狀態(tài)變成了偏向鎖

問題1:默認開啟偏向鎖的原因?
解釋:大多數(shù)情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,所以,Java并發(fā)中為了讓線程獲得鎖的代價更低而引入了偏向鎖。jdk6引入偏向鎖,就是支持使用無鎖-偏向鎖-輕量級鎖-重量級鎖這個結(jié)構(gòu),所以默認偏向鎖開啟。

問題2:偏向鎖延遲4秒的原因?
解釋:因為程序在啟動初期需要初始化大量類,此時會發(fā)生大量鎖競爭,如果開啟偏向鎖,在沖突時鎖撤銷要耗費大量時間。

5.2 實踐:同一個線程中,main線程偏向鎖的釋放和再次獲取,第二次只需要判斷就好,不需要cas(只涉及偏向鎖,驗證獲取偏向鎖的兩個if判斷)

解釋器執(zhí)行monitorenter時會進入到InterpreterRuntime.cpp的InterpreterRuntime::monitorenter函數(shù),具體實現(xiàn)如下:


在這里插入圖片描述

synchronizer.cpp文件的ObjectSynchronizer::fast_enter函數(shù):

在這里插入圖片描述

BiasedLocking::revoke_and_rebias函數(shù)過長,下面就簡單分析下(著重分析一個線程先獲得鎖,下面會通過實驗來驗證結(jié)論)

  1. 當線程訪問同步塊時首先檢查同步鎖對象的對象頭中是否存儲了當前線程(和java中的ThreadId不一樣),如果有則直接執(zhí)行同步代碼塊。


    在這里插入圖片描述

    即此時JavaThread*指向當前線程id

  2. 如果沒有,查看對象頭是否是偏向鎖且指向線程id為空,如下:


    在這里插入圖片描述

測試代碼

public class TestBiasedLock {
    public static void main(String[] args) throws InterruptedException {
        TimeUnit.SECONDS.sleep(5);
        O object = new O();

        synchronized (object) {
            System.out.println("1\n" + ClassLayout.parseInstance(object).toPrintable());
        }
        TimeUnit.SECONDS.sleep(1);
        System.out.println("2\n" + ClassLayout.parseInstance(object).toPrintable());
    }
}   

測試結(jié)果

在這里插入圖片描述

注意:這個二進制是一個個字節(jié)從后往前讀的:

十進制的50321413,就是二進制的 00000010 11111111 11011000 00000101
在這里插入圖片描述

00000010 11111111 11011000 00000101 作為32bit就是

線程id 23 + 偏向時間戳 2 + 分代年齡 4 = 29 + lock 3
線程id 00000010 11111111 1101100 (這里打印的是對象頭,如果沒有synchronized,根本就不訪問同步代碼塊,那么object對象頭里面的就是 00000000 00000000 00000000 00000101 線程id為全0,因為object沒有作為同步鎖對象,main線程不需要訪問同步代碼塊)
epoch偏向時間戳 0 0
age分代年齡 0000
鎖信息 101

第一部分和第二部分,對象頭都是 101
第一個因為延遲5s,所以已經(jīng)執(zhí)行了4s的偏向啟動延遲,所以是101 上面解釋過了

結(jié)合初始化的測試,我們可以得知偏向鎖的獲取方式。

第一次,CAS設(shè)置當前對象頭指向自己,如果成功,則獲得偏向鎖(t1獲得了偏向鎖)開始執(zhí)行代碼。
第二次,知道了擁有偏向鎖的線程在執(zhí)行完成后,偏向鎖JavaTherad*依然指向第一次的偏向。

5.3 實踐:t2線程升級為輕量級鎖

問題1:偏向鎖什么時候升級為輕量級鎖?
回答1:偏向鎖中epoche偏向時間戳:Epoch 默認最大值為40,到超過40后會變成輕量級鎖。

問題2:為什么鎖可以升級不能降級?
這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率

t2撤銷偏向鎖升級為輕量級鎖:t2嘗試獲取偏向鎖,此時對象頭指向的不是自己(指向t1,而不是t2),開始撤銷偏向鎖, 升級為輕量級鎖。偏向鎖的撤銷,需要等待全局安全點,然后檢查持有偏向鎖的線程(t1)是否活著。

偏向鎖撤銷兩條件:獲取偏向鎖失敗+全局安全點safepoint

(1). 如果t1存活:讓該線程(t1)獲取輕量級鎖,將對象頭中的Mark Word替換為指向鎖記錄的指針,然后喚醒被暫停的線程。 也就是說將當前鎖(即t1持有的偏向鎖)升級為輕量級鎖,并且讓之前持有偏向鎖的線程(t1)繼續(xù)持有輕量級鎖。

(2). 如果t1已經(jīng)死亡:將t1對象頭設(shè)置成無鎖狀態(tài)

偏向鎖升級為輕量級鎖之后,t1繼續(xù)持有輕量級鎖;之前嘗試獲取偏向鎖失敗引發(fā)鎖升級的線程(t2)繼續(xù)嘗試獲取輕量級鎖,具體做法是:線程2 嘗試使用 CAS將對象頭中的Mark Word替換為指向鎖記錄的指針,如果失敗,開始自旋(即重復(fù)獲取一定次數(shù)),在自旋過程中過CAS設(shè)置成功,則成功獲取到輕量鎖對象。

tip1:tips:在當前線程的棧楨中然后創(chuàng)建用于存儲鎖記錄的空間,并將對象頭中的Mark Word復(fù)制到鎖記錄中,官方稱為Displaced Mark Word。

tip2:JVM中采用的是自適應(yīng)自旋鎖,即如果第一次自旋獲取鎖成功了,那么在下次自旋時,自旋次數(shù)會適當增加,采用自旋的原因是盡量減少內(nèi)核用戶態(tài)的切換,也就是說t2嘗試獲取偏向鎖失敗,導致偏向鎖的撤銷,撤銷后,線程(t2)繼續(xù)嘗試獲取輕量級鎖(這就是自適應(yīng)自旋,減少內(nèi)核用戶態(tài)的切換)。

public class TestLightweightLock3 {
    public static void main(String[] args) throws InterruptedException {
        TimeUnit.SECONDS.sleep(5);
        O object = new O();

        Thread thread1 = new Thread() {
            @Override
            public void run() {
                synchronized (object) {   // 訪問同步代碼塊
                    System.out.println("thread1 獲取偏向鎖成功");
                    System.out.println(ClassLayout.parseInstance(object).toPrintable());  // object同步鎖的持有對象是thread1,狀態(tài)為偏向鎖
                }
            }
        };

        Thread thread2 = new Thread() {
            @Override
            public void run() {
                synchronized (object) {   // 線程2訪問同步代碼塊
                    System.out.println("thread2 獲取偏向鎖失敗,升級為輕量級鎖,獲取輕量級鎖成功");
                    System.out.println(ClassLayout.parseInstance(object).toPrintable());  // object同步鎖的持有對象是thread2,狀態(tài)為輕量級鎖
                }
            }
        };
        thread1.start();
        thread1.join();   //讓thread1死亡
        // 中間沒有Thread.sleep() thread1沒有足夠時間打印對象頭,就讓thread2啟動,造成競爭,從而將偏向鎖撤銷,升級為輕量級鎖
        thread2.start();
        thread2.join();   //thread2死亡
        
        System.out.println("thread2執(zhí)行結(jié)束,釋放輕量級鎖");
        System.out.println(ClassLayout.parseInstance(object).toPrintable());  // 最后打印同步鎖
    }
}

上述測試的是,thread1獲取了偏向鎖,JavaThread*指向thread1。thread2在thread1執(zhí)行完畢后嘗試獲取偏向鎖,發(fā)現(xiàn)該偏向鎖指向thread1,因此開始撤銷偏向鎖,然后嘗試獲取輕量級鎖。

測試結(jié)果

t1先執(zhí)行獲取偏向鎖成功,開始執(zhí)行。
t2獲取偏向鎖失敗,升級為輕量級鎖

在這里插入圖片描述

問題:第一個打印,為什么持有鎖的是thread1?
回答:thread1是第一個訪問synchronized同步代碼塊,將object對象設(shè)置為同步鎖對象的(main線程沒有訪問synchronized同步代碼塊將object對象設(shè)置為同步鎖對象,thread2在后面才這樣做),所以持有object同步鎖的是thread1。

問題:第一個打印,為什么object同步鎖對象的對象頭為偏向鎖?
回答:啟動的時候延遲5秒,所以是偏向鎖。

問題:第二個打印,為什么持有object同步鎖的是thread2?
回答:main線程中,先執(zhí)行 thread1.join(); 再執(zhí)行thread2.start(); ,此時thread1合并到主線程,thread2一定可以競爭到object對象。

問題:第二個打印,為什么object同步鎖對象的對象頭為輕量級鎖?
回答:main線程中,先執(zhí)行 thread1.join(); 再執(zhí)行thread2.start(); ,但是 中間沒有Thread.sleep() thread1沒有足夠時間打印對象頭,就讓thread2啟動,造成競爭,從而將偏向鎖撤銷,升級為輕量級鎖,所以thread2獲取到的是輕量級鎖。

t2獲取輕量級鎖成功,執(zhí)行同步代碼塊

在這里插入圖片描述

問題:第三個打印,為什么線程id全為0?
回答:同步代碼塊執(zhí)行結(jié)束,object不再是同步鎖對象,而是一個普通的Object對象,所以線程id全為0;

問題:第三個打印,為什么為001,無鎖狀態(tài)?
回答:thread2釋放輕量級鎖的時候,使用原子的CAS操作將Displaced Mark Word替換回到對象頭,這里成功,沒有競爭發(fā)生,變成了無鎖。

t2在自旋過程中成功獲取了輕量級鎖,那么t2開始執(zhí)行。此時對象頭格式為: 00 輕量級鎖;

在t2執(zhí)行結(jié)束后,釋放輕量級鎖,鎖狀態(tài)為 001 無鎖。

5.4 實踐:t2線程升級為輕量級鎖,然后自旋未獲取輕量級,升級為重量級鎖

如果t2在自旋過程中未能獲得輕量鎖,達到閾值,那么此時膨脹為重量級鎖,將當前輕量級鎖標志位變?yōu)?10)重量級,創(chuàng)建objectMonitor對象,讓t1持有重量級鎖。然后當前線程開始阻塞。

public class TestMonitor {
    public static void main(String[] args) throws InterruptedException {
        TimeUnit.SECONDS.sleep(5);   //延遲5s
        O object = new O();
        Thread thread1 = new Thread() {
            @Override
            public void run() {
                synchronized (object) {   // object作為同步鎖的鎖對象
                    System.out.println("thread1 獲得偏向鎖");
                    System.out.println(ClassLayout.parseInstance(object).toPrintable());   // 打印對象頭
                    try {
                        //讓線程晚點兒死亡,造成鎖的競爭
                        TimeUnit.SECONDS.sleep(6);    // 同步代碼塊長久一點,就是thread1持有鎖的時間長久一點
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("thread2 獲取鎖失敗導致鎖升級,此時thread1還在執(zhí)行");  
                    System.out.println(ClassLayout.parseInstance(object).toPrintable());
                }
            }
        };
        Thread thread2 = new Thread() {
            @Override
            public void run() {
                synchronized (object) {   
                    System.out.println("thread2 獲取偏向鎖失敗,最終升級為重量級鎖,等待thread1執(zhí)行完畢,獲取重量鎖成功");
                    System.out.println(ClassLayout.parseInstance(object).toPrintable());
                    try {
                        TimeUnit.SECONDS.sleep(3);   // thread2打印對象頭,需要點時間
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        thread1.start();
        //對象頭打印需要時間,先讓thread1獲取偏向鎖
        TimeUnit.SECONDS.sleep(5);
        thread2.start();  
    }
}

測試結(jié)果

在這里插入圖片描述
在這里插入圖片描述

總結(jié):至此鎖升級已經(jīng)介紹完畢,接下來在介紹一下重量級鎖的實現(xiàn)機制ObjectMonitor即可。再次梳理整個過程(主要是一個線程t1已經(jīng)獲得鎖的情況下,另一個線程t2去嘗試獲取鎖):

  1. t2嘗試獲取偏向鎖,發(fā)現(xiàn)偏向鎖指向t1,獲取失敗
  2. 失敗后開始偏向鎖撤銷,如果t1還存活將輕量級鎖指向它,它繼續(xù)運行;t2嘗試獲取鎖,開始自旋等待t1釋放輕量級鎖。
  3. 如果在自旋過程中t1釋放了鎖,那么t2獲取輕量級鎖成功。
  4. 如果在自旋結(jié)束后,t2未能獲取輕量鎖,那么鎖升級為重量級鎖,使t1持有objectmonitor對象,將t2加入EntryList,t2開始阻塞,等待t1釋放監(jiān)視器

六、面試金手指(synchronizd底層實現(xiàn):為面試而準備的內(nèi)容,語言文字為主)

6.1 synchronizd底層實現(xiàn):synchronized五種用法和同步鎖對象頭的五種狀態(tài)

6.1.1 synchronized五種用法(多線程+鎖對象)

image.png

五種情況主要是方法級別鎖和代碼級別鎖,還有就是鎖對象的不同,方法級別鎖和代碼級別鎖很好理解,看代碼就懂,鎖對象不同是什么意思?

第一種情況和第三種情況,形成鎖競爭:普通方法和代碼塊中使用this是同一個監(jiān)視器(鎖),即某個具體調(diào)用該代碼的對象

第二種情況和第四種情況,形成競爭:靜態(tài)方法和代碼塊中使用該類的class對象是同一個監(jiān)視器,任何該類的對象調(diào)用該段代碼時都是在爭奪同一個監(jiān)視器的鎖定

小結(jié):同步兩因素:第一多線程,第二鎖對象

  1. 多線程:就是因為存在不止一個線程才能形成競爭,只有一個線程你和誰競爭,所以一旦設(shè)置同步,多線程必不可少;
  2. 鎖對象:鎖對象表示的是競爭的對象,即多個線程之間競爭什么,競爭的對象相同才能形成競爭,競爭的對象不同是無法形成競爭的,舉一反三,比如非同步方式(沒有synchronized修飾),就啥都不競爭,和同步原子沒半毛錢關(guān)系,代碼中通過對競爭的資源加鎖解鎖形成原子操作,所以一旦涉及同步,鎖對象必不可少。

6.1.2 synchronized代碼級別和方法級別區(qū)別

synchronized代碼級別鎖底層:monitorenter+monitorexit
synchronized方法級別鎖底層:ACC_SYNCHRONIZED
如果synchronized在方法上,底層使用ACC_SYNCHRONIZED修飾該方法,然后在常量池中獲取到鎖對象,實際實現(xiàn)原理和同步塊一致

6.1.3 對象頭 + Mark Word

對象頭三個部分:
對象頭=Mark Word + Class Metadata Address + Array length,三個每一個占一個字寬。
Markdown存放對象的hashCode或鎖信息;(無鎖狀態(tài):29bit hashcode+ 3bit lock)
Class Metadata Address存放存儲到對象類型數(shù)據(jù)的指針;
ArrayLength,數(shù)組類型特有,存放數(shù)組類型長度。

小結(jié):五種狀態(tài):

  1. 無鎖狀態(tài):
    hashcode 哈希碼 29bit
    biased_lock: 偏向鎖標識位,1bit
    lock: 鎖狀態(tài)標識位,2bit 01

  2. 偏向鎖(線程id 23 + 偏向時間戳 2 + 分代年齡 4 = 29 + lock 3):
    JavaThread: 保存持有偏向鎖的線程ID,23bit
    epoch: 保存偏向時間戳,2bit(到40,升級為輕量級鎖)
    age: 保存對象的分代年齡,4bit
    biased_lock: 偏向鎖標識位,1bit
    lock: 鎖狀態(tài)標識位,2bit 01
    其中,線程id 23 + 偏向時間戳 2 + 分代年齡 4 都是偏向鎖特有,后兩個都是涉及時間

  3. 輕量鎖和重量鎖(30+2):
    ptr: monitor的指針(就是鎖指針,同步代碼塊的鎖由monitorenter和monitorexit完成,同步方法的鎖由ACC_SYNCHRONIZED修飾,底層是一致的),30bit
    lock: 鎖狀態(tài)標識位,2bit 00 10

  4. GC標志(30+2)
    空,30bit
    lock: 鎖狀態(tài)標識位,2bit 11

記憶方法:偏向鎖和無鎖一起記憶,其他三個,輕量級鎖和重量級鎖一起記憶。

6.2 synchronizd底層實現(xiàn):鎖升級

6.2.1 鎖升級的由來

1、只能升級不能降級,目的是為了提高 獲得鎖和釋放鎖的效率
2、升級順序:無鎖狀態(tài) 0 01、偏向鎖狀態(tài)101、輕量級鎖狀 態(tài)000和重量級鎖狀態(tài)010

6.2.2 偏向鎖獲取 + 偏向鎖撤銷/解鎖 + 偏向鎖關(guān)閉

獲取偏向鎖:
關(guān)于獲取偏向鎖,就是兩個對于同步鎖對象的判斷,同步鎖對象中是否存儲當前線程id?是的話直接執(zhí)行同步代碼塊,不是的話判斷同步鎖對象中偏向標志是否為0,為0表示當前無鎖,直接設(shè)置偏向標志為1和線程id即可,不為0表示已經(jīng)有其他線程持有偏向鎖,然后自旋,自旋兩個結(jié)果,鎖競爭失敗方撤銷偏向鎖或者將鎖更新為輕量級鎖。

偏向鎖撤銷的兩個階段(考慮撤銷 + 執(zhí)行撤銷):

  1. 考慮撤銷:等待有線程嘗試競爭偏向鎖,才會考慮撤銷。
  2. 執(zhí)行撤銷:考慮撤銷完畢后,如果是確定要撤銷,一定要等到JVM的safepoint點,才會執(zhí)行撤銷,因為這里沒有正在執(zhí)行的字節(jié)碼。

tip:這里是第一點和第二點使用了考慮撤銷和執(zhí)行撤銷,有先后順序,考慮撤銷并決定撤銷后才會在safepoint執(zhí)行撤銷偏向鎖,因為這里沒有正在執(zhí)行的字節(jié)碼。

偏向鎖撤銷流程:

  1. 首先暫停擁有偏向鎖的線程,然后檢查持有偏向鎖的線程是否活著;
  2. 如果線程活動狀態(tài),則將同步鎖對象頭設(shè)置成無鎖狀態(tài);
  3. 如果線程非活動狀態(tài),擁有偏向鎖的棧會被執(zhí)行,遍歷偏向?qū)ο蟮逆i記錄lock record,棧中的鎖記錄lock record和同步鎖對象的對象頭的Mark Word要么重新偏向于其他線程,要么恢復(fù)到無鎖或者標記對象不適合作為偏向鎖;
  4. 最后喚醒暫停的線程。

6.2.3 輕量鎖加鎖 + 輕量鎖解鎖

輕量鎖加鎖流程(偏向鎖升級為輕量級鎖,即輕量級鎖加鎖)

  1. 線程嘗試使用CAS操作,將同步鎖對象頭中的Mark Word替換為指向鎖記錄的指針。
  2. 如果成功,當前線程獲得鎖,輕量級鎖加鎖成功。
  3. 如果失敗,表示其他線程已經(jīng)競爭到輕量級鎖(金手指:這里thread1成功,獲得到鎖,thread2失敗,表示thread1已經(jīng)成功得到鎖,只能自旋),當前線程便嘗試使用自旋來獲取鎖。

偏向鎖加鎖和輕量級鎖加鎖
偏向鎖和輕量級鎖,鎖競爭失敗方都是cas自旋獲取鎖,修改mark word,如果加鎖失敗,也都是自旋。

輕量鎖解鎖流程

  1. 同步代碼塊執(zhí)行完成,輕量級解鎖時,會使用原子的CAS操作將Displaced Mark Word替換回到對象頭;
  2. 如果成功,則表示沒有競爭發(fā)生,輕量級鎖解鎖成功;
  3. 如果失敗,表示當前鎖存在競爭(如線程1執(zhí)行完代碼塊,輕量級鎖釋放失敗,因為thread2在競爭鎖),鎖就會膨脹成重量級鎖。

6.2.4 重量鎖加鎖 + 重量鎖解鎖 + 調(diào)用wait()和hashcode()直接變?yōu)橹亓挎i

重量級鎖加鎖

  1. _owner字段 :通過CAS嘗試把monitor的_owner字段設(shè)置為當前線程;
  2. synchronized和lock都是可重入鎖 _owner、_recursions:如果設(shè)置之前的_owner指向當前線程,說明當前線程再次進入monitor,即重入鎖,執(zhí)行_recursions ++ ,記錄重入的次數(shù);
  3. 查看當前線程的鎖記錄空間中的Displaced Mark Word,即是否是該鎖的輕量級鎖持有者,如果是則是第一次加重量級鎖,設(shè)置_recursions為1,_owner為當前線程,該線程成功獲得鎖并返回;
  4. 如果獲取鎖失敗,則等待鎖的釋放;

重量級鎖釋放

  1. 初始化ObjectMonitor的屬性值,如果是重入鎖遞歸次數(shù)減一,等待下次調(diào)用此方法,直到為0,該鎖被釋放完畢。
  2. 根據(jù)不同的策略(由QMode指定),從cxq或EntryList中獲取頭節(jié)點,通過ObjectMonitor::ExitEpilog方法喚醒該節(jié)點封裝的線程,喚醒操作最終由unpark完成。
    調(diào)用wait()的線程加入_WaitSet中,然后等待notify喚醒他們,重新加入到鎖的競爭之中,值得注意的是notify并不會立即釋放鎖,而是等到同步代碼執(zhí)行完畢。

調(diào)用wait()和hashcode()直接變?yōu)橹亓挎i

  1. 調(diào)用wait()變?yōu)橹亓挎i:調(diào)用wait()直接將偏向鎖升級為重量級鎖,構(gòu)造demo很簡單,新建兩個線程,休眠5秒,啟動第一個線程,打印對象頭為偏向鎖,調(diào)用wait()進入阻塞狀態(tài),釋放鎖,將鎖直接設(shè)置為重量級鎖,啟動第二個線程打印對象頭就可以知道
  2. 調(diào)用hashcode()變?yōu)橹亓挎i:調(diào)用hashcode()直接將偏向鎖升級為重量級鎖,構(gòu)造demo很簡單,新建一個線程,休眠5秒,啟動線程,打印對象頭為偏向鎖,調(diào)用hashcode(),打印對象頭為重量級鎖。

鎖也可以降級,在安全點判斷是否有線程嘗試獲取此鎖,如果沒有進行鎖降級

七、小結(jié)

synchronized關(guān)鍵字全解析,完成。

天天打碼,天天進步!?。?/p>

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

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