碼農會鎖,synchronized 對象頭結構(mark-word、Klass Pointer)、指針壓縮、鎖競爭,源碼解毒!

做者:小傅哥
博客:https://bugstack.cnhtml

沉澱、分享、成長,讓本身和他人都能有所收穫!😄

1、前言

感受什麼都不會,從哪開始呀!java

這是最近我總能被問到的問題,也確實是。一個初入編程職場的新人,或是一個想從新努力學習的老司機,這也不會,那也不會,總會犯愁從哪開始。git

講道理,畢竟 Java 涉及的知識太多了,要學應該是學會學習的能力,而不是去背題、背答案,拾人牙慧是不會有太多收益的。github

學習的過程要找對方法,遇到問題時最好能本身想一想,你有哪些方式學會這些知識。是不感受即便讓你去百度搜,你都不知道應該拿哪一個關鍵字搜!只能拿着問題直接找人問,這樣缺乏思考,缺乏大腦撞南牆的過程,其實最後也很難學會。面試

因此,你要學會的是自我學習的能力,以後是從哪開始均可以,重要的是開始和堅持!編程

2、面試題

謝飛機,小記,週末逛完奧特萊斯,回來就跑面試官家去了!安全

謝飛機:duang、duang、duang,我來了!性能優化

面試官:來的還挺準時,洗洗手吃飯吧!數據結構

謝飛機:嘿嘿...併發

面試官:你看我這塊魚豆腐,像不像 synchronized 鎖!

謝飛機:啊!?

面試官:飛機,正好問你。synchronized、volatile,有什麼區別呀?

謝飛機:嗯,volatile 保證可見性,synchronized 保證原子性!

面試官:那不用 volatile,只用 synchronized 修飾方式,能保證可見性嗎?

謝飛機:這...,我沒驗證過!

面試官:吃吧,吃吧!一會給你個 synchronized 學習大綱,照着整理知識點!

3、synchronized 解毒

圖 15-0 面試官給謝飛機的,synchronized 學習大綱

1. 對象結構

1.1 對象結構介紹

圖 15-1 64位JVM對象結構描述

HotSpot虛擬機 markOop.cpp 中的 C++ 代碼註釋片斷,描述了 64bits 下 mark-word 的存儲狀態,也就是圖 15-1 的結構示意。

這部分的源碼註釋以下:

64 bits:
--------
unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
size:64 ----------------------------------------------------->| (CMS free block)

unused:25 hash:31 -->| cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && normal object)
JavaThread*:54 epoch:2 cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && biased object)
narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)

源碼地址jdk8/hotspot/file/vm/oops/markOop.hpp

HotSpot虛擬機中,對象在內存中存儲的佈局能夠分爲三塊區域:對象頭(Header)實例數據(Instance Data)對齊填充(Padding)

  • mark-word:對象標記字段佔4個字節,用於存儲一些列的標記位,好比:哈希值、輕量級鎖的標記位,偏向鎖標記位、分代年齡等。
  • Klass Pointer:Class對象的類型指針,Jdk1.8默認開啓指針壓縮後爲4字節,關閉指針壓縮(-XX:-UseCompressedOops)後,長度爲8字節。其指向的位置是對象對應的Class對象(其對應的元數據對象)的內存地址。
  • 對象實際數據:包括對象的全部成員變量,大小由各個成員變量決定,好比:byte佔1個字節8比特位、int佔4個字節32比特位。
  • 對齊:最後這段空間補全並不是必須,僅僅爲了起到佔位符的做用。因爲HotSpot虛擬機的內存管理系統要求對象起始地址必須是8字節的整數倍,因此對象頭正好是8字節的倍數。所以當對象實例數據部分沒有對齊的話,就須要經過對齊填充來補全。

另外,在mark-word鎖類型標記中,無鎖,偏向鎖,輕量鎖,重量鎖,以及GC標記,5種類中無法用2比特標記(2比特最終有4種組合00011011),因此無鎖、偏向鎖,前又佔了一位偏向鎖標記。最終:001爲無鎖、101爲偏向鎖。

1.2 驗證對象結構

