07-深入理解synchronized

Java共享內存模型帶來的線程安全問題

思考: 兩個線程對初始值爲 0 的靜態變量一個做自增,一個做自減,各做 5000 次,結果是 0 嗎?

@Slf4j
public class SyncDemo {

    private static volatile int counter = 0;

    public static void increment() {
        counter++;
    }

    public static void decrement() {
        counter--;
    }

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                increment();
            }
        }, "t1");

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                decrement();
            }
        }, "t2");

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        //思考: counter=?
        log.info("counter={}", counter);


    }
}

問題分析

以上的結果可能是正數、負數、零。爲什麼呢?因爲 Java 中對靜態變量的自增,自減並不是原子操作。

我們可以查看 i++和 i--(i 爲靜態變量)的 JVM 字節碼指令 ( 可以在idea中安裝一個jclasslib插件)

i++的JVM 字節碼指令

getstatic i // 獲取靜態變量i的值 
iconst_1 // 將int常量1壓入操作數棧
iadd // 自增 
putstatic //寫回共享資源

i--的JVM 字節碼指令

getstatic i // 獲取靜態變量i的值 
iconst_1 // 將int常量1壓入操作數棧
isub // 自減 
putstatic //寫回共享資源

如果是單線程以上代碼是順序執行(不會交錯)沒有問題。

但多線程下代碼可能交錯運行:

image

臨界區( Critical Section)

  • 一個程序運行多個線程本身是沒有問題的

  • 問題出在多個線程訪問共享資源

      • 多個線程讀共享資源其實也沒有問題
      • 在多個線程對共享資源讀寫操作時發生指令交錯,就會出現問題

一段代碼塊內如果存在對共享資源的多線程讀寫操作,稱這段代碼塊爲臨界區,其共享資源爲臨界資源

//臨界資源
private static int counter = 0;

public static void increment() { //臨界區
    counter++;
}

public static void decrement() {//臨界區
    counter--;
}

競態條件( Race Condition )

多個線程在臨界區內執行,由於代碼的執行序列不同而導致結果無法預測,稱之爲發生了競態條件

爲了避免臨界區的競態條件發生,有多種手段可以達到目的:

  • 阻塞式的解決方案:synchronized,Lock
  • 非阻塞式的解決方案:原子變量,CAS

注意:

雖然 java 中互斥和同步都可以採用 synchronized 關鍵字來完成,但它們還是有區別的:

互斥是保證臨界區的競態條件發生,同一時刻只能有一個線程執行臨界區代碼

同步是由於線程執行的先後、順序不同、需要一個線程等待其它線程運行到某個點

synchronized的使用

synchronized 同步塊是 Java 提供的一種原子性內置鎖,Java 中的每個對象都可以把它當作一個同步鎖來使用,這些 Java 內置的使用者看不到的鎖被稱爲內置鎖,也叫作監視器鎖。

加鎖方式

image

解決之前的共享問題

方式一

public static synchronized void increment() {
    counter++;
}

public static synchronized void decrement() {
    counter--;
}

方式二

private static String lock = "";

public static void increment() {
    synchronized (lock){
        counter++;
    }
}

public static void decrement() {
    synchronized (lock) {
        counter--;
    }
}

synchronized 實際是用對象鎖保證了臨界區內代碼的原子性

image

synchronized底層原理

synchronized是JVM內置鎖,基於Monitor機制實現,依賴底層操作系統的互斥原語Mutex(互斥量),它是一個重量級鎖,性能較低。當然,JVM內置鎖在1.5之後版本做了重大的優化,如鎖粗化(Lock Coarsening)、鎖消除(Lock Elimination)、輕量級鎖(Lightweight Locking)、偏向鎖(Biased Locking)、自適應自旋(Adaptive Spinning)等技術來減少鎖操作的開銷,內置鎖的併發性能已經基本與Lock持平。

The Java® Language Specification

Each object is associated with a monitor (§17.1), which is used by synchronized methods (§8.4.3) and the synchronized statement (§14.19) to provide control over concurrent access to state by multiple threads (§17 (Threads and Locks)).

The Java® Virtual Machine Specification

The Java Virtual Machine supports synchronization of both methods and sequences of instructions within a method by a single synchronization construct: the monitor.

Java虛擬機通過一個同步結構支持方法和方法中的指令序列的同步:monitor。

同步方法是通過方法中的access_flags中設置ACC_SYNCHRONIZED標誌來實現;同步代碼塊是通過monitorenter和monitorexit來實現。兩個指令的執行是JVM通過調用操作系統的互斥原語mutex來實現,被阻塞的線程會被掛起、等待重新調度,會導致“用戶態和內核態”兩個態之間來回切換,對性能有較大影響。

查看synchronized的字節碼指令序列

image

Monitor(管程/監視器)

Monitor,直譯爲“監視器”,而操作系統領域一般翻譯爲“管程”。管程是指管理共享變量以及對共享變量操作的過程,讓它們支持併發。在Java 1.5之前,Java語言提供的唯一併發語言就是管程,Java 1.5之後提供的SDK併發包也是以管程爲基礎的。除了Java之外,C/C++、C#等高級語言也都是支持管程的。synchronized關鍵字和wait()、notify()、notifyAll()這三個方法是Java中實現管程技術的組成部分。

MESA模型

在管程的發展史上,先後出現過三種不同的管程模型,分別是Hasen模型、Hoare模型和MESA模型。現在正在廣泛使用的是MESA模型。下面我們便介紹MESA模型:

