1. 程式人生 > >Synchronized用法原理和鎖優化升級過程(面試)

Synchronized用法原理和鎖優化升級過程(面試)

簡介


多執行緒一直是面試中的重點和難點,無論你現在處於啥級別段位,對synchronized關鍵字的學習避免不了,這是我的心得體會。下面咱們以面試的思維來對synchronized做一個系統的描述,如果有面試官問你,說說你對synchronized的理解?你可以從synchronized使用層面,synchronized的JVM層面,synchronized的優化層面3個方面做系統回答,說不定面試官會對你刮目相看哦!文章會有大量的程式碼是方便理解的,如果你有時間一定要動手敲下加深理解和記憶。如果這篇文章能對您能有所幫助是我創作路上最大欣慰。

synchronized使用層面


大家都知道synchronized是一把鎖,鎖究竟是什麼呢?舉個例子,你可以把鎖理解為廁所門上那把鎖的唯一鑰匙,每個人要進去只能拿著這把鑰匙可以去開這個廁所的門,這把鑰匙在一時刻只能有一個人擁有,有鑰匙的人可以反覆出入廁所,在程式中我們叫做這種重複出入廁所行為叫鎖的可重入。它可以修飾靜態方法,例項方法和程式碼塊 ,那下面我們一起來看看synchronized用於同步程式碼鎖表達的意思。

  • 對於普通同步方法,鎖的是物件例項。
  • 對於靜態同步方法,鎖的是類的Class物件。
  • 對於同步程式碼塊,鎖的是括號中的物件。

先說下同步和非同步的概念。

  • 同步:交替執行。
  • 非同步:同時執行。

舉個例子比如吃飯和看電視兩件事情,先吃完飯後再去看電視,在時間維度上這兩件事是有先後順序的,叫同步。可以一邊吃飯,一邊看刷劇,在時間維度上是不分先後同時進行的,飯吃完了電視也看了,就可以去學習了,這就是非同步,非同步的好處是可以提高效率,這樣你就可以節省時間去學習了。

下面我們看看程式碼,程式碼中有做了很詳細的註釋,可以複製到本地進行測試。如果有synchronized基礎的童鞋,可以跳過鎖使用層面的講解。

 1 /**
 2  * @author :jiaolian
 3  * @date :Created in 2020-12-17 14:48
 4  * @description:測試靜態方法同步和普通方法同步是不同的鎖,包括synchronized修飾的靜態程式碼塊用法;
 5  * @modified By:
 6  * 公眾號:叫練
 7  */
 8 public class SyncTest {
 9 
10     public static void main(String[] args) {
11         Service service = new Service();
12         /**
13          * 啟動下面4個執行緒,分別測試m1-m4方法。
14          */
15         Thread threadA = new Thread(() -> Service.m1());
16         Thread threadB = new Thread(() -> Service.m2());
17         Thread threadC = new Thread(() -> service.m3());
18         Thread threadD = new Thread(() -> service.m4());
19         threadA.start();
20         threadB.start();
21         threadC.start();
22         threadD.start();
23 
24     }
25 
26     /**
27      * 此案例說明了synchronized修飾的靜態方法和普通方法獲取的不是同一把鎖,因為他們是非同步的,相當於是同步執行;
28      */
29     private static class Service {
30         /**
31          * m1方法synchronized修飾靜態方法,鎖表示鎖定的是Service.class
32          */
33         public synchronized static void m1() {
34             System.out.println("m1 getlock");
35             try {
36                 Thread.sleep(2000);
37             } catch (InterruptedException e) {
38                 e.printStackTrace();
39             }
40             System.out.println("m1 releaselock");
41         }
42 
43         /**
44          * m2方法synchronized修飾靜態方法,鎖表示鎖定的是Service.class
45          * 當執行緒AB同時啟動,m1和m2方法是同步的。可以證明m1和m2是同一把鎖。
46          */
47         public synchronized static void m2() {
48             System.out.println("m2 getlock");
49             System.out.println("m2 releaselock");
50         }
51 
52         /**
53          * m3方法synchronized修飾的普通方法,鎖表示鎖定的是Service service = new Service();中的service物件;
54          */
55         public synchronized void m3() {
56             System.out.println("m3 getlock");
57             try {
58                 Thread.sleep(1000);
59             } catch (InterruptedException e) {
60                 e.printStackTrace();
61             }
62             System.out.println("m3 releaselock");
63         }
64 
65         /**
66          * 1.m4方法synchronized修飾的同步程式碼塊,鎖表示鎖定的是當前物件例項,也就是Service service = new Service();中的service物件;和m3一樣,是同一把鎖;
67          * 2.當執行緒CD同時啟動,m3和m4方法是同步的。可以證明m3和m4是同一把鎖。
68          * 3.synchronized也可以修飾其他物件,比如synchronized (Service.class),此時m4,m1,m2方法是同步的,啟動執行緒ABD可以證明。
69          */
70         public void m4() {
71             synchronized (this) {
72                 System.out.println("m4 getlock");
73                 System.out.println("m4 releaselock");
74             }
75         }
76 
77     }
78 }

 