爲了能夠更加直觀的看到對象結構,咱們能夠藉助 openjdk 提供的 jol-core 進行打印分析。

引入POM

<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-cli -->
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-cli</artifactId>
    <version>0.14</version>
</dependency>

測試代碼

public static void main(String[] args) {
    System.out.println(VM.current().details());
    Object obj = new Object();
    System.out.println(obj + " 十六進制哈希:" + Integer.toHexString(obj.hashCode()));
    System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
1.2.1 指針壓縮開啓(默認)

運行結果

# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# Objects are 8 bytes aligned.
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]

java.lang.Object 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)                           e5 01 00 f8 (11100101 00000001 00000000 11111000) (-134217243)
     12     4        (loss due to the next object alignment)
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

圖 15-2 指針壓縮開啓,對象頭佈局

  • Object對象,總共佔16字節
  • 對象頭佔 12 個字節,其中:mark-word 佔 8 字節、Klass Point 佔 4 字節
  • 最後 4 字節,用於數據填充找齊
1.2.2 指針壓縮關閉

Run-->Edit Configurations->VM Options 配置參數 -XX:-UseCompressedOops 關閉指針壓縮。

運行結果

java.lang.Object object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           01 12 0c 53 (00000001 00010010 00001100 01010011) (1393299969)
      4     4        (object header)                           02 00 00 00 (00000010 00000000 00000000 00000000) (2)
      8     4        (object header)                           00 1c b9 1b (00000000 00011100 10111001 00011011) (465116160)
     12     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

圖 15-3 指針壓縮關閉,對象頭佈局

  • 關閉指針壓縮後,mark-word 仍是佔 8 字節不變。
  • 重點在類型指針 Klass Point 的變化,由原來的 4 字節,如今擴增到 8 字節。
1.2.3 對象頭哈希值存儲驗證

接下來,咱們調整下測試代碼,看下哈希值在對象頭中具體是怎麼存放的。

測試代碼

