天天看點

淺談Java多線程與并發原理前序深入了解synchronized底層實作原理:

前序

線程安全問題的主要誘因

  1. 存在共享資料(也稱臨界資源)
  2. 存在多條線程共同操作這些共享資料

解決方法:同一時刻有且隻有一個線程在操作共享資料,其他線程必須等到該線程處理完資料後再對共享資料進行操作

互斥鎖的特征

互斥性:即在同一時間隻允許一個線程持有某個對象鎖,通過這種特性來實作多線程協調機制,這樣在同一時間隻有一個線程對需要同步的代碼塊(複合操作)進行通路。互斥性也稱為操作的原子性。

可見性:必須確定在鎖被釋放之前,對共享變量所做的修改,對于随後獲得該鎖的另一個線程是可見的(即在獲得鎖時應該獲得最新共享變量的值),否則另一個線程可能是在本地緩存的某個副本上繼續操作,進而引起不一緻。

注:synchronized 鎖的不是代碼,鎖的是對象

擷取鎖的分類:擷取對象鎖、擷取類鎖

擷取對象鎖的兩種用法:

  • 同步代碼塊(synchronized(this),synchronized(類執行個體對象)),鎖是小括号中的執行個體對象
  • 同步非靜态方法(synchronized method) 鎖是目前對象的執行個體對象

擷取類鎖的兩種用法:

  • 同步代碼塊(synchronized(類.class)),鎖是小括号中的類對象(Class對象)
  • 同步非靜态方法(synchronized static method) 鎖是目前對象的類對象(Class對象)

類鎖和對象鎖在鎖同一個對象的時候表現行為是一樣的,因為class也是對象鎖,隻是比較特殊,所有的執行個體共享同一個類(同一個class對象)

如果鎖的是不同對象(同一個class的不同執行個體)表現就不一樣了,類鎖是全同步的,對象鎖是按對象區分同步的。

類鎖和對象鎖互不幹擾的,因為對象執行個體和類是兩個不同的對象。

對象鎖和類鎖的終結

  • 有線程通路對象的同步代碼塊時,另外的線程可以通路該對象的非同步代碼塊
  • 若鎖住的是同一個對象,一個線程在通路對象的同步代碼塊時,另一個通路對象的同步代碼塊的線程會被阻塞
  • 若鎖住的是同一個對象,一個線程在通路對象的同步方法時候另一個通路對象同步方法的線程會被阻塞
  • 若鎖住的是同一個對象,一個線程在通路對象的同步代碼塊時,另一個線程通路對象同步方法會被阻塞,反之亦然
  • 同一個類的不同對象鎖互不幹擾
  • 類鎖由于是一種特殊的對象鎖,是以表現和上述1、2、3、4一緻,而由于一個類隻有一把對象鎖,是以同一個類的不同對象使用類鎖将會是同步的
  • 類鎖和對象鎖互不幹擾

樂觀鎖

樂觀鎖是一種樂觀思想,即認為讀多寫少,遇到并發寫的可能性低,每次去拿資料的時候都認為别人不會修改,是以不會上鎖,但是在更新的時候會判斷一下在此期間别人有沒有去更新這個資料,采取在寫時先讀出目前版本号,然後加鎖操作(比較跟上一次的版本号,如果一樣則更新),如果失敗則要重複讀-比較-寫的操作。

java中的樂觀鎖基本都是通過CAS操作實作的,CAS是一種更新的原子操作,比較目前值跟傳入值是否一樣,一樣則更新,否則失敗。

悲觀鎖

悲觀鎖是就是悲觀思想,即認為寫多,遇到并發寫的可能性高,每次去拿資料的時候都認為别人會修改,是以每次在讀寫資料的時候都會上鎖,這樣别人想讀寫這個資料就會block直到拿到鎖。java中的悲觀鎖就是Synchronized,AQS架構下的鎖則是先嘗試cas樂觀鎖去擷取鎖,擷取不到,才會轉換為悲觀鎖,如RetreenLock。