image

管程中引入了條件變量的概念,而且每個條件變量都對應有一個等待隊列。條件變量和等待隊列的作用是解決線程之間的同步問題。

wait()的正確使用姿勢

對於MESA管程來說,有一個編程範式:

while(條件不滿足) {
  wait();
}

喚醒的時間和獲取到鎖繼續執行的時間是不一致的,被喚醒的線程再次執行時可能條件又不滿足了,所以循環檢驗條件。MESA模型的wait()方法還有一個超時參數,爲了避免線程進入等待隊列永久阻塞。

notify()和notifyAll()分別何時使用

滿足以下三個條件時,可以使用notify(),其餘情況儘量使用notifyAll():

  1. 所有等待線程擁有相同的等待條件;
  2. 所有等待線程被喚醒後,執行相同的操作;
  3. 只需要喚醒一個線程。

Java語言的內置管程synchronized

Java 參考了 MESA 模型,語言內置的管程(synchronized)對 MESA 模型進行了精簡。MESA 模型中,條件變量可以有多個,Java 語言內置的管程裏只有一個條件變量。模型如下圖所示。

image

Monitor機制在Java中的實現

java.lang.Object 類定義了 wait(),notify(),notifyAll() 方法,這些方法的具體實現,依賴於 ObjectMonitor 實現,這是 JVM 內部基於 C++ 實現的一套機制。

ObjectMonitor其主要數據結構如下(hotspot源碼ObjectMonitor.hpp):