public static void main(String[] args) {
    System.out.println(VM.current().details());
    Object obj = new Object();
    System.out.println(obj + " 十六進制哈希:" + Integer.toHexString(obj.hashCode()));
    System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
  • 改動很少,只是把哈希值和對象打印出來,方便咱們驗證對象頭關於哈希值的存放結果。

運行結果

圖 15-3 對象頭哈希值存放

  • 如圖 15-3,對象的哈希值是16進制的,0x2530c12
  • 在對象頭哈希值存放的結果上看,也有對應的數值。只不過這個結果是倒過來的。

關於這個倒過來的問題是由於,大小端存儲致使;

  • Big-Endian:高位字節存放於內存的低地址端,低位字節存放於內存的高地址端
  • Little-Endian:低位字節存放於內存的低地址端,高位字節存放於內存的高地址端

mark-word結構

圖 15-5 無鎖狀態,64位虛擬機mark-word結構

如圖 15-5 最右側的 3 Bit(1 Bit標識偏向鎖,2 Bit描述鎖的類型)是跟鎖類型和GC標記相關的,而 synchronized 的鎖優化升級膨脹就是修改的這三位上的標識,來區分不一樣的鎖類型。從而採起不一樣的策略來提高性能。

1.3 Monitor 對象

在HotSpot虛擬機中,monitor是由C++中ObjectMonitor實現。

synchronized 的運行機制,就是當 JVM 監測到對象在不一樣的競爭情況時,會自動切換到適合的鎖實現,這種切換就是鎖的升級、降級。

那麼三種不一樣的 Monitor 實現,也就是常說的三種不一樣的鎖:偏斜鎖(Biased Locking)、輕量級鎖和重量級鎖。當一個 Monitor 被某個線程持有後,它便處於鎖定狀態。

Monitor 主要數據結構以下

// initialize the monitor, exception the semaphore, all other fields
// are simple integers or pointers
ObjectMonitor() {
    _header       = NULL;
    _count        = 0;       // 記錄個數
    _waiters      = 0,
    _recursions   = 0;       // 線程重入次數
    _object       = NULL;    // 存儲 Monitor 對象
    _owner        = NULL;    // 持有當前線程的 owner
    _WaitSet      = NULL;    // 處於wait狀態的線程,會被加入到 _WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;   // 單向列表
    FreeNext      = NULL ;
    _EntryList    = NULL ;   // 處於等待鎖block狀態的線程,會被加入到該列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
}

源碼地址jdk8/hotspot/file/vm/runtime/objectMonitor.hpp

  • ObjectMonitor,有兩個隊列:_WaitSet _EntryList,用來保存 ObjectWaiter 對象列表。
  • _owner,獲取 Monitor 對象的線程進入 _owner 區時, _count + 1。若是線程調用了 wait() 方法,此時會釋放 Monitor 對象, _owner 恢復爲空, _count - 1。同時該等待線程進入 _WaitSet 中,等待被喚醒。

鎖🔒執行效果以下

圖 15-06,鎖🔒執行效果

如圖 15-06,每一個 Java 對象頭中都包括 Monitor 對象(存儲的指針的指向),synchronized 也就是經過這一種方式獲取鎖,也就解釋了爲何 synchronized() 括號裏聽任何對象都能得到鎖🔒!

2. synchronized 特性

2.1 原子性

原子性是指一個操做是不可中斷的,要麼所有執行成功要麼所有執行失敗。

案例代碼

private static volatile int counter = 0;
public static void main(String[] args) throws InterruptedException {
    for (int i = 0; i < 10; i++) {
        Thread thread = new Thread(() -> {
            for (int i1 = 0; i1 < 10000; i1++) {
                add();
            }
        });
        thread.start();
    }
    // 等10個線程運行完畢
    Thread.sleep(1000);
    System.out.println(counter);
}
public static void add() {
    counter++;
}

這段代碼開啓了 10 個線程來累加 counter,按照預期結果應該是 100000。但實際運行會發現,counter 值每次運行都小於 10000,這是由於 volatile 並不能保證原子性,因此最後的結果不會是10000。

修改方法 add(),添加 synchronized:

public static void add() {
    synchronized (AtomicityTest.class) {
        counter++;
    }
}

這回測試結果就是:100000 了!

由於 synchronized 能夠保證統一時間只有一個線程能拿到鎖,進入到代碼塊執行。

反編譯查看指令碼

javap -v -p AtomicityTest

public static void add();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=0
         0: ldc           #12                 // class org/itstack/interview/AtomicityTest
         2: dup
         3: astore_0
         4: monitorenter
         5: getstatic     #10                 // Field counter:I
         8: iconst_1
         9: iadd
        10: putstatic     #10                 // Field counter:I
        13: aload_0
        14: monitorexit
        15: goto          23
        18: astore_1
        19: aload_0
        20: monitorexit
        21: aload_1
        22: athrow
        23: return
      Exception table:

同步方法

ACC_SYNCHRONIZED 這是一個同步標識,對應的16進制值是 0x0020

這10個線程進入這個方法時,都會判斷是否有此標識,而後開始競爭 Monitor 對象。

同步代碼

  • monitorenter,在判斷擁有同步標識 ACC_SYNCHRONIZED 搶先進入此方法的線程會優先擁有 Monitor 的 owner ,此時計數器 +1。
  • monitorexit,當執行完退出後,計數器 -1,歸 0 後被其餘進入的線程得到。

2.2 可見性

在上一章節 volatile 篇中,咱們知道它保證變量對全部線程的可見性。最終的效果就是在添加 volatile 的屬性變量時,線程A修改值後,線程B使用此變量能夠作出相應的反應,好比 while(!變量) 退出。

那麼,synchronized 具有可見性嗎,咱們作給例子。

public static boolean sign = false;
public static void main(String[] args) {
    Thread Thread01 = new Thread(() -> {
        int i = 0;
        while (!sign) {
            i++;
            add(i);
        }
    });
    Thread Thread02 = new Thread(() -> {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException ignore) {
        }
        sign = true;
        logger.info("vt.sign = true  while (!sign)")
    });
    Thread01.start();
    Thread02.start();
}

public static int add(int i) {
    return i + 1;
}

