多線程一直是面試中的重點和難點,不管你如今處於啥級別段位,對synchronized關鍵字的學習避免不了,這是個人心得體會。下面我們以面試的思惟來對synchronized作一個系統的描述,若是有面試官問你,說說你對synchronized的理解?你能夠從synchronized使用層面,synchronized的JVM層面,synchronized的優化層面3個方面作系統回答,說不定面試官會對你另眼相看哦!文章會有大量的代碼是方便理解的,若是你有時間必定要動手敲下加深理解和記憶。若是這篇文章能對您能有所幫助是我創做路上最大欣慰。java
你們都知道synchronized是一把鎖,鎖到底是什麼呢?舉個例子,你能夠把鎖理解爲廁所門上那把鎖的惟一鑰匙,每一個人要進去只能拿着這把鑰匙能夠去開這個廁所的門,這把鑰匙在一時刻只能有一我的擁有,有鑰匙的人能夠反覆出入廁所,在程序中咱們叫作這種重複出入廁所行爲叫鎖的可重入。它能夠修飾靜態方法,實例方法和代碼塊 ,那下面咱們一塊兒來看看synchronized用於同步代碼鎖表達的意思。面試
先說下同步和異步的概念。性能優化
舉個例子好比吃飯和看電視兩件事情,先吃完飯後再去看電視,在時間維度上這兩件事是有前後順序的,叫同步。能夠一邊吃飯,一邊看刷劇,在時間維度上是不分前後同時進行的,飯吃完了電視也看了,就能夠去學習了,這就是異步,異步的好處是能夠提升效率,這樣你就能夠節省時間去學習了。多線程
下面咱們看看代碼,代碼中有作了很詳細的註釋,能夠複製到本地進行測試。若是有synchronized基礎的童鞋,能夠跳過鎖使用層面的講解。異步
/** * @author :jiaolian * @date :Created in 2020-12-17 14:48 * @description:測試靜態方法同步和普通方法同步是不一樣的鎖,包括synchronized修飾的靜態代碼塊用法; * @modified By: * 公衆號:叫練 */ public class SyncTest { public static void main(String[] args) { Service service = new Service(); /** * 啓動下面4個線程,分別測試m1-m4方法。 */ Thread threadA = new Thread(() -> Service.m1()); Thread threadB = new Thread(() -> Service.m2()); Thread threadC = new Thread(() -> service.m3()); Thread threadD = new Thread(() -> service.m4()); threadA.start(); threadB.start(); threadC.start(); threadD.start(); } /** * 此案例說明了synchronized修飾的靜態方法和普通方法獲取的不是同一把鎖,由於他們是異步的,至關因而同步執行; */ private static class Service { /** * m1方法synchronized修飾靜態方法,鎖表示鎖定的是Service.class */ public synchronized static void m1() { System.out.println("m1 getlock"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("m1 releaselock"); } /** * m2方法synchronized修飾靜態方法,鎖表示鎖定的是Service.class * 當線程AB同時啓動,m1和m2方法是同步的。能夠證實m1和m2是同一把鎖。 */ public synchronized static void m2() { System.out.println("m2 getlock"); System.out.println("m2 releaselock"); } /** * m3方法synchronized修飾的普通方法,鎖表示鎖定的是Service service = new Service();中的service對象; */ public synchronized void m3() { System.out.println("m3 getlock"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("m3 releaselock"); } /** * 1.m4方法synchronized修飾的同步代碼塊,鎖表示鎖定的是當前對象實例,也就是Service service = new Service();中的service對象;和m3同樣,是同一把鎖; * 2.當線程CD同時啓動,m3和m4方法是同步的。能夠證實m3和m4是同一把鎖。 * 3.synchronized也能夠修飾其餘對象,好比synchronized (Service.class),此時m4,m1,m2方法是同步的,啓動線程ABD能夠證實。 */ public void m4() { synchronized (this) { System.out.println("m4 getlock"); System.out.println("m4 releaselock"); } } } }
通過上面的測試,你能夠能會有疑問,鎖既然是存在的,那它存儲在什麼地方?答案:對象裏面。下面咱們用代碼來證實下。jvm
鎖在對象頭裏面,一個對象包括對象頭,實例數據和對齊填充。對象頭包括MarkWord和對象指針,對象指針是指向方法區的對象類型的,,實例對象就是屬性數據,一個對象可能有不少屬性,屬性是動態的。對齊填充是爲了補齊字節數的,若是對象大小不是8字節的整數倍,須要補齊剩餘的字節數,這是方便計算機來計算的。在64位機器裏面,一個對象的對象頭通常佔12個本身大小,在64位操做系統通常佔4個字節,因此MarkWord就是8個字節了。maven
MarkWord包括對象hashcode,偏向鎖標誌位,線程id和鎖的標識。爲了方便測試對象頭的內容,須要引入maven openjdk的依賴包。ide
<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層面的意思。學習
最後上圖文總結:
/** * @author :jiaolian * @date :Created in 2020-12-20 13:43 * @description:鎖的jvm層面使用 * @modified By: * 公衆號:叫練 */ public class SyncJvmTest { public static void main(String[] args) { synchronized (SyncJvmTest.class) { System.out.println("jvm同步測試"); } } }
上面的案例中,咱們同步代碼塊中咱們簡單輸出一句話,咱們主要看看jvm中它是怎麼實現的。咱們用Javap -v SyncJvmTest.class反編譯出上面的代碼,以下圖所示。
上圖第一行有一個monitorenter和第六行一個monitorexit,中間的jvm指令(2-5行)對應的Java代碼中的main方法的代碼,synchronized就是依賴於這兩個指令實現。咱們來看看JVM規範中monitorenter語義。
synchronized是一個重量級鎖,主要是由於線程競爭鎖會引發操做系統用戶態和內核態切換,浪費資源效率不高,在jdk1.5以前,synchronized沒有作任何優化,但在jdk1.6作了性能優化,它會經歷偏向鎖,輕量級鎖,最後纔到重量級鎖這個過程,在性能方面有了很大的提高,在jdk1.7的ConcurrentHashMap是基於ReentrantLock的實現了鎖,但在jdk1.8以後又替換成了synchronized,就從這一點能夠看出JVM團隊對synchronized的性能仍是挺有信心的。下面咱們分別來介紹下無鎖,偏向鎖,輕量級鎖,重量級鎖。下面咱們我畫張圖來描述這幾個級別鎖的在對象頭存儲狀態。如圖所示。
下面咱們代碼來看下偏向鎖的鎖狀態。
package com.duyang.base.basic.markword; import lombok.SneakyThrows; import org.openjdk.jol.info.ClassLayout; /** * @author :jiaolian * @date :Created in 2020-12-19 11:25 * @description:markword測試 * @modified By: * 公衆號:叫練 */ public class MarkWordTest { private static Fruit fruit = new Fruit(); public static void main(String[] args) throws InterruptedException { Task task = new Task(); Thread threadA = new Thread(task); Thread threadB = new Thread(task); Thread threadC = new Thread(task); threadA.start(); //threadA.join(); //threadB.start(); //threadC.start(); } private static class Task extends Thread { @SneakyThrows @Override public void run() { synchronized (fruit) { System.out.println("==================="+Thread.currentThread().getId()+" "); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.print(ClassLayout.parseInstance(fruit).toPrintable()); } } } }
上面代碼啓動線程A,控制檯輸出以下圖所示,紅色標記3個bit是101分別表示,高位的1表示是偏向鎖,01是偏向鎖標識位。符合偏向鎖標識的狀況。
下面咱們代碼來測試下輕量級鎖的鎖狀態。
打開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一直是個很重要的話題,也是面試中常見的考點。但願你們都能儘快理解掌握,分享給大家但願大家喜歡!
我是叫練,多叫多練,歡迎你們和我一塊兒討論交流,我會盡快回復你們,喜歡點贊和關注哦!公衆號【叫練】。