ObjectMonitor() {
    _header       = NULL; //對象頭  markOop
    _count        = 0;  
    _waiters      = 0,   
    _recursions   = 0;   // 鎖的重入次數 
    _object       = NULL;  //存儲鎖對象
    _owner        = NULL;  // 標識擁有該monitor的線程(當前獲取鎖的線程) 
    _WaitSet      = NULL;  // 等待線程(調用wait)組成的雙向循環鏈表,_WaitSet是第一個節點
    _WaitSetLock  = 0 ;    
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ; //多線程競爭鎖會先存到這個單向鏈表中 (FILO棧結構)
    FreeNext      = NULL ;
    _EntryList    = NULL ; //存放在進入或重新進入時被阻塞(blocked)的線程 (也是存競爭鎖失敗的線程)
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;

image

在獲取鎖時,是將當前線程插入到cxq的頭部,而釋放鎖時,默認策略(QMode=0)是:如果EntryList爲空,則將cxq中的元素按原有順序插入到EntryList,並喚醒第一個線程,也就是當EntryList爲空時,是後來的線程先獲取鎖。_EntryList不爲空,直接從_EntryList中喚醒線程。

思考:synchronized加鎖加在對象上,鎖對象是如何記錄鎖狀態的?

對象的內存佈局

Hotspot虛擬機中,對象在內存中存儲的佈局可以分爲三塊區域:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。

  • 對象頭:比如 hash碼,對象所屬的年代,對象鎖,鎖狀態標誌,偏向鎖(線程)ID,偏向時間,數組長度(數組對象纔有)等。
  • 實例數據:存放類的屬性數據信息,包括父類的屬性信息;
  • 對齊填充:由於虛擬機要求 對象起始地址必須是8字節的整數倍。填充數據不是必須存在的,僅僅是爲了字節對齊。

image

對象頭詳解

HotSpot虛擬機的對象頭包括:

  • Mark Word

用於存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等,這部分數據的長度在32位和64位的虛擬機中分別爲32bit和64bit,官方稱它爲“Mark Word”。

  • Klass Pointer

對象頭的另外一部分是klass類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。 32位4字節,64位開啓指針壓縮或最大堆內存<32g時4字節,否則8字節。jdk1.8默認開啓指針壓縮後爲4字節,當在JVM參數中關閉指針壓縮(-XX:-UseCompressedOops)後,長度爲8字節。

  • 數組長度(只有數組對象有)

如果對象是一個數組, 那在對象頭中還必須有一塊數據用於記錄數組長度。 4字節

image

使用JOL工具查看內存佈局

給大家推薦一個可以查看普通java對象的內部佈局工具JOL(JAVA OBJECT LAYOUT),使用此工具可以查看new出來的一個java對象的內部佈局,以及一個普通的java對象佔用多少字節。

引入maven依賴

<!-- 查看Java 對象佈局、大小工具 -->
<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.10</version>
</dependency>

測試

import org.openjdk.jol.info.ClassLayout;

public class JolTest {

    public static void main(String[] args) {
        Object obj = new Object();
        //查看對象內部信息
        System.out.println(ClassLayout.parseInstance(obj).toPrintable());
    }
}

利用jol查看64位系統java對象(空對象),默認開啓指針壓縮,總大小顯示16字節,後4個字節用於對齊,前12字節爲對象頭,前8個爲Mark Word ,

image

  • OFFSET:偏移地址,單位字節;
  • SIZE:佔用的內存大小,單位爲字節;
  • TYPE DESCRIPTION:類型描述,其中object header爲對象頭;
  • VALUE:對應內存中當前存儲的值,二進制32位;

壓縮指針

開啓壓縮指針。 默認開啓

-XX:+UseCompressedOops

關閉關閉壓縮指針。 對象頭16字節

-XX:-UseCompressedOops

image

回到之前的問題: synchronized加鎖加在對象上,對象是如何記錄鎖狀態的?

鎖狀態被記錄在每個對象的對象頭的Mark Word中

Mark Word是如何記錄鎖狀態的

Hotspot通過markOop類型實現Mark Word,具體實現位於markOop.hpp文件中。由於對象需要存儲的運行時數據很多,考慮到虛擬機的內存使用,markOop被設計成一個非固定的數據結構,以便在極小的空間存儲儘量多的數據,根據對象的狀態複用自己的存儲空間。

簡單點理解就是:MarkWord 結構搞得這麼複雜,是因爲需要節省內存,讓同一個內存區域在不同階段有不同的用處。

Mark Word的結構

//  32 bits:
//  --------
//             hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
//             JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)
//             size:32 ------------------------------------------>| (CMS free block)
//             PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
//
//  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)

。。。。。。
//    [JavaThread* | epoch | age | 1 | 01]       lock is biased toward given thread
//    [0           | epoch | age | 1 | 01]       lock is anonymously biased
//
//  - the two lock bits are used to describe three states: locked/unlocked and monitor.
//
//    [ptr             | 00]  locked             ptr points to real header on stack
//    [header      | 0 | 01]  unlocked           regular object header
//    [ptr             | 10]  monitor            inflated lock (header is wapped out)
//    [ptr             | 11]  marked             used by markSweep to mark an object
  • hash: 保存對象的哈希碼。運行期間調用System.identityHashCode()來計算,延遲計算,並把結果賦值到這裏。
  • age: 保存對象的分代年齡。表示對象被GC的次數,當該次數到達閾值的時候,對象就會轉移到老年代。
  • biased_lock: 偏向鎖標識位。由於無鎖和偏向鎖的鎖標識都是 01,沒辦法區分,這裏引入一位的偏向鎖標識位。
  • lock: 鎖狀態標識位。區分鎖狀態,比如11時表示對象待GC回收狀態, 只有最後2位鎖標識(11)有效。
  • JavaThread*: 保存持有偏向鎖的線程ID。偏向模式的時候,當某個線程持有對象的時候,對象這裏就會被置爲該線程的ID。 在後面的操作中,就無需再進行嘗試獲取鎖的動作。這個線程ID並不是JVM分配的線程ID號,和Java Thread中的ID是兩個概念。
  • epoch: 保存偏向時間戳。偏向鎖在CAS鎖操作過程中,偏向性標識,表示對象更偏向哪個鎖。
32位JVM下的對象結構描述

image

64位JVM下的對象結構描述

image

  • ptr_to_lock_record:輕量級鎖狀態下,指向棧中鎖記錄的指針。當鎖獲取是無競爭時,JVM使用原子操作而不是OS互斥,這種技術稱爲輕量級鎖定。在輕量級鎖定的情況下,JVM通過CAS操作在對象的Mark Word中設置指向鎖記錄的指針。
  • ptr_to_heavyweight_monitor:重量級鎖狀態下,指向對象監視器Monitor的指針。如果兩個不同的線程同時在同一個對象上競爭,則必須將輕量級鎖定升級到Monitor以管理等待的線程。在重量級鎖定的情況下,JVM在對象的ptr_to_heavyweight_monitor設置指向Monitor的指針

Mark Word中鎖標記枚舉

enum { locked_value             = 0,    //00 輕量級鎖 
         unlocked_value           = 1,   //001 無鎖
         monitor_value            = 2,   //10 監視器鎖,也叫膨脹鎖,也叫重量級鎖
         marked_value             = 3,   //11 GC標記
         biased_lock_pattern      = 5    //101 偏向鎖
}

更直觀的理解方式:

image

測試:利用JOL工具跟蹤鎖標記變化

偏向鎖

偏向鎖在jdk1.6之後是默認開啓的。通過jvm的參數-XX:-UseBiasedLocking,可以關閉偏向鎖,然後默認會進入輕量級鎖。

偏向鎖是一種針對加鎖操作的優化手段,經過研究發現,在大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,因此爲了消除數據在無競爭情況下鎖重入(CAS操作)的開銷而引入偏向鎖。對於沒有鎖競爭的場合,偏向鎖有很好的優化效果。

當JVM啓用了偏向鎖模式(jdk6默認開啓),新創建對象的Mark Word中的Thread Id爲0,說明此時處於可偏向但未偏向任何線程,也叫做匿名偏向狀態(anonymously biased)。

偏向鎖延遲偏向

偏向鎖模式存在偏向鎖延遲機制:HotSpot 虛擬機在啓動後有個 4s 的延遲纔會對每個新建的對象開啓偏向鎖模式。JVM啓動時會進行一系列的複雜活動,比如裝載配置,系統類初始化等等。在這個過程中會使用大量synchronized關鍵字對對象加鎖,且這些鎖大多數都不是偏向鎖。爲了減少初始化時間,JVM默認延時加載偏向鎖。

//關閉延遲開啓偏向鎖,這個值默認爲4秒
-XX:BiasedLockingStartupDelay=0
//禁止偏向鎖
-XX:-UseBiasedLocking 
//啓用偏向鎖
-XX:+UseBiasedLocking

驗證

@Slf4j
public class LockEscalationDemo{

    public static void main(String[] args) throws InterruptedException {
        log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
        Thread.sleep(4000);
        log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
    }
}

4s後偏向鎖爲可偏向或者匿名偏向狀態:

image

思考:如果鎖LockEscalationDemo.class會是什麼狀態?

偏向鎖狀態跟蹤
import lombok.extern.slf4j.Slf4j;
import org.openjdk.jol.info.ClassLayout;

/**
 * 測試 偏向鎖,輕量級鎖,重量級鎖標記變化
 * 關閉延遲開啓偏向鎖: -XX:BiasedLockingStartupDelay=0
 * 無鎖 001
 * 偏向鎖 101
 * 輕量級鎖 00
 * 重量級鎖 10
 */
@Slf4j
public class LockEscalationDemo2 {

    public static void main(String[] args) throws InterruptedException {
        log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
        //HotSpot 虛擬機在啓動後有個 4s 的延遲纔會對每個新建的對象開啓偏向鎖模式
        Thread.sleep(4000);
        Object obj = new Object();

        new Thread(new Runnable() {
            @Override
            public void run() {
                log.debug(Thread.currentThread().getName()+"開始執行。。。\n"
                        +ClassLayout.parseInstance(obj).toPrintable());
                synchronized (obj){
                    log.debug(Thread.currentThread().getName()+"獲取鎖執行中。。。\n"
                            +ClassLayout.parseInstance(obj).toPrintable());
                }
                log.debug(Thread.currentThread().getName()+"釋放鎖。。。\n"
                        +ClassLayout.parseInstance(obj).toPrintable());
            }
        },"thread1").start();

        Thread.sleep(5000);
        log.debug(ClassLayout.parseInstance(obj).toPrintable());
    }
}

image

思考:如果對象調用了hashCode,還會開啓偏向鎖模式嗎?

public static void main(String[] args) throws InterruptedException {
    log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
    //HotSpot 虛擬機在啓動後有個 4s 的延遲纔會對每個新建的對象開啓偏向鎖模式
    Thread.sleep(4000);
    Object obj = new Object();
    obj.hashCode();
    new Thread(new Runnable() {
        @Override
        public void run() {
            log.debug(Thread.currentThread().getName()+"開始執行。。。\n"
                    +ClassLayout.parseInstance(obj).toPrintable());
            synchronized (obj){
                log.debug(Thread.currentThread().getName()+"獲取鎖執行中。。。\n"
                        +ClassLayout.parseInstance(obj).toPrintable());
            }
            log.debug(Thread.currentThread().getName()+"釋放鎖。。。\n"
                    +ClassLayout.parseInstance(obj).toPrintable());
        }
    },"thread1").start();

    Thread.sleep(5000);
    log.debug(ClassLayout.parseInstance(obj).toPrintable());
}

結果如下

image

偏向鎖撤銷之調用對象HashCode

調用鎖對象的obj.hashCode()或System.identityHashCode(obj)方法會導致該對象的偏向鎖被撤銷。因爲對於一個對象,其HashCode只會生成一次並保存,偏向鎖是沒有地方保存hashcode的。

  • 輕量級鎖會在鎖記錄中記錄 hashCode
  • 重量級鎖會在 Monitor 中記錄 hashCode

當對象處於可偏向(也就是線程ID爲0)和已偏向的狀態下,調用HashCode計算將會使對象再也無法偏向:

  • 當對象可偏向時,MarkWord將變成未鎖定狀態,並只能升級成輕量鎖;
  • 當對象正處於偏向鎖時,調用HashCode將使偏向鎖強制升級成重量鎖。

代碼

public static void main(String[] args) throws InterruptedException {
    log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
    //HotSpot 虛擬機在啓動後有個 4s 的延遲纔會對每個新建的對象開啓偏向鎖模式
    Thread.sleep(4000);
    Object obj = new Object();
    new Thread(new Runnable() {
        @Override
        public void run() {
            log.debug(Thread.currentThread().getName()+"開始執行。。。\n"
                    +ClassLayout.parseInstance(obj).toPrintable());
            synchronized (obj){
                obj.hashCode();
                log.debug(Thread.currentThread().getName()+"獲取鎖執行中。。。\n"
                        +ClassLayout.parseInstance(obj).toPrintable());
            }
            log.debug(Thread.currentThread().getName()+"釋放鎖。。。\n"
                    +ClassLayout.parseInstance(obj).toPrintable());
        }
    },"thread1").start();

    Thread.sleep(5000);
    log.debug(ClassLayout.parseInstance(obj).toPrintable());
}

結果如下

image

重量鎖的釋放有一個過程

偏向鎖撤銷之調用wait/notify

偏向鎖狀態執行obj.notify() 會升級爲輕量級鎖

public static void main(String[] args) throws InterruptedException {
    log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
    //HotSpot 虛擬機在啓動後有個 4s 的延遲纔會對每個新建的對象開啓偏向鎖模式
    Thread.sleep(4000);
    Object obj = new Object();
    //obj.hashCode();

    new Thread(new Runnable() {
        @Override
        public void run() {
            log.debug(Thread.currentThread().getName()+"開始執行。。。\n"
                    +ClassLayout.parseInstance(obj).toPrintable());
            synchronized (obj){
                //obj.hashCode();
                obj.notify();
                /*try {
                    obj.wait(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }*/
                log.debug(Thread.currentThread().getName()+"獲取鎖執行中。。。\n"
                        +ClassLayout.parseInstance(obj).toPrintable());
            }
            log.debug(Thread.currentThread().getName()+"釋放鎖。。。\n"
                    +ClassLayout.parseInstance(obj).toPrintable());
        }
    },"thread1").start();


    log.debug(ClassLayout.parseInstance(obj).toPrintable());
}

結果如下

image

調用obj.wait(timeout) 會升級爲重量級鎖

public static void main(String[] args) throws InterruptedException {
    log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
    //HotSpot 虛擬機在啓動後有個 4s 的延遲纔會對每個新建的對象開啓偏向鎖模式
    Thread.sleep(4000);
    Object obj = new Object();
    //obj.hashCode();

    new Thread(new Runnable() {
        @Override
        public void run() {
            log.debug(Thread.currentThread().getName()+"開始執行。。。\n"
                    +ClassLayout.parseInstance(obj).toPrintable());
            synchronized (obj){
                //obj.hashCode();
                //obj.notify();
                try {
                    obj.wait(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug(Thread.currentThread().getName()+"獲取鎖執行中。。。\n"
                        +ClassLayout.parseInstance(obj).toPrintable());
            }
            log.debug(Thread.currentThread().getName()+"釋放鎖。。。\n"
                    +ClassLayout.parseInstance(obj).toPrintable());
        }
    },"thread1").start();


    log.debug(ClassLayout.parseInstance(obj).toPrintable());
}

結果

image

輕量級鎖

倘若偏向鎖失敗,虛擬機並不會立即升級爲重量級鎖,它還會嘗試使用一種稱爲輕量級鎖的優化手段,此時Mark Word 的結構也變爲輕量級鎖的結構。輕量級鎖所適應的場景是線程交替執行同步塊的場合,如果存在同一時間多個線程訪問同一把鎖的場合,就會導致輕量級鎖膨脹爲重量級鎖。

輕量級鎖跟蹤

public class LockEscalationDemo {
    public static void main(String[] args) throws InterruptedException {

        log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
        //HotSpot 虛擬機在啓動後有個 4s 的延遲纔會對每個新建的對象開啓偏向鎖模式
        Thread.sleep(4000);
        Object obj = new Object();
        // 思考: 如果對象調用了hashCode,還會開啓偏向鎖模式嗎
        obj.hashCode();
       //log.debug(ClassLayout.parseInstance(obj).toPrintable());

        new Thread(new Runnable() {
            @Override
            public void run() {
                log.debug(Thread.currentThread().getName()+"開始執行。。。\n"
                        +ClassLayout.parseInstance(obj).toPrintable());
                synchronized (obj){
                    log.debug(Thread.currentThread().getName()+"獲取鎖執行中。。。\n"
                            +ClassLayout.parseInstance(obj).toPrintable());
                }
                log.debug(Thread.currentThread().getName()+"釋放鎖。。。\n"
                        +ClassLayout.parseInstance(obj).toPrintable());
            }
        },"thread1").start();
        
        Thread.sleep(5000);
        log.debug(ClassLayout.parseInstance(obj).toPrintable());
   }

結果

image

測試:鎖升級場景

偏向鎖升級輕量級鎖

模擬兩個線程輕微競爭場景

@Slf4j
public class LockEscalationDemo {

    public static void main(String[] args) throws InterruptedException {
        log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
        //HotSpot 虛擬機在啓動後有個 4s 的延遲纔會對每個新建的對象開啓偏向鎖模式
        Thread.sleep(4000);
        Object obj = new Object();
        // 思考: 如果對象調用了hashCode,還會開啓偏向鎖模式嗎
        //obj.hashCode();
        //log.debug(ClassLayout.parseInstance(obj).toPrintable());

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                log.debug(Thread.currentThread().getName() + "開始執行。。。\n"
                        + ClassLayout.parseInstance(obj).toPrintable());
                synchronized (obj) {
                    // 思考:偏向鎖執行過程中,調用hashcode會發生什麼?
                    //obj.hashCode();
                    log.debug(Thread.currentThread().getName() + "獲取鎖執行中。。。\n"
                            + ClassLayout.parseInstance(obj).toPrintable());

                }
                log.debug(Thread.currentThread().getName() + "釋放鎖。。。\n"
                        + ClassLayout.parseInstance(obj).toPrintable());
            }
        }, "thread1");
        thread1.start();
        
        //控制線程競爭時機
        Thread.sleep(1);

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                log.debug(Thread.currentThread().getName()+"開始執行。。。\n"
                        +ClassLayout.parseInstance(obj).toPrintable());
                synchronized (obj){
                    log.debug(Thread.currentThread().getName()+"獲取鎖執行中。。。\n"
                            +ClassLayout.parseInstance(obj).toPrintable());
                }
                log.debug(Thread.currentThread().getName()+"釋放鎖。。。\n"
                        +ClassLayout.parseInstance(obj).toPrintable());
            }
        },"thread2");
        thread2.start();

        Thread.sleep(5000);
        log.debug(ClassLayout.parseInstance(obj).toPrintable());

    }