這是兩個線程操做一個變量的例子,由於線程間對變量 sign 的不可見性,線程 Thread01 中的 while (!sign) 會一直執行,不會隨着線程 Thread02 修改 sign = true 而退出循環。

如今咱們給方法 add 添加 synchronized 關鍵字修飾,以下:

public static synchronized int add(int i) {
    return i + 1;
}

添加後運行結果

23:55:33.849 [Thread-1] INFO  org.itstack.interview.VisibilityTest - vt.sign = true  while (!sign)

Process finished with exit code 0

能夠看到當線程 Thread02 改變變量 sign = true 後,線程 Thread01 當即退出了循環。

注意:不要在方法中添加 System.out.println() ,由於這個方法中含有 synchronized 會影響測試結果!

那麼爲何添加 synchronized 也能保證變量的可見性呢?

由於:

  1. 線程解鎖前,必須把共享變量的最新值刷新到主內存中。
  2. 線程加鎖前,將清空工做內存中共享變量的值,從而使用共享變量時須要從主內存中從新讀取最新的值。
  3. volatile 的可見性都是經過內存屏障(Memnory Barrier)來實現的。
  4. synchronized 靠操做系統內核互斥鎖實現,至關於 JMM 中的 lock、unlock。退出代碼塊時刷新變量到主內存。

2.3 有序性

as-if-serial,保證無論編譯器和處理器爲了性能優化會如何進行指令重排序,都須要保證單線程下的運行結果的正確性。也就是常說的:若是在本線程內觀察,全部的操做都是有序的;若是在一個線程觀察另外一個線程,全部的操做都是無序的。

這裏有一段雙重檢驗鎖(Double-checked Locking)的經典案例:

public class Singleton {
    private Singleton() {
    }

    private volatile static Singleton instance;

    public Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

}

爲何,synchronized 也有可見性的特色,還須要 volatile 關鍵字?

由於,synchronized 的有序性,不是 volatile 的防止指令重排序。

那若是不加 volatile 關鍵字可能致使的結果,就是第一個線程在初始化初始化對象,設置 instance 指向內存地址時。第二個線程進入時,有指令重排。在判斷 if (instance == null) 時就會有出錯的可能,由於這會可能 instance 可能尚未初始化成功。

2.4 可重入性

synchronized 是可重入鎖,也就是說,容許一個線程二次請求本身持有對象鎖的臨界資源,這種狀況稱爲可重入鎖🔒。

那麼咱們就寫一個例子,來證實這樣的狀況。

public class ReentryTest extends A{

    public static void main(String[] args) {
        ReentryTest reentry = new ReentryTest();
        reentry.doA();
    }

    public synchronized void doA() {
        System.out.println("子類方法:ReentryTest.doA() ThreadId:" + Thread.currentThread().getId());
        doB();
    }

    private synchronized void doB() {
        super.doA();
        System.out.println("子類方法:ReentryTest.doB() ThreadId:" + Thread.currentThread().getId());
    }

}


class A {
    public synchronized void doA() {
        System.out.println("父類方法:A.doA() ThreadId:" + Thread.currentThread().getId());
    }
}

測試結果

子類方法:ReentryTest.doA() ThreadId:1
父類方法:A.doA() ThreadId:1
子類方法:ReentryTest.doB() ThreadId:1

Process finished with exit code 0

這段單例代碼是遞歸調用含有 synchronized 鎖的方法,從運行正常的測試結果看,並無發生死鎖。全部能夠證實 synchronized 是可重入鎖。

synchronized鎖對象的時候有個計數器,他會記錄下線程獲取鎖的次數,在執行完對應的代碼塊以後,計數器就會-1,直到計數器清零,就釋放鎖了。

之因此,是能夠重入。是由於 synchronized 鎖對象有個計數器,會隨着線程獲取鎖後 +1 計數,當線程執行完畢後 -1,直到清零釋放鎖。

3. 鎖升級過程

關於 synchronized 鎖🔒升級有一張很是完整的圖,能夠參考:

圖 15-7 synchronized 鎖升級過程

synchronized 鎖有四種交替升級的狀態:無鎖、偏向鎖、輕量級鎖和重量級,這幾個狀態隨着競爭狀況逐漸升級。