經過上面的測試,你可以能會有疑問,鎖既然是存在的,那它儲存在什麼地方?答案:物件裡面。下面我們用程式碼來證明下。

鎖在物件頭裡面,一個物件包括物件頭,例項資料和對齊填充。物件頭包括MarkWord和物件指標,物件指標是指向方法區的物件型別的,,例項物件就是屬性資料,一個物件可能有很多屬性,屬性是動態的。對齊填充是為了補齊位元組數的,如果物件大小不是8位元組的整數倍,需要補齊剩餘的位元組數,這是方便計算機來計算的。在64位機器裡面,一個物件的物件頭一般佔12個自己大小,在64位作業系統一般佔4個位元組,所以MarkWord就是8個位元組了。

MarkWord包括物件hashcode,偏向鎖標誌位,執行緒id和鎖的標識。為了方便測試物件頭的內容,需要引入maven openjdk的依賴包。

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

 

 

/**
 * @author :duyang
 * @date :Created in 2020-05-14 20:21
 * @description:物件佔用記憶體
 * @modified By:
 *
 *  Fruit物件頭是12位元組(markword+class)
 *  int 佔4個位元組
 *
 *  32位機器可能佔8個位元組;
 *
 *  Object物件頭12 對齊填充4 一共是16
 */
public class ObjectMemory {
    public static void main(String[] args) {
        //System.out.print(ClassLayout.parseClass(Fruit.class).toPrintable());
        System.out.print(ClassLayout.parseInstance(Fruit.class).toPrintable());
    }
}

/**
 *Fruit 測試類
 */
public class Fruit {

    //佔一個位元組大小
    private boolean flag;

}

 

 

測試結果:下面畫紅線的3行分別表示物件頭,例項資料和對齊填充。物件頭是12個位元組,例項資料Fruit物件的一個boolean欄位flag佔1個位元組大小,其餘3個位元組是對齊填充的部分,一共是16個位元組大小。

 

咦?你說的鎖呢,怎麼沒有看到呢?小夥,彆著急,待會我們講到synchronized升級優化層面的時候再來詳細分析一波。下面我們先分析下synchronized在JVM層面的意思。

 

最後上圖文總結:

 

synchronized JVM層面


 

 1 /**
 2  * @author :jiaolian
 3  * @date :Created in 2020-12-20 13:43
 4  * @description:鎖的jvm層面使用
 5  * @modified By:
 6  * 公眾號:叫練
 7  */
 8 public class SyncJvmTest {
 9     public static void main(String[] args) {
10         synchronized (SyncJvmTest.class) {
11             System.out.println("jvm同步測試");
12         }
13     }
14 }

 

上面的案例中,我們同步程式碼塊中我們簡單輸出一句話,我們主要看看jvm中它是怎麼實現的。我們用Javap -v SyncJvmTest.class反編譯出上面的程式碼,如下圖所示。

上圖第一行有一個monitorenter和第六行一個monitorexit,中間的jvm指令(2-5行)對應的Java程式碼中的main方法的程式碼,synchronized就是依賴於這兩個指令實現。我們來看看JVM規範中monitorenter語義。

  1. 每個物件都有一把鎖,當一個執行緒進入同步程式碼塊,都會去獲取這個物件所持有monitor物件鎖(C++實現),如果當前執行緒獲取鎖,會把monitor物件進入數自增1次。
  2. 如果該執行緒重複進入,會把monitor物件進入數再次自增1次。
  3. 當有其他執行緒進入,會把其他執行緒放入等待佇列排隊,直到獲取鎖的執行緒將monitor物件的進入數設定為0釋放鎖,其他執行緒才有機會獲取鎖。

 

synchronized的優化層面


synchronized是一個重量級鎖,主要是因為執行緒競爭鎖會引起作業系統使用者態和核心態切換,浪費資源效率不高,在jdk1.5之前,synchronized沒有做任何優化,但在jdk1.6做了效能優化,它會經歷偏向鎖,輕量級鎖,最後才到重量級鎖這個過程,在效能方面有了很大的提升,在jdk1.7的ConcurrentHashMap是基於ReentrantLock的實現了鎖,但在jdk1.8之後又替換成了synchronized,就從這一點可以看出JVM團隊對synchronized的效能還是挺有信心的。下面我們分別來介紹下無鎖,偏向鎖,輕量級鎖,重量級鎖。下面我們我畫張圖來描述這幾個級別鎖的在物件頭儲存狀態。如圖所示。

  • 無鎖。如果不加synchronized關鍵字,表示無鎖,很好理解。
  • 偏向鎖。
    • 升級過程:當執行緒進入同步塊時,Markword會儲存偏向執行緒的id並且cas將Markword鎖狀態標識為01,是否偏向用1表示當前處於偏向鎖(對著上圖來看),如果是偏向執行緒下次進入同步程式碼只要比較Markword的執行緒id是否和當前執行緒id相等,如果相等不用做任何操作就可以進入同步程式碼執行,如果不比較後不相等說明有其他執行緒競爭鎖,synchronized會升級成輕量級鎖。這個過程中在作業系統層面不用做核心態和使用者態的切換,減少切換執行緒帶來的資源消耗。
    • 膨脹過程:當有另外執行緒進入,偏向鎖會升級成輕量級鎖。比如執行緒A是偏向鎖,這是B執行緒進入,就會成輕量級鎖,只要有兩個執行緒就會升級成輕量級鎖。