阻塞代價

java的線程是映射到作業系統原生線程之上的,如果要阻塞或喚醒一個線程就需要作業系統介入,需要在戶态與核心态之間切換,這種切換會消耗大量的系統資源,因為使用者态與核心态都有各自專用的記憶體空間,專用的寄存器等,使用者态切換至核心态需要傳遞給許多變量、參數給核心,核心也需要保護好使用者态在切換時的一些寄存器值、變量等,以便核心态調用結束後切換回使用者态繼續工作。

  • 如果線程狀态切換是一個高頻操作時,這将會消耗很多CPU處理時間;
  • 如果對于那些需要同步的簡單的代碼塊,擷取鎖挂起操作消耗的時間比使用者代碼執行的時間還要長,這種同步政策顯然非常糟糕的。

synchronized會導緻争用不到鎖的線程進入阻塞狀态,是以說它是java語言中一個重量級的同步操縱,被稱為重量級鎖,為了緩解上述性能問題,JVM從1.5開始,引入了輕量鎖與偏向鎖,預設啟用了自旋鎖,他們都屬于樂觀鎖。

深入了解synchronized底層實作原理:

Java對象頭和Monitor是實作synchronized的基礎

hotspot中對象在記憶體的布局是分3部分 :

  1. 對象頭
  2. 執行個體資料
  3. 對其填充

    這裡主要講對象頭:一般而言synchronized使用的鎖對象是存儲在對象頭裡的,對象頭是由Mark Word和Class Metadata Address組成

要詳細了解java對象的結構點選:

https://blog.csdn.net/zqz_zqz/article/details/70246212
虛拟機位數 頭對象結構 說明
32/64bit Mark Word 預設存儲對象的hashCode,分代年齡、鎖類型、鎖标志位等資訊
Class Metadata Address 類型指針指向對象的類中繼資料,JVM通過這個指針确定該對象是哪個類型的資料

mark word存儲自身運作時資料,是實作輕量級鎖和偏向鎖的關鍵,預設存儲對象的hasCode、分代年齡、鎖類型、鎖标志位等資訊。

mark word資料的長度在32位和64位的虛拟機(未開啟壓縮指針)中分别為32bit和64bit,它的最後2bit是鎖狀态标志位,用來标記目前對象的狀态,對象的所處的狀态,決定了markword存儲的内容,如下表所示:

淺談Java多線程與并發原理前序深入了解synchronized底層實作原理:

由于對象頭的資訊是與對象定義的資料沒有關系的額外存儲成本,是以考慮到jvm的空間效率,mark word 被設計出一個非固定的存儲結構,以便存儲更多有效的資料,它會根據對象本身的狀态複用自己的存儲空間(輕量級鎖和偏向鎖是java6後對synchronized優化後新增加的)

Monitor:每個Java對象天生就自帶了一把看不見的鎖,它叫内部鎖或者Monitor鎖(螢幕鎖)。上圖的重量級鎖的指針指向的就是Monitor的起始位址。

每個對象都存在一個Monitor與之關聯,對象與其Monitor之間的關系存在多種實作方式,如Monitor可以和對象一起建立銷毀、或當線程擷取對象鎖時自動生成,當線程擷取鎖時Monitor處于鎖定狀态。

Monitor是虛拟機源碼裡面用C++實作的。

淺談Java多線程與并發原理前序深入了解synchronized底層實作原理:

源碼解讀:

_WaitSet

_EntryList

就是之前學的等待池和鎖池,

_owner

是指向持有Monitor對象的線程。當多個線程通路同一個對象的同步代碼的時候,首先會進入到

_EntryList

集合裡面,當線程擷取到對象Monitor後就會進入到

_object

區域并把

_owner

設定成目前線程,同時Monitor裡面的

_count

會加一。當調用wait方法會釋放目前對象的Monitor,

_owner

恢複成null,

_count

減一,同時該線程執行個體進入

_WaitSet