3.1 偏向鎖

synchronizer源碼:/src/share/vm/runtime/synchronizer.cpp

// NOTE: must use heavy weight monitor to handle jni monitor exit
void ObjectSynchronizer::jni_exit(oop obj, Thread* THREAD) {
  TEVENT (jni_exit) ;
  if (UseBiasedLocking) {
    Handle h_obj(THREAD, obj);
    BiasedLocking::revoke_and_rebias(h_obj, false, THREAD);
    obj = h_obj();
  }
  assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");

  ObjectMonitor* monitor = ObjectSynchronizer::inflate(THREAD, obj);
  // If this thread has locked the object, exit the monitor.  Note:  can't use
  // monitor->check(CHECK); must exit even if an exception is pending.
  if (monitor->check(THREAD)) {
     monitor->exit(true, THREAD);
  }
}
  • UseBiasedLocking 是一個偏向鎖檢查,1.6以後是默認開啓的,1.5中是關閉的,須要手動開啓參數是 XX:-UseBiasedLocking=false

偏斜鎖會延緩 JIT 預熱進程,因此不少性能測試中會顯式地關閉偏斜鎖,偏斜鎖並不適合全部應用場景,撤銷操做(revoke)是比較重的行爲,只有當存在較多不會真正競爭的 synchronized 塊兒時,才能體現出明顯改善。

3.2 輕量級鎖

當鎖是偏向鎖的時候,被另外一個線程所訪問,偏向鎖就會升級爲輕量級鎖,其餘線程會經過自旋的形式嘗試獲取鎖,不會阻塞,提升性能。

在代碼進入同步塊的時候,若是同步對象鎖狀態爲無鎖狀態(鎖標誌位爲「01」狀態,是否爲偏向鎖爲「0」),JVM虛擬機首先將在當前線程的棧幀中創建一個名爲鎖記錄(Lock Record)的空間,用於存儲鎖對象目前的Mark Word的拷貝,官方稱之爲 Displaced Mark Word。

3.3 自旋鎖

自旋鎖是指嘗試獲取鎖的線程不會當即阻塞,而是採用循環的方式去嘗試獲取鎖,這樣的好處是減小線程上下文切換的消耗,缺點是循環會消耗CPU。

自旋鎖的默認大小是10次,能夠調整:-XX:PreBlockSpin

若是自旋n次失敗了,就會升級爲重量級的鎖。重量級的鎖,在 1.3 Monitor 對象中已經介紹。

3.4 鎖會降級嗎?

以前一直了解到 Java 不會進行鎖降級,但最近整理了大量的資料發現鎖降級確實是會發生。

When safepoints are used?

Below are few reasons for HotSpot JVM to initiate a safepoint:
Garbage collection pauses
Code deoptimization
Flushing code cache
Class redefinition (e.g. hot swap or instrumentation)
Biased lock revocation
Various debug operation (e.g. deadlock check or stacktrace dump)

Biased lock revocation,當 JVM 進入安全點 SafePoint的時候,會檢查是否有閒置的 Monitor,而後試圖進行降級。

4、總結

  • 本章關於 synchronized 鎖涉及到了較多的C++源碼分析學習,源碼地址:https://github.com/JetBrains/jdk8u_hotspot
  • 關於鎖的細節挖掘除了本文提到的還有不少知識點能夠繼續學習,能夠結合 ifeve、併發編程、深刻理解JVM虛擬機,等系列知識整理。
  • 學習過程當中結合C++源代碼中關於鎖的實現,更容易理解可能本來晦澀難懂的概念。在結合實際的案例驗證,會容易接受這部分知識。
  • 好了,這篇就寫到這裏了,若是有觀點和文章不許確的表達歡迎留言,互相學習,互相掃盲,互相進步。

5、傅詩一手

  • 會所🏢,裏的碼農會鎖。
  • 擁擠🤼‍♂️,就需加價升級。
  • 項目🤯,按摩對象頭皮。
  • 效果🤨,可見原子有序。

6、系列推薦

相關文章
相關標籤/搜索