結果

image

輕量級鎖膨脹爲重量級鎖
@Slf4j
public class LockEscalationDemo {

    public static void main(String[] args) throws InterruptedException {

        log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
        //HotSpot 虛擬機在啓動後有個 4s 的延遲纔會對每個新建的對象開啓偏向鎖模式
        Thread.sleep(4000);
        Object obj = new Object();

        new Thread(new Runnable() {
            @Override
            public void run() {
                log.debug(Thread.currentThread().getName()+"開始執行。。。\n"
                        +ClassLayout.parseInstance(obj).toPrintable());
                synchronized (obj){
                    log.debug(Thread.currentThread().getName()+"獲取鎖執行中。。。\n"
                            +ClassLayout.parseInstance(obj).toPrintable());
                }
                log.debug(Thread.currentThread().getName()+"釋放鎖。。。\n"
                        +ClassLayout.parseInstance(obj).toPrintable());
            }
        },"thread1").start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                log.debug(Thread.currentThread().getName()+"開始執行。。。\n"
                        +ClassLayout.parseInstance(obj).toPrintable());
                synchronized (obj){
                    log.debug(Thread.currentThread().getName()+"獲取鎖執行中。。。\n"
                            +ClassLayout.parseInstance(obj).toPrintable());
                }
                log.debug(Thread.currentThread().getName()+"釋放鎖。。。\n"
                        +ClassLayout.parseInstance(obj).toPrintable());
            }
        },"thread2").start();

        Thread.sleep(5000);
        log.debug(ClassLayout.parseInstance(obj).toPrintable());
    }
}

