多線程的東西不少,也頗有意思,因此我最近的重心可能都是多線程的方向去靠了,不知道你們喜歡否?java
閱讀本文以前閱讀如下兩篇文章會幫助你更好的理解:c++
Volatilegit
樂觀鎖&悲觀鎖程序員
咱們正常去使用Synchronized通常都是用在下面這幾種場景:編程
修飾實例方法,對當前實例對象this加鎖api
public class Synchronized { public synchronized void husband(){ } }
修飾靜態方法,對當前類的Class對象加鎖緩存
public class Synchronized { public void husband(){ synchronized(Synchronized.class){ } } }
修飾代碼塊,指定一個加鎖的對象,給對象加鎖數據結構
public class Synchronized { public void husband(){ synchronized(new test()){ } } }
其實就是鎖方法、鎖代碼塊和鎖對象,那他們是怎麼實現加鎖的呢?多線程
在這以前,我就先跟你們聊一下咱們Java對象的構成併發
在 JVM 中,對象在內存中分爲三塊區域:
對象頭
實例數據
對其填充
Tip:不知道你們有沒有被問過一個空對象佔多少個字節?就是8個字節,是由於對齊填充的關係哈,不到8個字節對其填充會幫咱們自動補齊。
咱們常常說到的,有序性、可見性、原子性,synchronized又是怎麼作到的呢?
我在Volatile章節已經說過了CPU會爲了優化咱們的代碼,會對咱們程序進行重排序。
無論編譯器和CPU如何重排序,必須保證在單線程狀況下程序的結果是正確的,還有就是有數據依賴的也是不能重排序的。
就好比:
int a = 1; int b = a;
這兩段是怎麼都不能重排序的,b的值依賴a的值,a若是不先賦值,那就爲空了。
一樣在Volatile章節我介紹到了現代計算機的內存結構,以及JMM(Java內存模型),這裏我須要說明一下就是JMM並非實際存在的,而是一套規範,這個規範描述了不少java程序中各類變量(線程共享變量)的訪問規則,以及在JVM中將變量存儲到內存和從內存中讀取變量這樣的底層細節,Java內存模型是對共享數據的可見性、有序性、和原子性的規則和保障。
你們感興趣,也記得去了解計算機的組成部分,cpu、內存、多級緩存等,會幫助更好的理解java這麼作的緣由。
其實他保證原子性很簡單,確保同一時間只有一個線程能拿到鎖,可以進入代碼塊這就夠了。
這幾個是咱們使用鎖常常用到的特性,那synchronized他本身自己又具備哪些特性呢?
synchronized鎖對象的時候有個計數器,他會記錄下線程獲取鎖的次數,在執行完對應的代碼塊以後,計數器就會-1,直到計數器清零,就釋放鎖了。
那可重入有什麼好處呢?
能夠避免一些死鎖的狀況,也可讓咱們更好封裝咱們的代碼。
不可中斷就是指,一個線程獲取鎖以後,另一個線程處於阻塞或者等待狀態,前一個不釋放,後一個也一直會阻塞或者等待,不能夠被中斷。
值得一提的是,Lock的tryLock方法是能夠被中斷的。
這裏看實現很簡單,我寫了一個簡單的類,分別有鎖方法和鎖代碼塊,咱們反編譯一下字節碼文件,就能夠了。
先看看我寫的測試類:
/** *@Description: Synchronize *@Author: 敖丙 *@date: 2020-05-17 **/ public class Synchronized { public synchronized void husband(){ synchronized(new Volatile()){ } } }
編譯完成,咱們去對應目錄執行 javap -c xxx.class 命令查看反編譯的文件:
MacBook-Pro-3:juc aobing$ javap -p -v -c Synchronized.class Classfile /Users/aobing/IdeaProjects/Thanos/laogong/target/classes/juc/Synchronized.class Last modified 2020-5-17; size 375 bytes MD5 checksum 4f5451a229e80c0a6045b29987383d1a Compiled from "Synchronized.java" public class juc.Synchronized minor version: 0 major version: 49 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #3.#14 // java/lang/Object."<init>":()V #2 = Class #15 // juc/Synchronized #3 = Class #16 // java/lang/Object #4 = Utf8 <init> #5 = Utf8 ()V #6 = Utf8 Code #7 = Utf8 LineNumberTable #8 = Utf8 LocalVariableTable #9 = Utf8 this #10 = Utf8 Ljuc/Synchronized; #11 = Utf8 husband #12 = Utf8 SourceFile #13 = Utf8 Synchronized.java #14 = NameAndType #4:#5 // "<init>":()V #15 = Utf8 juc/Synchronized #16 = Utf8 java/lang/Object { public juc.Synchronized(); descriptor: ()V flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 8: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Ljuc/Synchronized; public synchronized void husband(); descriptor: ()V flags: ACC_PUBLIC, ACC_SYNCHRONIZED // 這裏 Code: stack=2, locals=3, args_size=1 0: ldc #2 // class juc/Synchronized 2: dup 3: astore_1 4: monitorenter // 這裏 5: aload_1 6: monitorexit // 這裏 7: goto 15 10: astore_2 11: aload_1 12: monitorexit // 這裏 13: aload_2 14: athrow 15: return Exception table: from to target type 5 7 10 any 10 13 10 any LineNumberTable: line 10: 0 line 12: 5 line 13: 15 LocalVariableTable: Start Length Slot Name Signature 0 16 0 this Ljuc/Synchronized; } SourceFile: "Synchronized.java"
你們能夠看到幾處我標記的,我在最開始提到過對象頭,他會關聯到一個monitor對象。
全部的互斥,其實在這裏,就是看你可否得到monitor的全部權,一旦你成爲owner就是得到者。
不知道你們注意到方法那的一個特殊標誌位沒,ACC_SYNCHRONIZED。
同步方法的時候,一旦執行到這個方法,就會先判斷是否有標誌位,而後,ACC_SYNCHRONIZED會去隱式調用剛纔的兩個指令:monitorenter和monitorexit。
因此歸根究底,仍是monitor對象的爭奪。
我說了這麼屢次這個對象,你們是否是覺得就是個虛無的東西,其實不是,monitor監視器源碼是C++寫的,在虛擬機的ObjectMonitor.hpp文件中。
我看了下源碼,他的數據結構長這樣:
ObjectMonitor() { _header = NULL; _count = 0; _waiters = 0, _recursions = 0; // 線程重入次數 _object = NULL; // 存儲Monitor對象 _owner = NULL; // 持有當前線程的owner _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; }
這塊c++代碼,我也放到了個人開源項目了,你們自行查看。
synchronized底層的源碼就是引入了ObjectMonitor,這一塊你們有興趣能夠看看,反正我上面說的,還有你們常常聽到的概念,在這裏都能找到源碼。
你們說熟悉的鎖升級過程,其實就是在源碼裏面,調用了不一樣的實現去獲取獲取鎖,失敗就調用更高級的實現,最後升級完成。
你們在看ObjectMonitor源碼的時候,會發現Atomic::cmpxchg_ptr,Atomic::inc_ptr等內核函數,對應的線程就是park()和upark()。
這個操做涉及用戶態和內核態的轉換了,這種切換是很耗資源的,因此知道爲啥有自旋鎖這樣的操做了吧,按道理相似死循環的操做更費資源纔是對吧?其實不是,你們瞭解一下就知道了。
Linux系統的體系結構你們大學應該都接觸過了,分爲用戶空間(應用程序的活動空間)和內核。
咱們全部的程序都在用戶空間運行,進入用戶運行狀態也就是(用戶態),可是不少操做可能涉及內核運行,比我I/O,咱們就會進入內核運行狀態(內核態)。
這個過程是很複雜的,也涉及不少值的傳遞,我簡單歸納下流程:
因此你們一直說,1.6以前是重量級鎖,沒錯,可是他重量的本質,是ObjectMonitor調用的過程,以及Linux內核的複雜運行機制決定的,大量的系統資源消耗,因此效率才低。
還有兩種狀況也會發生內核態和用戶態的切換:異常事件和外圍設備的中斷 你們也能夠了解下。
那都說過了效率低,官方也是知道的,因此他們作了升級,你們若是看了我剛纔提到的那些源碼,就知道他們的升級其實也作得很簡單,只是多了幾個函數調用,不過不得不設計仍是很巧妙的。
咱們就來看一下升級後的鎖升級過程:
簡單版本:
升級方向:
Tip:切記這個升級過程是不可逆的,最後我會說明他的影響,涉及使用場景。
看完他的升級,咱們就來好好聊聊每一步怎麼作的吧。
以前我提到過了,對象頭是由Mark Word和Klass pointer 組成,鎖爭奪也就是對象頭指向的Monitor對象的爭奪,一旦有線程持有了這個對象,標誌位修改成1,就進入偏向模式,同時會把這個線程的ID記錄在對象的Mark Word中。
這個過程是採用了CAS樂觀鎖操做的,每次同一線程進入,虛擬機就不進行任何同步的操做了,對標誌位+1就行了,不一樣線程過來,CAS會失敗,也就意味着獲取鎖失敗。
偏向鎖在1.6以後是默認開啓的,1.5中是關閉的,須要手動開啓參數是xx:-UseBiasedLocking=false。
偏向鎖關閉,或者多個線程競爭偏向鎖怎麼辦呢?
仍是跟Mark Work 相關,若是這個對象是無鎖的,jvm就會在當前線程的棧幀中創建一個叫鎖記錄(Lock Record)的空間,用來存儲鎖對象的Mark Word 拷貝,而後把Lock Record中的owner指向當前對象。
JVM接下來會利用CAS嘗試把對象本來的Mark Word 更新會Lock Record的指針,成功就說明加鎖成功,改變鎖標誌位,執行相關同步操做。
若是失敗了,就會判斷當前對象的Mark Word是否指向了當前線程的棧幀,是則表示當前的線程已經持有了這個對象的鎖,不然說明被其餘線程持有了,繼續鎖升級,修改鎖的狀態,以後等待的線程也阻塞。
我不是在上面提到了Linux系統的用戶態和內核態的切換很耗資源,其實就是線程的等待喚起過程,那怎麼才能減小這種消耗呢?
自旋,過來的如今就不斷自旋,防止線程被掛起,一旦能夠獲取資源,就直接嘗試成功,直到超出閾值,自旋鎖的默認大小是10次,-XX:PreBlockSpin能夠修改。
自旋都失敗了,那就升級爲重量級的鎖,像1.5的同樣,等待喚起咯。
至此我基本上吧synchronized的先後概念都講到了,你們好好消化。
資料參考:《高併發編程》《黑馬程序員講義》《深刻理解JVM虛擬機》
咱們先看看他們的區別:
二者一個是JDK層面的一個是JVM層面的,我以爲最大的區別其實在,咱們是否須要豐富的api,還有一個咱們的場景。
好比我如今是滴滴,我早上有打車高峯,我代碼使用了大量的synchronized,有什麼問題?鎖升級過程是不可逆的,過了高峯咱們仍是重量級的鎖,那效率是否是大打折扣了?這個時候你用Lock是否是很好?
場景是必定要考慮的,我如今告訴你哪一個好都是扯淡,由於脫離了業務,一切技術討論都沒有了價值。
我是敖丙,一個在互聯網苟且偷生的工具人。
你知道的越多,你不知道的越多,人才們的 【三連】 就是丙丙創做的最大動力,咱們下期見!
注:若是本篇博客有任何錯誤和建議,歡迎人才們留言!