下面我們程式碼來看下偏向鎖的鎖狀態。

 1 package com.duyang.base.basic.markword;
 2 
 3 import lombok.SneakyThrows;
 4 import org.openjdk.jol.info.ClassLayout;
 5 
 6 /**
 7  * @author :jiaolian
 8  * @date :Created in 2020-12-19 11:25
 9  * @description:markword測試
10  * @modified By:
11  * 公眾號:叫練
12  */
13 public class MarkWordTest {
14 
15     private static Fruit fruit = new Fruit();
16 
17     public static void main(String[] args) throws InterruptedException {
18         Task task = new Task();
19         Thread threadA = new Thread(task);
20         Thread threadB = new Thread(task);
21         Thread threadC = new Thread(task);
22         threadA.start();
23         //threadA.join();
24         //threadB.start();
25         //threadC.start();
26     }
27 
28     private static class Task extends Thread {
29 
30         @SneakyThrows
31         @Override
32         public void run() {
33             synchronized (fruit) {
34                 System.out.println("==================="+Thread.currentThread().getId()+" ");
35                 try {
36                     Thread.sleep(3000);
37                 } catch (InterruptedException e) {
38                     e.printStackTrace();
39                 }
40                 System.out.print(ClassLayout.parseInstance(fruit).toPrintable());
41             }
42         }
43     }
44 }

 

上面程式碼啟動執行緒A,控制檯輸出如下圖所示,紅色標記3個bit是101分別表示,高位的1表示是偏向鎖,01是偏向鎖標識位。符合偏向鎖標識的情況。

  • 輕量級鎖。
    • 升級過程:線上程執行獲取鎖後,會在棧幀中創造鎖記錄並將MarkWord複製到鎖記錄,然後將MarkWord指向鎖記錄,如果當前執行緒持有鎖,其他執行緒再進入,此時其他執行緒會cas自旋,直到獲取鎖,輕量級鎖適合多執行緒交替執行,效率高(cas只消耗cpu,我在cas原理一篇文章中詳細講過。)。
    • 膨脹過程:有兩種情況會膨脹成重量級鎖。1種情況是cas自旋10次還沒獲取鎖。第2種情況其他執行緒正在cas獲取鎖,第三個執行緒競爭獲取鎖,鎖也會膨脹變成重量級鎖。

下面我們程式碼來測試下輕量級鎖的鎖狀態。

開啟23行-24行程式碼,執行執行緒A,B,我的目的是順序執行執行緒A B ,所以我在程式碼中先執行threadA.join(),讓A執行緒先執行完畢,再執行B執行緒,如下圖所示MarkWord鎖狀態變化,執行緒A開始是偏向鎖用101表示,執行執行緒B就變成輕量級鎖了,鎖狀態變成了00,符合輕量級鎖鎖狀態。證明完畢。

  • 重量級鎖。重量級鎖升級後是不可逆的,也就是說重量鎖不可以再變為輕量級鎖。

開啟25行程式碼,執行執行緒A,B,C,我的目的是先執行執行緒A,在程式碼中先執行threadA.join(),讓A執行緒先執行完畢,然後再同時執行執行緒BC ,如下圖所示看看MarkWord鎖狀態變化,執行緒A開始是偏向鎖,到同時執行執行緒BC,因為有激烈競爭,屬於輕量級鎖膨脹條件第2種情況,當其他執行緒正在cas獲取鎖,第三個執行緒競爭獲取鎖,鎖也會膨脹變成重量級鎖。此時BC執行緒鎖狀態都變成了10,這種情況符合重量級鎖鎖狀態。膨脹重量級鎖證明完畢。

 

 

到此為止,我們已經把synchronized鎖升級過程中的鎖狀態通過程式碼的形式都證明了一遍,希望對你有幫助。下圖是自己總結。

 

總結


多執行緒synchronized一直是個很重要的話題,也是面試中常見的考點。希望大家都能儘快理解掌握,分享給你們希望你們喜歡!

我是叫練,多叫多練,歡迎大家和我一起討論交流,我會盡快回復大家,喜歡點贊和關注哦!公眾號【叫練】。

 

 
  • 清除所有標記
  • 清除選中的標記
  • 錯誤型別
  • 無錯字 - 寫作(線上版)
  &nbs