結果

image

思考:重量級鎖釋放之後變爲無鎖,此時有新的線程來調用同步塊,會獲取什麼鎖?

總結:鎖對象狀態轉換

image

鎖升級的原理分析

Synchronized輕量級鎖源碼分析

image

Synchronized重量級鎖加鎖解鎖執行邏輯

image

synchronized鎖優化

偏向鎖批量重偏向&批量撤銷

從偏向鎖的加鎖解鎖過程中可看出,當只有一個線程反覆進入同步塊時,偏向鎖帶來的性能開銷基本可以忽略,但是當有其他線程嘗試獲得鎖時,就需要等到safe point時,再將偏向鎖撤銷爲無鎖狀態或升級爲輕量級,會消耗一定的性能,所以在多線程競爭頻繁的情況下,偏向鎖不僅不能提高性能,還會導致性能下降。於是,就有了批量重偏向與批量撤銷的機制。

原理

以class爲單位,爲每個class維護一個偏向鎖撤銷計數器,每一次該class的對象發生偏向撤銷操作時,該計數器+1,當這個值達到重偏向閾值(默認20)時,JVM就認爲該class的偏向鎖有問題,因此會進行批量重偏向。

每個class對象會有一個對應的epoch字段,每個處於偏向鎖狀態對象的Mark Word中也有該字段,其初始值爲創建該對象時class中的epoch的值。每次發生批量重偏向時,就將該值+1,同時遍歷JVM中所有線程的棧,找到該class所有正處於加鎖狀態的偏向鎖,將其epoch字段改爲新值。下次獲得鎖時,發現當前對象的epoch值和class的epoch不相等,那就算當前已經偏向了其他線程,也不會執行撤銷操作,而是直接通過CAS操作將其Mark Word的Thread Id 改成當前線程Id。