集合中等待喚醒。如果目前線程執行完畢也會釋放Monitor鎖并複位對應變量的值。

淺談Java多線程與并發原理前序深入了解synchronized底層實作原理:

接下來是位元組碼的分析:

package interview.thread;

/**
 * 位元組碼分析synchronized
 * @Author: hankli
 * @Date: 2019/5/20 13:50
 */
public class SyncBlockAndMethod {
    public void syncsTask() {
        synchronized (this) {
            System.out.println("Hello");
        }
    }

    public synchronized void syncTask() {
        System.out.println("Hello Again");
    }
}           

然後控制台輸入

javac thread/SyncBlockAndMethod.java

然後反編譯

javap -verbose thread/SyncBlockAndMethod.class

先看看syncsTask方法裡的同步代碼塊:

淺談Java多線程與并發原理前序深入了解synchronized底層實作原理:

從位元組碼中可以看出 同步代碼塊 使用的是 monitorenter 和 monitorexit ,當執行monitorenter指令時目前線程講試圖擷取對象的鎖,當Monitor的count 為0時将獲的monitor,并将count設定為1表示取鎖成功。如果目前線程之前有這個monitor的持有權它可以重入這個Monnitor。monitorexit指令會釋放monitor鎖并将計數器設為0。為了保證正常執行monitorenter 和 monitorexit 編譯器會自動生成一個異常處理器,該處理器可以處理所有異常。主要保證異常結束時monitorexit(位元組碼中多了個monitorexit指令的目的)釋放monitor鎖

注:重入是從互斥鎖的設計上來說的,當一個線程試圖操作一個由其他線程持有的對象鎖的臨界資源時,将會處于阻塞狀态,當一個線程再次請求自己持有對象鎖的臨界資源時,這種情況屬于重入。就像如下情況:hello2也是會輸出的,并不會鎖住。

淺談Java多線程與并發原理前序深入了解synchronized底層實作原理:

再看看syncTask同步方法:

淺談Java多線程與并發原理前序深入了解synchronized底層實作原理:

解讀:這個位元組碼中沒有monitorenter和monitorexit指令并且位元組碼也比較短,其實方法級的同步是隐式實作的(無需位元組碼來控制)ACC_SYNCHRONIZED是用來區分一個方法是否同步方法,如果設定了ACC_SYNCHRONIZED執行線程将持有monitor,然後執行方法,無論方法是否正常完成都會釋放調monitor,在方法執行期間,其他線程都無法在獲得這個monitor。如果同步方法在執行期間抛出異常而且在方法内部無法處理此異常,那麼這個monitor将會在異常抛到方法之外時自動釋放。

java6之前Synchronized效率低下的原因:

在早期版本Synchronized屬于重量級鎖,性能低下,因為螢幕鎖(monitor)是依賴于底層作業系統的的MutexLock實作的。

而作業系統切換線程時需要從使用者态轉換到核心态,時間較長,開銷較大

java6以後Synchronized性能得到了很大提升(hotspot從jvm層面做了較大優化,減少重量級鎖的使用):

  • Adaptive Spinning 自适應自旋
  • Lock Eliminate 鎖消除
  • Lock Coarsening 鎖粗化
  • Lightweight Locking 輕量級鎖
  • Biased Locking偏向鎖
  • ……

自旋鎖:

  • 許多情況下,共享資料的鎖定狀态持續時間較短,切換線程不值得
  • 通過讓線程執行while循環等待鎖的釋放,不讓出CPU
  • java4就引入了,不過預設是關閉的,java6後預設開啟的
  • 自旋本質和阻塞狀态并不相同,如果鎖占用時間非常短,那自旋鎖性能會很好

缺點:若鎖被其他線程長時間占用,會帶來許多性能上的開銷,因為自旋一直會占用CPU資源且白白消耗掉CPU資源。

如果線程超過了限定次數還沒有擷取到鎖,就該使用傳統方式挂起線程(可以設定VM的PreBlockSpin參數來更改限定次數)