當達到重偏向閾值(默認20)後,假設該class計數器繼續增長,當其達到批量撤銷的閾值後(默認40),JVM就認爲該class的使用場景存在多線程競爭,會標記該class爲不可偏向,之後,對於該class的鎖,直接走輕量級鎖的邏輯。

應用場景

批量重偏向(bulk rebias)機制是爲了解決:一個線程創建了大量對象並執行了初始的同步操作,後來另一個線程也來將這些對象作爲鎖對象進行操作,這樣會導致大量的偏向鎖撤銷操作。

批量撤銷(bulk revoke)機制是爲了解決:在明顯多線程競爭劇烈的場景下使用偏向鎖是不合適的。

JVM的默認參數值

設置JVM參數-XX:+PrintFlagsFinal,在項目啓動時即可輸出JVM的默認參數值

intx BiasedLockingBulkRebiasThreshold   = 20   //默認偏向鎖批量重偏向閾值

我們可以通過-XX:BiasedLockingBulkRebiasThreshold 和 -XX:BiasedLockingBulkRevokeThreshold 來手動設置閾值

測試:批量重偏向

當撤銷偏向鎖閾值超過 20 次後,jvm 會這樣覺得,我是不是偏向錯了,於是會在給這些對象加鎖時重新偏向至加鎖線程,重偏向會重置對象 的 Thread ID

@Slf4j
public class BiasedLockingTest {
    //延時產生可偏向對象
    Thread.sleep(5000);
    // 創建一個list,來存放鎖對象
    List<Object> list = new ArrayList<>();
    
    // 線程1
    new Thread(() -> {
        for (int i = 0; i < 50; i++) {
            // 新建鎖對象
            Object lock = new Object();
            synchronized (lock) {
                list.add(lock);
            }
        }
        try {
            //爲了防止JVM線程複用,在創建完對象後,保持線程thead1狀態爲存活
            Thread.sleep(100000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }, "thead1").start();
    
    //睡眠3s鍾保證線程thead1創建對象完成
    Thread.sleep(3000);
    log.debug("打印thead1,list中第20個對象的對象頭:");
    log.debug((ClassLayout.parseInstance(list.get(19)).toPrintable()));
    
    // 線程2
    new Thread(() -> {
        for (int i = 0; i < 40; i++) {
            Object obj = list.get(i);
            synchronized (obj) {
                if(i>=15&&i<=21||i>=38){
                    log.debug("thread2-第" + (i + 1) + "次加鎖執行中\t"+
                            ClassLayout.parseInstance(obj).toPrintable());
                }
            }
            if(i==17||i==19){
                log.debug("thread2-第" + (i + 1) + "次釋放鎖\t"+
                        ClassLayout.parseInstance(obj).toPrintable());
            }
        }
        try {
            Thread.sleep(100000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }, "thead2").start();

    LockSupport.park();
    }

測試結果:

thread1: 創建50個偏向線程thread1的偏向鎖 1-50 偏向鎖

image

thread2:

1-18 偏向鎖撤銷,升級爲輕量級鎖 (thread1釋放鎖之後爲偏向鎖狀態)

19-40 偏向鎖撤銷達到閾值(20),執行了批量重偏向 (測試結果在第19就開始批量重偏向了)

image

測試:批量撤銷

當撤銷偏向鎖閾值超過 40 次後,jvm 會認爲不該偏向,於是整個類的所有對象都會變爲不可偏向的,新建的對象也是不可偏向的。

注意:時間-XX:BiasedLockingDecayTime=25000ms範圍內沒有達到40次,撤銷次數清爲0,重新計時

@Slf4j
public class BiasedLockingTest {
    public static void main(String[] args) throws  InterruptedException {
        //延時產生可偏向對象
        Thread.sleep(5000);
        // 創建一個list,來存放鎖對象
        List<Object> list = new ArrayList<>();
        
        // 線程1
        new Thread(() -> {
            for (int i = 0; i < 50; i++) {
                // 新建鎖對象
                Object lock = new Object();
                synchronized (lock) {
                    list.add(lock);
                }
            }
            try {
                //爲了防止JVM線程複用,在創建完對象後,保持線程thead1狀態爲存活
                Thread.sleep(100000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "thead1").start();

        //睡眠3s鍾保證線程thead1創建對象完成
        Thread.sleep(3000);
        log.debug("打印thead1,list中第20個對象的對象頭:");
        log.debug((ClassLayout.parseInstance(list.get(19)).toPrintable()));
        
        // 線程2
        new Thread(() -> {
            for (int i = 0; i < 40; i++) {
                Object obj = list.get(i);
                synchronized (obj) {
                    if(i>=15&&i<=21||i>=38){
                        log.debug("thread2-第" + (i + 1) + "次加鎖執行中\t"+
                                ClassLayout.parseInstance(obj).toPrintable());
                    }
                }
                if(i==17||i==19){
                    log.debug("thread2-第" + (i + 1) + "次釋放鎖\t"+
                            ClassLayout.parseInstance(obj).toPrintable());
                }
            }
            try {
                Thread.sleep(100000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "thead2").start();


        Thread.sleep(3000);

        new Thread(() -> {
            for (int i = 0; i < 50; i++) {
                Object lock =list.get(i);
                if(i>=17&&i<=21||i>=35&&i<=41){
                    log.debug("thread3-第" + (i + 1) + "次準備加鎖\t"+
                            ClassLayout.parseInstance(lock).toPrintable());
                }
                synchronized (lock){
                    if(i>=17&&i<=21||i>=35&&i<=41){
                        log.debug("thread3-第" + (i + 1) + "次加鎖執行中\t"+
                                ClassLayout.parseInstance(lock).toPrintable());
                    }
                }
            }
        },"thread3").start();

        Thread.sleep(3000);
        log.debug("查看新創建的對象");
        log.debug((ClassLayout.parseInstance(new Object()).toPrintable()));

        LockSupport.park();
    }

測試結果:

thread3:

1-18 從無鎖狀態直接獲取輕量級鎖 (thread2釋放鎖之後變爲無鎖狀態)

image

19-40 偏向鎖撤銷,升級爲輕量級鎖 (thread2釋放鎖之後爲偏向鎖狀態)

image

41-50 達到偏向鎖撤銷的閾值40,批量撤銷偏向鎖,升級爲輕量級鎖 (thread1釋放鎖之後爲偏向鎖狀態)

image

新創建的對象: 無鎖狀態

image

總結

  1. 批量重偏向和批量撤銷是針對類的優化,和對象無關。
  2. 偏向鎖重偏向一次之後不可再次重偏向。
  3. 當某個類已經觸發批量撤銷機制後,JVM會默認當前類產生了嚴重的問題,剝奪了該類的新實例對象使用偏向鎖的權利

自旋優化

重量級鎖競爭的時候,還可以使用自旋來進行優化,如果當前線程自旋成功(即這時候持鎖線程已經退出了同步塊,釋放了鎖),這時當前線程就可以避免阻塞。

  • 自旋會佔用 CPU 時間,單核 CPU 自旋就是浪費,多核 CPU 自旋才能發揮優勢。
  • 在 Java 6 之後自旋是自適應的,比如對象剛剛的一次自旋操作成功過,那麼認爲這次自旋成功的可能性會高,就多自旋幾次;反之,就少自旋甚至不自旋,比較智能。
  • Java 7 之後不能控制是否開啓自旋功能

注意:自旋的目的是爲了減少線程掛起的次數,儘量避免直接掛起線程(掛起操作涉及系統調用,存在用戶態和內核態切換,這纔是重量級鎖最大的開銷)

鎖粗化

假設一系列的連續操作都會對同一個對象反覆加鎖及解鎖,甚至加鎖操作是出現在循環體中的,即使沒有出現線程競爭,頻繁地進行互斥同步操作也會導致不必要的性能損耗。如果JVM檢測到有一連串零碎的操作都是對同一對象的加鎖,將會擴大加鎖同步的範圍(即鎖粗化)到整個操作序列的外部。

StringBuffer buffer = new StringBuffer();
/**
 * 鎖粗化
 */
public void append(){
    buffer.append("aaa").append(" bbb").append(" ccc");

上述代碼每次調用 buffer.append 方法都需要加鎖和解鎖,如果JVM檢測到有一連串的對同一個對象加鎖和解鎖的操作,就會將其合併成一次範圍更大的加鎖和解鎖操作,即在第一次append方法時進行加鎖,最後一次append方法結束後進行解鎖。

鎖消除

鎖消除即刪除不必要的加鎖操作。鎖消除是Java虛擬機在JIT編譯期間,通過對運行上下文的掃描,去除不可能存在共享資源競爭的鎖,通過鎖消除,可以節省毫無意義的請求鎖時間。

public class LockEliminationTest {
    /**
     * 鎖消除
     * -XX:+EliminateLocks 開啓鎖消除(jdk8默認開啓)
     * -XX:-EliminateLocks 關閉鎖消除
     * @param str1
     * @param str2
     */
    public void append(String str1, String str2) {
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append(str1).append(str2);
    }

    public static void main(String[] args) throws InterruptedException {
        LockEliminationTest demo = new LockEliminationTest();
        long start = System.currentTimeMillis();
        for (int i = 0; i < 100000000; i++) {
            demo.append("aaa", "bbb");
        }
        long end = System.currentTimeMillis();
        System.out.println("執行時間:" + (end - start) + " ms");
    }

StringBuffer的append是個同步方法,但是append方法中的 StringBuffer 屬於一個局部變量,不可能從該方法中逃逸出去,因此其實這過程是線程安全的,可以將鎖消除。

測試結果: 關閉鎖消除執行時間4688 ms 開啓鎖消除執行時間:2601 ms

逃逸分析(Escape Analysis)

逃逸分析,是一種可以有效減少Java 程序中同步負載和內存堆分配壓力的跨函數全局數據流分析算法。通過逃逸分析,Java Hotspot編譯器能夠分析出一個新的對象的引用的使用範圍從而決定是否要將這個對象分配到堆上。逃逸分析的基本行爲就是分析對象動態作用域。

方法逃逸(對象逃出當前方法)

當一個對象在方法中被定義後,它可能被外部方法所引用,例如作爲調用參數傳遞到其他地方中。

線程逃逸((對象逃出當前線程)

這個對象甚至可能被其它線程訪問到,例如賦值給類變量或可以在其它線程中訪問的實例變量。

使用逃逸分析,編譯器可以對代碼做如下優化:

1.同步省略或鎖消除(Synchronization Elimination)。如果一個對象被發現只能從一個線程被訪問到,那麼對於這個對象的操作可以不考慮同步。

2.將堆分配轉化爲棧分配(Stack Allocation)。如果一個對象在子程序中被分配,要使指向該對象的指針永遠不會逃逸,對象可能是棧分配的候選,而不是堆分配。

3.分離對象或標量替換(Scalar Replacement)。有的對象可能不需要作爲一個連續的內存結構存在也可以被訪問到,那麼對象的部分(或全部)可以不存儲在內存,而是存儲在CPU寄存器中。

jdk6纔開始引入該技術,jdk7開始默認開啓逃逸分析。在Java代碼運行時,可以通過JVM參數指定是否開啓逃逸分析:

-XX:+DoEscapeAnalysis  //表示開啓逃逸分析 (jdk1.8默認開啓)
-XX:-DoEscapeAnalysis //表示關閉逃逸分析。
-XX:+EliminateAllocations   //開啓標量替換(默認打開)

測試

/**
 * @author  Fox
 *
 * 進行兩種測試
 * 關閉逃逸分析,同時調大堆空間,避免堆內GC的發生,如果有GC信息將會被打印出來
 * VM運行參數:-Xmx4G -Xms4G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
 *
 * 開啓逃逸分析  jdk8默認開啓
 * VM運行參數:-Xmx4G -Xms4G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails -XX:+HeapDumpOnOutOfMemoryError
 *
 * 執行main方法後
 * jps 查看進程
 * jmap -histo 進程ID
 *
 */
@Slf4j
public class EscapeTest {

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 500000; i++) {
            alloc();
        }

        long end = System.currentTimeMillis();

        log.info("執行時間:" + (end - start) + " ms");
        try {
            Thread.sleep(Integer.MAX_VALUE);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
    }

    /**
     * JIT編譯時會對代碼進行逃逸分析
     * 並不是所有對象存放在堆區,有的一部分存在線程棧空間
     * Ponit沒有逃逸
     */
    private static String alloc() {
        Point point = new Point();
        return point.toString();
    }

    /**
     *同步省略(鎖消除)  JIT編譯階段優化,JIT經過逃逸分析之後發現無線程安全問題,就會做鎖消除
     */
    public void append(String str1, String str2) {
        StringBuffer stringBuffer = new StringBuffer();
        stringBuffer.append(str1).append(str2);
    }

    /**
     * 標量替換
     *
     */
    private static void test2() {
        Point point = new Point(1,2);
        System.out.println("point.x="+point.getX()+"; point.y="+point.getY());

//        int x=1;
//        int y=2;
//        System.out.println("point.x="+x+"; point.y="+y);
    }
}

@Data
@AllArgsConstructor
@NoArgsConstructor
class Point{
    private int x;
    private int y;
}

測試結果:開啓逃逸分析,部分對象會在棧上分配

image

關閉逃逸分析

image

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章