【多線程與高併發原理篇:4_深入理解synchronized】

1. 前言

越是簡單的東西,在深入瞭解後發現越複雜。想起了曾在初中階段,語文老師給我們解說《論語》的道理,順便給我們提了一句,說老子的無爲思想比較消極,學生時代不要太關注。現在有了一定的生活閱歷,再來看老子的《道德經》,發現那纔是大智慧,《論語》屬於儒家是講人與人的關係,《道德經》屬於道家講人與自然的關係,效法天之道來行人之道,儒家講入世,仁義禮智信,道家講出世,無爲而無不爲。老子把道比作水、玄牝(女性的生殖器)、嬰兒、山谷等,高深莫測,卻滋養萬物,源源不斷的化生萬物並滋養之,而不居功,故能天長地久。儒家教我們做聖人,道家教我們修成仙,顯然境界更高。

synchronized的設計思想就像道家的思想一樣,看着用起來很簡單,但是底層極其複雜,好像永遠看不透一樣。一直想深入寫一篇synchronized的文章,卻一直不敢動手,直到最近讀了幾遍hotspot源碼後,纔有勇氣寫一些自己的理解。下文就從幾個層面逐步深入,談談對synchronized的理解,總結精華思想並用到我們自己的項目設計中。

2. 從頂層到底層

首先通過一張圖來概覽synchronized在各層面的實現細節與原理,並在下面的章節逐一分析

2.1 應用層

臨界區與臨界資源

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

@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 < 100; i++) {
                increment();
            }
        }, "t1");

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

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

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

    }
}

以上的結果可能是正數、負數、零。爲什麼呢?因爲 Java 中對靜態變量的自增、自減並不是原子操作,個線程執行過程中會被其他線程打斷。

競態條件
多個線程在臨界區內執行,由於代碼的執行序列不同而導致結果無法預測,稱之爲發生了競態條件。爲了避免臨界區的競態條件發生,有多種手段可以達到目的:

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

synchronized 同步塊是 Java 提供的一種原子性內置鎖,Java 中的每個對象都可以把它當作一個同步鎖來使用,這些 Java 內置的使用者看不到的鎖被稱爲內置鎖,也叫作監視器鎖,目的就是保證多個線程在進入synchronized代碼段或者方法時,將並行變串行。

使用層面

解決之前的共享問題,只需要在兩個方法前面加上synchronized,就可以得到期望爲零的結果。

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

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

2.2 字節碼層面

通過IDEA自帶的工具view->show bytecode with jclasslib,查看到訪問標誌0x0029

通過查詢java 字節碼手冊,synchronized對應的字節碼爲ACC_SYNCHRONIZED,0x0020,三個修飾符加起來正好是0x0029。

如果加在代碼塊或者對象上, 對應的字節碼是monitorentermonitorexit

2.3 java虛擬機規範層面

同步方法
The Java® Virtual Machine Specification針對同步方法的說明

Method-level synchronization is performed implicitly, as part of method invocation and return. A synchronized method is distinguished in the run-time constant pool’s method_info structure by the ACC_SYNCHRONIZED flag, which is checked by the method invocation instructions. When invoking a method for which ACC_SYNCHRONIZED is set, the executing thread enters a monitor, invokes the method itself, and exits the monitor whether the method invocation completes normally or abruptly. During the time the executing thread owns the monitor, no other thread may enter it. If an exception is thrown during invocation of the synchronized method and the synchronized method does not handle the exception, the monitor for the method is automatically exited before the exception is rethrown out of the synchronized method.

這段話適合好好讀讀,大致含義如下:

同步方法是隱式的。一個同步方法會在運行時常量池中的method_info結構體中存放ACC_SYNCHRONIZED標識符。當一個線程訪問方法時,會去檢查是否存在ACC_SYNCHRONIZED標識,如果存在,則先要獲得對應的monitor鎖,然後執行方法。當方法執行結束(不管是正常return還是拋出異常)都會釋放對應的monitor鎖。如果此時有其他線程也想要訪問這個方法時,會因得不到monitor鎖而阻塞。當同步方法中拋出異常且方法內沒有捕獲,則在向外拋出時會先釋放已獲得的monitor鎖。

同步代碼塊

同步代碼塊使用monitorenter和monitorexit兩個指令實現同步, The Java® Virtual Machine Specification中有關於這兩個指令的介紹:

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:
If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.
If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.
If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.

大致含義如下:

每個對象都會與一個monitor相關聯,當某個monitor被擁有之後就會被鎖住,當線程執行到monitorenter指令時,就會去嘗試獲得對應的monitor。步驟如下:
1.每個monitor維護着一個記錄着擁有次數的計數器。未被擁有的monitor的該計數器爲0,當一個線程獲得monitor(執行monitorenter)後,該計數器自增變爲 1 。

  • 當同一個線程再次獲得該monitor的時候,計數器再次自增;
  • 當不同線程想要獲得該monitor的時候,就會被阻塞。

2.當同一個線程釋放 monitor(執行monitorexit指令)的時候,計數器再自減。當計數器爲0的時候。monitor將被釋放,其他線程便可以獲得monitor。

2.4 操作系統層面

Monitor(管程/監視器)
monitor又稱監視器,操作系統層面爲管程,管程是指管理共享變量以及對共享變量操作的過程,讓它們支持併發,所以管程是操作系統層面的同步機制。

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

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

synchronized關鍵字和wait()、notify()、notifyAll()這三個方法是Java中實現管程技術的組成部分,設計思想也是來源與操作系統的管程。在應用層面有個經典的等待通知範式

等待方
等待方遵循如下的原則:

  • 獲取對象的鎖
  • 如果條件不滿足,那麼調用對象的wait方法,被通知後仍然需要檢查條件
  • 條件滿足則繼續執行對應的邏輯
synchronized( 對象 ) {
    while( 條件不滿足 ) {
        對象.wait();
    }
    對應的處理邏輯
}

通知方
通知方遵循如下的原則:

  • 獲得對象的鎖
  • 改變條件
  • 通知所有等待在對象上的線程
synchronized( 對象 ) {
    改變條件;
    對象.notifyAll();
}

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

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

內核態
jdk1.5之前,synchronized是重量級鎖,會直接由操作系統內核態控制,jdk1.6以後,對synchronized做了大量的優化,如鎖粗化(Lock Coarsening)、鎖消除(Lock Elimination)、輕量級鎖(Lightweight Locking)、偏向鎖(Biased Locking)、自適應自旋(Adaptive Spinning)等技術來減少鎖操作的開銷,內置鎖的併發性能已經基本與Lock持平,只有升級到重量級鎖後纔會有大的開銷。

等待通知範式案例
用上面講的等待通知範式,實現一個數據庫連接池;連接池有獲取連接,釋放連接兩個方法,DBPool模擬一個容器放連接,初始化20個連接,獲取連接的線程得到連接池爲空時,阻塞等待,喚起釋放連接的線程;

import java.sql.Connection;
import java.util.LinkedList;

/**
 *類說明:連接池的實現
 */
public class DBPool {

    /*容器,存放連接*/
    private static LinkedList<Connection> pool = new LinkedList<Connection>();

    /*限制了池的大小=20*/
    public DBPool(int initialSize) {
        if (initialSize > 0) {
            for (int i = 0; i < initialSize; i++) {
                pool.addLast(SqlConnectImpl.fetchConnection());
            }
        }
    }

    /*釋放連接,通知其他的等待連接的線程*/
    public void releaseConnection(Connection connection) {
        if (connection != null) {
            synchronized (pool){
                pool.addLast(connection);
                //通知其他等待連接的線程
                pool.notifyAll();
            }
        }
    }

    /*獲取*/
    // 在mills內無法獲取到連接,將會返回null 1S
    public Connection fetchConnection(long mills)
            throws InterruptedException {
        synchronized (pool){
            //永不超時
            if(mills<=0){
                while(pool.isEmpty()){
                    pool.wait();
                }
                return pool.removeFirst();
            }else{
                /*超時時刻*/
                long future = System.currentTimeMillis()+mills;
                /*等待時長*/
                long remaining = mills;
                while(pool.isEmpty()&&remaining>0){
                    pool.wait(remaining);
                    /*喚醒一次,重新計算等待時長*/
                    remaining = future-System.currentTimeMillis();
                }
                Connection connection = null;
                if(!pool.isEmpty()){
                    connection = pool.removeFirst();
                }
                return connection;
            }
        }

    }
}
import java.sql.*;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.Executor;

/**
 *類說明:
 */
public class SqlConnectImpl implements Connection{
	
    /*拿一個數據庫連接*/
    public static final Connection fetchConnection(){
        return new SqlConnectImpl();
    }

    @Override
    public void commit() throws SQLException {
	SleepTools.ms(70);
    }
......
}

測試類:總共50個線程,每個線程嘗試20次

import java.sql.Connection;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;

/**
 *類說明:
 */
public class DBPoolTest {
    static DBPool pool  = new DBPool(10);
    // 控制器:控制main線程將會等待所有Woker結束後才能繼續執行
    static CountDownLatch end;

    public static void main(String[] args) throws Exception {
    	// 線程數量
        int threadCount = 50;
        end = new CountDownLatch(threadCount);
        int count = 20;//每個線程的操作次數
        AtomicInteger got = new AtomicInteger();//計數器:統計可以拿到連接的線程
        AtomicInteger notGot = new AtomicInteger();//計數器:統計沒有拿到連接的線程
        for (int i = 0; i < threadCount; i++) {
            Thread thread = new Thread(new Worker(count, got, notGot), 
            		"worker_"+i);
            thread.start();
        }
        end.await();// main線程在此處等待
        System.out.println("總共嘗試了: " + (threadCount * count));
        System.out.println("拿到連接的次數:  " + got);
        System.out.println("沒能連接的次數: " + notGot);
    }

    static class Worker implements Runnable {
        int           count;
        AtomicInteger got;
        AtomicInteger notGot;

        public Worker(int count, AtomicInteger got,
                               AtomicInteger notGot) {
            this.count = count;
            this.got = got;
            this.notGot = notGot;
        }

        public void run() {
            while (count > 0) {
                try {
                    // 從線程池中獲取連接,如果1000ms內無法獲取到,將會返回null
                    // 分別統計連接獲取的數量got和未獲取到的數量notGot
                    Connection connection = pool.fetchConnection(1000);
                    if (connection != null) {
                        try {
                            connection.createStatement();
//                            PreparedStatement preparedStatement
//                                    = connection.prepareStatement("");
//                            preparedStatement.execute();
                            connection.commit();
                        } finally {
                            pool.releaseConnection(connection);
                            got.incrementAndGet();
                        }
                    } else {
                        notGot.incrementAndGet();
                        System.out.println(Thread.currentThread().getName()
                        		+"等待超時!");
                    }
                } catch (Exception ex) {
                } finally {
                    count--;
                }
            }
            end.countDown();
        }
    }
}

結果:

2.5 hotspot實現層面

hotspot源碼ObjectMonitor.hpp定義了ObjectMonitor的數據結構

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;
  }

大致執行過程如下如所示:

大量線程進入監視器的等待隊列EntryList,只有通過CAS拿到鎖的線程,把進入鎖的標識變量_recursions置爲1,如果方法是遞歸循環調用,支持鎖重入,_recursions可以累加,並將_owner置爲獲取鎖的線程ID,調用wait方法後,釋放鎖,再重新進入等待隊列EntryList。

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

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

對象頭詳解
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字節,不開啓壓縮數組佔8字節,因爲數組本質上也是指針。

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

<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
        <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.9</version>
            <scope>provided</scope>
        </dependency>

使用方法:

//查看對象內部信息
System.out.println(ClassLayout.parseInstance(obj).toPrintable());

測試

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

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

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

關閉指針壓縮後,對象頭爲16字節:­XX:­UseCompressedOops

Mark Word的結構
Hotspot通過markOop類型實現Mark Word,具體實現位於markOop.hpp文件中。MarkWord 結構搞得這麼複雜,是因爲需要節省內存,讓同一個內存區域在不
同階段有不同的用處。

// Bit-format of an object header (most significant first, big endian layout below):
//
//  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
//                                               not valid at any other time
  • hash: 保存對象的哈希碼。運行期間調用System.identityHashCode()來計算,延遲計算,並把結果賦值到這裏。
  • age: 保存對象的分代年齡。表示對象被GC的次數,當該次數到達閾值的時候,對象就會轉移到老年代。
  • biased_lock: 偏向鎖標識位。由於無鎖和偏向鎖的鎖標識都是 01,沒辦法區分,這裏引入一位的偏向鎖標識位。
  • lock: 鎖狀態標識位。區分鎖狀態,比如11時表示對象待GC回收狀態, 只有最後2位鎖標識有效。
  • JavaThread*: 保存持有偏向鎖的線程ID。偏向模式的時候,當某個線程持有對象的時候,對象這裏就會被置爲該線程的ID。 在後面的操作中,就無需再進行嘗試獲取鎖的動作。這個線程ID並不是JVM分配的線程ID號,和Java Thread中的ID是兩個概念。
  • epoch: 用來計算偏向鎖的批量撤銷與批量重偏向。

32位JVM下的對象結構描述

64位JVM下的對象結構描述

  • 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 偏向鎖
};

更直觀的理解方式

偏向鎖
在大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,對於沒有鎖競爭的場合,偏向鎖有很好的優化效果。

/***StringBuffer內部同步***/
 public synchronized int length() {
  return count;
 }

 //System.out.println 無意識的使用鎖
 public void println(String x) {
  synchronized (this) {
  print(x); newLine();
  }
 }

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

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

//關閉延遲開啓偏向鎖
‐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(5000);
  log.info("===============延遲5秒後=================");
  log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
 }
}


5s後偏向鎖爲可偏向或者匿名偏向狀態,此時ThreadId=0;

偏向鎖狀態跟蹤

@Slf4j
public class LockEscalationDemo{
    private final static Logger log = LoggerFactory.getLogger(SyncDemo1.class);

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

        log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
        //HotSpot 虛擬機在啓動後有個 4s 的延遲纔會對每個新建的對象開啓偏向鎖模式
        Thread.sleep(5000);
        log.info("===============延遲5秒後=================");
        Object obj = new Object();
        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();
    }
}


上圖可以看出,只有一個線程時,從匿名偏向到偏向鎖,並在偏向鎖後面帶上了線程id。

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


偏向鎖撤銷之調用對象HashCode
匿名偏向後,調用對象HashCode,導致偏向鎖撤銷。因爲對於一個對象,其HashCode只會生成一次並保存,偏向鎖是沒有地方保存hashcode的。

  • 輕量級鎖會在鎖記錄中記錄 hashCode
  • 重量級鎖會在 Monitor 中記錄 hashCode
    當對象處於匿名偏向(也就是線程ID爲0)和已偏向的狀態下,調用HashCode計算將會使對象再也無法偏向。
  • 當對象匿名偏向,MarkWord將變成未鎖定狀態,並只能升級成輕量鎖;
  • 當對象正處於偏向鎖時,調用HashCode將使偏向鎖強制升級成重量鎖。
    如下圖


偏向鎖撤銷之調用wait/notify
偏向鎖狀態執行obj.notify() 會升級爲輕量級鎖,調用obj.wait(timeout) 會升級爲重量級鎖

synchronized (obj) {
  obj.notify();
  log.debug(Thread.currentThread().getName() + "獲取鎖執行中。。。\n"
  + ClassLayout.parseInstance(obj).toPrintable());
 }

synchronized (obj) {
 
  try {
   obj.wait(100);
  } catch (InterruptedException e) {
  e.printStackTrace();
  }

  log.debug(Thread.currentThread().getName() + "獲取鎖執行中。。。\n"
  + ClassLayout.parseInstance(obj).toPrintable());
 }

輕量級鎖
倘若偏向鎖失敗,虛擬機並不會立即升級爲重量級鎖,它還會嘗試使用一種稱爲輕量級鎖的優化手段,此時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());
  }
 }

偏向鎖升級輕量級鎖
模擬兩個線程輕度競爭場景

@Slf4j
public class LockEscalationDemo{
    private final static Logger log = LoggerFactory.getLogger(SyncDemo1.class);

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

        log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
        //HotSpot 虛擬機在啓動後有個 4s 的延遲纔會對每個新建的對象開啓偏向鎖模式
        Thread.sleep(5000);
        log.info("===============延遲5秒後=================");
        Object obj = new Object();
        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(1);

        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();
    }
}


輕量級鎖膨脹爲重量級鎖

@Slf4j
public class LockEscalationDemo{
    private final static Logger log = LoggerFactory.getLogger(SyncDemo1.class);

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

        log.debug(ClassLayout.parseInstance(new Object()).toPrintable());
        //HotSpot 虛擬機在啓動後有個 4s 的延遲纔會對每個新建的對象開啓偏向鎖模式
        Thread.sleep(5000);
        log.info("===============延遲5秒後=================");
        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){
                    // 思考:偏向鎖執行過程中,調用hashcode會發生什麼?
//                    obj.hashCode();
//                    obj.notify();
//                    try {
//                        obj.wait(50);
//                    } 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();

        //控制線程競爭時機
//        Thread.sleep(1);

        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();
  }
}

將上述控制線程競爭時機的代碼注掉,讓線程2與線程1發生競爭,線程2就由原來的偏向鎖升級到重量級鎖。

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

通過實驗可以得出,後面的線程會獲得輕量級鎖,相當於線程競爭不激烈,多個線程通過CAS就能輪流獲取鎖,並且釋放。

思考2:爲什麼有輕量級鎖還需要重量級鎖?

因爲輕量級鎖時通過CAS自旋的方式獲取鎖,該方式消耗CPU資源的,如果鎖的時間長,或者自旋線程多,CPU會被大量消耗;而重量級鎖有等待隊列,所有拿不到鎖的進入等待隊列,不需要消耗CPU資源。

思考3:偏向鎖是否一定比輕量級鎖效率高嗎?

不一定,在明確知道會有多線程競爭的情況下,偏向鎖肯定會涉及鎖撤銷,需要暫停線程,回到安全點,並檢查線程釋放活着,故撤銷需要消耗性能,這時候直接使用輕量級鎖。
JVM啓動過程,會有很多線程競,所以默認情況啓動時不打開偏向鎖,過一段兒時間再打開。

鎖升級的狀態圖

無鎖是鎖升級前的一箇中間態,必須要恢復到無鎖才能進行升級,因爲需要有拷貝mark word的過程,並且修改指針。

鎖記錄的重入
輕量級鎖在拷貝mark word到線程棧Lock Record中時,如果有重入鎖,則在線程棧中繼續壓棧Lock Record記錄,只不過mark word的值爲空,等到解鎖後,依次彈出,最終將mard word恢復到對象頭中,如圖所示

鎖升級的具體細節會稍後結合hotspot源碼進行。

2.6 彙編層

上面談到的鎖升級,一直提到了一個CAS操作,CAS是英文單詞CompareAndSwap的縮寫,中文意思是:比較並替換。CAS需要有3個操作數:內存地址V,舊的預期值A,即將要更新的目標值B。

CAS指令執行時,當且僅當內存地址V的值與預期值A相等時,將內存地址V的值修改爲B,否則就什麼都不做。整個比較並替換的操作是一個原子操作。

這個方法是java native方法,在unsafe類中,

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

需要打開hotspot的unsafe.cpp

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

可以看到調用了Atomic::cmpxchg方法,Atomic::cmpxchg方法引入了彙編指令,

inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  int mp = os::is_MP();
  __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
                    : "=a" (exchange_value)
                    : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                    : "cc", "memory");
  return exchange_value;
}
  • mp是os::is_MP()的返回結果,os::is_MP()是一個內聯函數,用來判斷當前系統是否爲多處理器。如果當前系統是多處理器,該函數返回1。否則,返回0。

  • __asm__代表是彙編開始,volatile代表,禁止CPU指令重排,並且讓值修改後,立馬被其他CPU可見,保持數據一致性。

  • LOCK_IF_MP(mp)會根據mp的值來決定是否爲cmpxchg指令添加lock前綴。如果通過mp判斷當前系統是多處理器(即mp值爲1),則爲cmpxchg指令添加lock前綴。否則,不加lock前綴。

內嵌彙編模板

 asm ( assembler template

      : output operands               (optional)

      : input operands                (optional)

      : list of clobbered registers   
        (optional)   
);

這裏涉及操作內存,與CPU的寄存器,大致意思就是在先判斷CPU是否多核,如果是多核,禁止CPU級別的指令重排,並且通過Lock前綴指令,鎖住並進行比較與交換,然後把最新的值同步到內存,其他CPU從內存加載數據取最新的值。

2.7 CPU指令層

上面LOCK_IF_MP在atomic_linux_x86.inline.hpp有詳細宏定義

#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "

很明顯,帶了一個lock前綴的指令,lock 和cmpxchgl是CPU指令,lock指令是個前綴,可以修飾其他指令,cmpxchgl即爲CAS指令,查閱英特爾操作手冊,在Intel® 64 and IA-32 Architectures Software Developer’s Manual 中的章節LOCK—Assert LOCK# Signal Prefix 中給出LOCK指令的詳細解釋

就是排他的使用共享內存。這裏一般有兩種方式,鎖總線與鎖cpu的緩存行

  • 鎖總線
    LOCK#信號就是我們經常說到的總線鎖,處理器使用LOCK#信號達到鎖定總線,來解決原子性問題,當一個處理器往總線上輸出LOCK#信號時,其它處理器的請求將被阻塞,此時該處理器此時獨佔共享內存;總線鎖這種做法鎖定的範圍太大了,導致CPU利用率急劇下降,因爲使用LOCK#是把CPU和內存之間的通信鎖住了,這使得鎖定時期間,其它處理器不能操作其內存地址的數據 ,所以總線鎖的開銷比較大。
  • 鎖緩存行
    如果訪問的內存區域已經緩存在處理器的緩存行中,P6系統和之後系列的處理器則不會聲明LOCK#信號,它會對CPU的緩存中的緩存行進行鎖定,在鎖定期間,其它 CPU 不能同時緩存此數據,在修改之後,通過緩存一致性協議(在Intel CPU中,則體現爲MESI協議)來保證修改的原子性,這個操作被稱爲緩存鎖

lock指令會產生總線鎖也可能會產生緩存鎖,看具體的條件,有下面幾種情況只能使用總線鎖

  • 當操作的數據不能被緩存在處理器內部,這個必須得使用總線鎖了
  • 操作的數據跨多個緩存行(cache line),緩存鎖的前置條件是多個數據在一個緩存行裏面
  • 有些處理器不支持緩存鎖定。對於Intel 486和奔騰處理器,就算鎖定的內存區域在處理器的緩存行中也會調用總線鎖定。

無論是總線鎖還是緩存鎖這都是CPU在硬件層面上提供的鎖,肯定效率比軟件層面的鎖要高。

3. 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   //默認偏向鎖批量重偏向閾值
intx BiasedLockingBulkRevokeThreshold  = 40   //默認偏向鎖批量撤銷閾值

我們可以通過-XX:BiasedLockingBulkRebiasThreshold 和 -XX:BiasedLockingBulkRevokeThreshold 來手動設置閾值
測試:批量重偏向

@Slf4j
public class BiasedLockingTest {
    private final static Logger log = LoggerFactory.getLogger(BiasedLockingTest.class);
    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();

    }
}

當撤銷偏向鎖閾值超過 20 次後,jvm 會這樣覺得,我是不是偏向錯了,於是會在給這些對象加鎖時重新偏向至加鎖線程,重偏向會重置象 的 Thread ID
測試結果:
thread1:  創建50個偏向線程thread1的偏向鎖 1-50 偏向鎖

thread2:
1-18 偏向鎖撤銷,升級爲輕量級鎖  (thread1釋放鎖之後爲偏向鎖狀態)
19-40 偏向鎖撤銷達到閾值(20),執行了批量重偏向 (測試結果在第19就開始批量重偏向了)


測試:批量撤銷
當撤銷偏向鎖閾值超過 40 次後,jvm 會認爲不該偏向,於是整個類的所有對象都會變爲不可偏向的,新建的對象也是不可偏向的。
thread3:
1-18  從無鎖狀態直接獲取輕量級鎖  (thread2釋放鎖之後變爲無鎖狀態)

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

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

新創建的對象: 無鎖狀態

總結

  • 批量重偏向和批量撤銷是針對類的優化,和對象無關;
  • 偏向鎖重偏向一次之後不可再次重偏向;
  • 當某個類已經觸發批量撤銷機制後,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編譯期間,通過對運行上下文的掃描,去除不可能存在共享資源競爭的鎖,通過鎖消除,可以節省毫無意義的請求鎖時間。
StringBuffer的append是個同步方法,但是append方法中的 StringBuffer 屬於一個局部變量,不可能從該方法中逃逸出去,因此其實這過程是線程安全的,可以將鎖消除。

    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");
    }

通過比較實際,開啓鎖消除用時4秒多,未開啓鎖消除用時6秒多。

測試代碼:

/**
進行兩種測試
* 關閉逃逸分析,同時調大堆空間,避免堆內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 {
private final static Logger log = LoggerFactory.getLogger(EscapeTest.class);

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;

}

通過測試發現,開啓逃逸分析後,線程實例總共50萬個,只有8萬多個在堆中,其他都在棧上分配;

關閉逃逸分析後50萬全部都在堆中。

4. hotspot關於synchronized的源碼分析

首先準備好HotSpot源碼
jdk8 hotspot源碼下載地址:http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/,選擇gz或者zip包下載。

目錄結構

  • cpu:和cpu相關的一些操作
  • os:在不同操作系統上的一些區別操作
  • os_cpu:關聯os和cpu的實現
  • share:公共代碼

share下面還有兩個目錄

  • tools:一些工具類
  • vm:公共源碼

然後使用Source Insight工具打開源碼工程,如下圖

首先看入口InterpreterRuntime:: monitorenter方法

IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
#ifdef ASSERT
  thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
  if (PrintBiasedLockingStatistics) {
    Atomic::inc(BiasedLocking::slow_path_entry_count_addr());
  }
  Handle h_obj(thread, elem->obj());
  assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
         "must be NULL or an object");
  if (UseBiasedLocking) {
    // Retry fast entry if bias is revoked to avoid unnecessary inflation
    ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
  } else {
    ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
  }
  assert(Universe::heap()->is_in_reserved_or_null(elem->obj()),
         "must be NULL or an object");
#ifdef ASSERT
  thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
IRT_END

看到上面註解Retry fast entry if bias is revoked to avoid unnecessary inflation,意思就是如果偏向鎖打開,就直接進入ObjectSynchronizer的fast_enter方法,避免不必要的膨脹,否則進入slow_enter方法,由此可知偏向鎖執行fast_enter方法,鎖的升級則進入slow_enter方法。

接着進入synchronizer.cpp

void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {
 // 如果偏向鎖打開
 if (UseBiasedLocking) {
    // 非安全點
    if (!SafepointSynchronize::is_at_safepoint()) {
      // 重偏向
      BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
      if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
        return;
      }
    // 回到安全點
    } else {
      assert(!attempt_rebias, "can not rebias toward VM thread");
      BiasedLocking::revoke_at_safepoint(obj);
    }
    assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
 }

 // 重點看
 slow_enter (obj, lock, THREAD) ;
}
void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
  markOop mark = obj->mark();
  assert(!mark->has_bias_pattern(), "should not see bias pattern here");
  // 判斷是否無鎖
  if (mark->is_neutral()) {
    // Anticipate successful CAS -- the ST of the displaced mark must
    // be visible <= the ST performed by the CAS.
    //把mark word保存到偏向鎖的displaced_header字段上
    lock->set_displaced_header(mark);
    // 通過CAS將mark word更新爲指向Lock Record的指針
    if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
      TEVENT (slow_enter: release stacklock) ;
      return ;
    }
    // Fall through to inflate() ...
  } else
  // 已經有鎖,並且mark中的指針指向當前線程的指針
  if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
    assert(lock != mark->locker(), "must not re-lock the same lock");
    assert(lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock");
    // 鎖重入,將null入棧到本地線程棧
    lock->set_displaced_header(NULL);
    return;
  }

#if 0
  // The following optimization isn't particularly useful.
  if (mark->has_monitor() && mark->monitor()->is_entered(THREAD)) {
    lock->set_displaced_header (NULL) ;
    return ;
  }
#endif

  // The object header will never be displaced to this lock,
  // so it does not matter what the value is, except that it
  // must be non-zero to avoid looking like a re-entrant lock,
  // and must not look locked either.
  lock->set_displaced_header(markOopDesc::unused_mark());
  // 1.先膨脹生成ObjectMonitor對象,
  // 2.再進入enter方法
  ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD);
}

首先看ObjectMonitor生成的過程。

ObjectMonitor * ATTR ObjectSynchronizer::inflate (Thread * Self, oop object) {
  // Inflate mutates the heap ...
  // Relaxing assertion for bug 6320749.
  assert (Universe::verify_in_progress() ||
          !SafepointSynchronize::is_at_safepoint(), "invariant") ;
  //循環,保證多線程同時調用
  for (;;) {
      const markOop mark = object->mark() ;
      assert (!mark->has_bias_pattern(), "invariant") ;

      // The mark can be in one of the following states:
      // *  Inflated     - just return  重量級鎖直接返回
      // *  Stack-locked - coerce it to inflated 輕量級鎖則膨脹
      // *  INFLATING    - busy wait for conversion to complete  膨脹中,則等膨脹完成
      // *  Neutral      - aggressively inflate the object.  無鎖狀態則膨脹
      // *  BIASED       - Illegal.  We should never see this  非法狀態, 不會出現

      // CASE: inflated 如果重量級鎖
      if (mark->has_monitor()) {
          // 獲取指向ObjectMonitor的指針,並直接返回
          ObjectMonitor * inf = mark->monitor() ;
          assert (inf->header()->is_neutral(), "invariant");
          assert (inf->object() == object, "invariant") ;
          assert (ObjectSynchronizer::verify_objmon_isinpool(inf), "monitor is invalid");
          return inf ;
      }

      // CASE: inflation in progress - inflating over a stack-lock.
      // Some other thread is converting from stack-locked to inflated.
      // Only that thread can complete inflation -- other threads must wait.
      // The INFLATING value is transient.
      // Currently, we spin/yield/park and poll the markword, waiting for inflation to finish.
      // We could always eliminate polling by parking the thread on some auxiliary list.
      // 檢查是否在膨脹狀態,如果膨脹,調用ReadStableMark等待
      if (mark == markOopDesc::INFLATING()) {
         TEVENT (Inflate: spin while INFLATING) ;
         ReadStableMark(object) ;
         continue ;
      }

      // CASE: stack-locked
      // Could be stack-locked either by this thread or by some other thread.
      //
      // Note that we allocate the objectmonitor speculatively, _before_ attempting
      // to install INFLATING into the mark word.  We originally installed INFLATING,
      // allocated the objectmonitor, and then finally STed the address of the
      // objectmonitor into the mark.  This was correct, but artificially lengthened
      // the interval in which INFLATED appeared in the mark, thus increasing
      // the odds of inflation contention.
      //
      // We now use per-thread private objectmonitor free lists.
      // These list are reprovisioned from the global free list outside the
      // critical INFLATING...ST interval.  A thread can transfer
      // multiple objectmonitors en-mass from the global free list to its local free list.
      // This reduces coherency traffic and lock contention on the global free list.
      // Using such local free lists, it doesn't matter if the omAlloc() call appears
      // before or after the CAS(INFLATING) operation.
      // See the comments in omAlloc().
      /// 輕量級鎖時,開始膨脹
      if (mark->has_locker()) {
          // 創建ObjectMonitor對象
          ObjectMonitor * m = omAlloc (Self) ;
          // Optimistically prepare the objectmonitor - anticipate successful CAS
          // We do this before the CAS in order to minimize the length of time
          // in which INFLATING appears in the mark.
          // 對象初始化
          m->Recycle();
          m->_Responsible  = NULL ;
          m->OwnerIsThread = 0 ;
          m->_recursions   = 0 ;
          m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ;   // Consider: maintain by type/class
          // CAS設置爲膨脹中
          markOop cmp = (markOop) Atomic::cmpxchg_ptr (markOopDesc::INFLATING(), object->mark_addr(), mark) ;
          // CAS失敗,釋放重試
          if (cmp != mark) {
             omRelease (Self, m, true) ;
             continue ;       // Interference -- just retry
          }

          // We've successfully installed INFLATING (0) into the mark-word.
          // This is the only case where 0 will appear in a mark-work.
          // Only the singular thread that successfully swings the mark-word
          // to 0 can perform (or more precisely, complete) inflation.
          //
          // Why do we CAS a 0 into the mark-word instead of just CASing the
          // mark-word from the stack-locked value directly to the new inflated state?
          // Consider what happens when a thread unlocks a stack-locked object.
          // It attempts to use CAS to swing the displaced header value from the
          // on-stack basiclock back into the object header.  Recall also that the
          // header value (hashcode, etc) can reside in (a) the object header, or
          // (b) a displaced header associated with the stack-lock, or (c) a displaced
          // header in an objectMonitor.  The inflate() routine must copy the header
          // value from the basiclock on the owner's stack to the objectMonitor, all
          // the while preserving the hashCode stability invariants.  If the owner
          // decides to release the lock while the value is 0, the unlock will fail
          // and control will eventually pass from slow_exit() to inflate.  The owner
          // will then spin, waiting for the 0 value to disappear.   Put another way,
          // the 0 causes the owner to stall if the owner happens to try to
          // drop the lock (restoring the header from the basiclock to the object)
          // while inflation is in-progress.  This protocol avoids races that might
          // would otherwise permit hashCode values to change or "flicker" for an object.
          // Critically, while object->mark is 0 mark->displaced_mark_helper() is stable.
          // 0 serves as a "BUSY" inflate-in-progress indicator.


          // fetch the displaced mark from the owner's stack.
          // The owner can't die or unwind past the lock while our INFLATING
          // object is in the mark.  Furthermore the owner can't complete
          // an unlock on the object, either.
          // CAS成功,替換mark word到本地線程棧,並設置monitor的_header、_owner、_object
          markOop dmw = mark->displaced_mark_helper() ;
          assert (dmw->is_neutral(), "invariant") ;

          // Setup monitor fields to proper values -- prepare the monitor
          m->set_header(dmw) ;

          // Optimization: if the mark->locker stack address is associated
          // with this thread we could simply set m->_owner = Self and
          // m->OwnerIsThread = 1. Note that a thread can inflate an object
          // that it has stack-locked -- as might happen in wait() -- directly
          // with CAS.  That is, we can avoid the xchg-NULL .... ST idiom.
          m->set_owner(mark->locker());
          m->set_object(object);
          // TODO-FIXME: assert BasicLock->dhw != 0.

          // Must preserve store ordering. The monitor state must
          // be stable at the time of publishing the monitor address.
          guarantee (object->mark() == markOopDesc::INFLATING(), "invariant") ;
          // 設置mark word爲重量級鎖狀態
          object->release_set_mark(markOopDesc::encode(m));

          // Hopefully the performance counters are allocated on distinct cache lines
          // to avoid false sharing on MP systems ...
          if (ObjectMonitor::_sync_Inflations != NULL) ObjectMonitor::_sync_Inflations->inc() ;
          TEVENT(Inflate: overwrite stacklock) ;
          if (TraceMonitorInflation) {
            if (object->is_instance()) {
              ResourceMark rm;
              tty->print_cr("Inflating object " INTPTR_FORMAT " , mark " INTPTR_FORMAT " , type %s",
                (void *) object, (intptr_t) object->mark(),
                object->klass()->external_name());
            }
          }
          return m ;
      }

      // CASE: neutral
      // TODO-FIXME: for entry we currently inflate and then try to CAS _owner.
      // If we know we're inflating for entry it's better to inflate by swinging a
      // pre-locked objectMonitor pointer into the object header.   A successful
      // CAS inflates the object *and* confers ownership to the inflating thread.
      // In the current implementation we use a 2-step mechanism where we CAS()
      // to inflate and then CAS() again to try to swing _owner from NULL to Self.
      // An inflateTry() method that we could call from fast_enter() and slow_enter()
      // would be useful.

      assert (mark->is_neutral(), "invariant");
      // 如果無鎖狀態,創建ObjectMonitor並初始化
      ObjectMonitor * m = omAlloc (Self) ;
      // prepare m for installation - set monitor to initial state
      m->Recycle();
      m->set_header(mark);
      m->set_owner(NULL);
      m->set_object(object);
      m->OwnerIsThread = 1 ;
      m->_recursions   = 0 ;
      m->_Responsible  = NULL ;
      m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ;       // consider: keep metastats by type/class
      
      // CAS設置對象爲重量級鎖狀態,CAS失敗,釋放重量級鎖再重試
      if (Atomic::cmpxchg_ptr (markOopDesc::encode(m), object->mark_addr(), mark) != mark) {
          m->set_object (NULL) ;
          m->set_owner  (NULL) ;
          m->OwnerIsThread = 0 ;
          m->Recycle() ;
          omRelease (Self, m, true) ;
          m = NULL ;
          continue ;
          // interference - the markword changed - just retry.
          // The state-transitions are one-way, so there's no chance of
          // live-lock -- "Inflated" is an absorbing state.
      }

      // Hopefully the performance counters are allocated on distinct
      // cache lines to avoid false sharing on MP systems ...
      // 下面是緩存行避免僞共享發生的情況
      if (ObjectMonitor::_sync_Inflations != NULL) ObjectMonitor::_sync_Inflations->inc() ;
      TEVENT(Inflate: overwrite neutral) ;
      if (TraceMonitorInflation) {
        if (object->is_instance()) {
          ResourceMark rm;
          tty->print_cr("Inflating object " INTPTR_FORMAT " , mark " INTPTR_FORMAT " , type %s",
            (void *) object, (intptr_t) object->mark(),
            object->klass()->external_name());
        }
      }
      return m ;
  }
}

ObjectMonitor生成後,進入ObjectMonitor.enter方法,重量級加鎖的邏輯都是在這裏完成的。

void ATTR ObjectMonitor::enter(TRAPS) {
  Thread * const Self = THREAD ;
  void * cur ;
//通過CAS操作嘗試將_owner變量設置爲當前線程,如果_owner爲NULL表示鎖未被佔用
//CAS:內存值、預期值、新值,只有當內存值==預期值,才能將新值替換內存值
  cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
  if (cur == NULL) { //如果NULL,表示獲取鎖成功,直接返回即可
     assert (_recursions == 0   , "invariant") ;
     assert (_owner      == Self, "invariant") ;
     return ;
  }
//線程重入,synchronized的可重入特性原理,_owner保存的線程與當前正在執行的線程相同,將_recursions++
  if (cur == Self) { 
     _recursions ++ ;
     return ;
  }
//表示線程第一次進入monitor,則進行一些設置
  if (Self->is_lock_owned ((address)cur)) {
    assert (_recursions == 0, "internal state error");
    _recursions = 1 ;  //鎖的次數設置爲1
    _owner = Self ;  //將_owner設置爲當前線程
    OwnerIsThread = 1 ;  
    return ;
  }

  .....
  
 
    //獲取鎖失敗
    for (;;) {
      jt->set_suspend_equivalent();
    //等待鎖的釋放
      EnterI (THREAD) ;

      if (!ExitSuspendEquivalent(jt)) break ;

      _recursions = 0 ;
      _succ = NULL ;
      exit (false, Self) ;

      jt->java_suspend_self();
    }
    Self->set_current_pending_monitor(NULL);
  }
}

上面步驟總結爲

  • 1.通過CAS嘗試將_owner變量設置爲當前線程
  • 2.如果是線程重入(下面有舉例),則將_recurisons++
  • 3.如果線程是第一次進入,則將_recurisons設置爲1,將_owner設置爲當前線程,該線程獲取鎖成功並返回
  • 4.如果獲取鎖失敗,則等待鎖的釋放

接着進入所等待源碼
在鎖競爭源碼中最後一步,如果獲取鎖失敗,則等待鎖的釋放,由MonitorObject類中的EnterI()方法來實現

void ATTR ObjectMonitor::EnterI (TRAPS) {
    Thread * Self = THREAD ;
    assert (Self->is_Java_thread(), "invariant") ;
    assert (((JavaThread *) Self)->thread_state() == _thread_blocked   , "invariant") ;
  //再次嘗試獲取鎖,獲取成功直接返回
    if (TryLock (Self) > 0) {
		....
        return ;
    }

    DeferredInitialize () ;


  //嘗試自旋獲取鎖,獲取鎖成功直接返回
    if (TrySpin (Self) > 0) {
        ....
        return ;
    }

  //前面的嘗試都失敗,則將該線程信息封裝到node節點
    ObjectWaiter node(Self) ;
    Self->_ParkEvent->reset() ;
    node._prev   = (ObjectWaiter *) 0xBAD ;
    node.TState  = ObjectWaiter::TS_CXQ ;

  
    ObjectWaiter * nxt ;
    //將node節點插入到_cxq的頭部,前面說過鎖獲取失敗的線程首先會進入_cxq
    //_cxq是一個單鏈表,等到一輪過去在該_cxq列表中的線程還未成功獲取鎖,
    //則進入_EntryList列表
    for (;;) {                  //注意這裏的死循環操作
        node._next = nxt = _cxq ;
        //這裏插入節點時也使用了CAS,因爲可能有多個線程失敗將加入_cxq鏈表
        if (Atomic::cmpxchg_ptr (&node, &_cxq, nxt) == nxt) break ;


        //如果線程CAS插入_cxq鏈表失敗,它會再搶救一下看看能不能獲取到鎖
        if (TryLock (Self) > 0) {
            ...
            return ;
        }
    }


    //競爭減弱時,將該線程設置爲_Responsible(負責線程),定時輪詢_owner
    //後面該線程會調用定時的park方法,防止死鎖
    if ((SyncFlags & 16) == 0 && nxt == NULL && _EntryList == NULL) {
        Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ;
    }


    TEVENT (Inflated enter - Contention) ;
    int nWakeups = 0 ;
    int RecheckInterval = 1 ;
    //前面獲取鎖失敗的線程已經放入到了_cxq列表,但還未掛起
    //下面是將_cxq列表掛起的代碼,線程一旦掛起,必須喚醒之後才能繼續操作
    for (;;) {
        //掛起之前,再次嘗試獲取鎖,看看能不能成功,成功則跳出循環
        if (TryLock (Self) > 0) break ;
        assert (_owner != Self, "invariant") ;

        if ((SyncFlags & 2) && _Responsible == NULL) {
           Atomic::cmpxchg_ptr (Self, &_Responsible, NULL) ;
        }
        //將當前線程掛起(park()方法)
        // park self
        //如果當前線程是_Responsible線程,則調用定時的park方法,防止死鎖
        if (_Responsible == Self || (SyncFlags & 1)) {
            TEVENT (Inflated enter - park TIMED) ;
            Self->_ParkEvent->park ((jlong) RecheckInterval) ;
            // Increase the RecheckInterval, but clamp the value.
            RecheckInterval *= 8 ;
            if (RecheckInterval > 1000) RecheckInterval = 1000 ;
        } else {
            TEVENT (Inflated enter - park UNTIMED) ;
            Self->_ParkEvent->park() ;
        }
        //當線程被喚醒之後,會再次嘗試獲取鎖
        if (TryLock(Self) > 0) break ;
		//喚醒鎖之後,還出現競爭,記錄喚醒次數,這裏的計數器
		//並沒有受鎖的保護,也沒有原子更新,爲了獲取更低的探究影響
        TEVENT (Inflated enter - Futile wakeup) ;
        if (ObjectMonitor::_sync_FutileWakeups != NULL) {
           ObjectMonitor::_sync_FutileWakeups->inc() ;
        }
        ++ nWakeups ; //喚醒次數

        //自旋嘗試獲取鎖
        if ((Knob_SpinAfterFutile & 1) && TrySpin (Self) > 0) break ;


        if ((Knob_ResetEvent & 1) && Self->_ParkEvent->fired()) {
           Self->_ParkEvent->reset() ;
           OrderAccess::fence() ;
        }
        if (_succ == Self) _succ = NULL ;

        // Invariant: after clearing _succ a thread *must* retry _owner before parking.
        OrderAccess::fence() ;
    }

    //已經獲取到了鎖,將當前節點從_EntryList隊列中刪除
    UnlinkAfterAcquire (Self, &node) ;
    if (_succ == Self) _succ = NULL ;

  	...
    return ;
}


步驟分爲如下幾步:

  • 1.首先tryLock再次嘗試獲取鎖,之後再CAS嘗試獲取鎖;失敗後將當前線程信息封裝成ObjectWaiter對象;
  • 2.在for(;;)循環中,通過CAS將該節點插入到_cxq鏈表的頭部(這個時刻可能有多個獲取鎖失敗的線程要插入),CAS插入失敗的線程再次嘗試獲取鎖;
  • 3.如果還沒獲取到鎖,則將線程掛起;等待喚醒;
  • 4.當線程被喚醒時,再次嘗試獲取鎖。

這裏的設計精髓是通過多次tryLock嘗試獲取鎖和CAS獲取鎖無限推遲了線程的掛起操作,你可以看到從開始到線程掛起的代碼中,出現了多次的嘗試獲取鎖;因爲線程的掛起與喚醒涉及到了狀態的轉換(內核態和用戶態),這種頻繁的切換必定會給系統帶來性能上的瓶頸。所以它的設計意圖就是儘量推辭線程的掛起時間,取一個極限的時間掛起線程。另外源碼中定義了負責線程_Responsible,這種標識的線程調用的是定時的park(線程掛起),避免死鎖.

鎖釋放源碼

void ATTR ObjectMonitor::exit(bool not_suspended, TRAPS) {
   Thread * Self = THREAD ; 
   if (THREAD != _owner) {  //判斷當前線程是否是線程持有者
    //當前線程是之前持有輕量級鎖的線程。由輕量級鎖膨脹後還沒調用過enter方法,_owner會是指向Lock Record的指針
     if (THREAD->is_lock_owned((address) _owner)) {

       assert (_recursions == 0, "invariant") ;
       _owner = THREAD ;
       _recursions = 0 ;
       OwnerIsThread = 1 ;
     } else {  //當前線程不是鎖的持有者--》出現異常
       TEVENT (Exit - Throw IMSX) ;
       assert(false, "Non-balanced monitor enter/exit!");
       if (false) {
          THROW(vmSymbols::java_lang_IllegalMonitorStateException());
       }
       return;
     }
   }
  //重入,計數器-1,返回
   if (_recursions != 0) {
     _recursions--;        // this is simple recursive enter
     TEVENT (Inflated exit - recursive) ;
     return ;
   }

   //_Responsible設置爲NULL
   if ((SyncFlags & 4) == 0) {
      _Responsible = NULL ;
   }

#if INCLUDE_JFR
   if (not_suspended && EventJavaMonitorEnter::is_enabled()) {
    _previous_owner_tid = JFR_THREAD_ID(Self);
   }
#endif

   for (;;) {
      assert (THREAD == _owner, "invariant") ;


      if (Knob_ExitPolicy == 0) {
       
         //先釋放鎖,這時如果有其他線程獲取鎖,則能獲取到
         OrderAccess::release_store_ptr (&_owner, NULL) ;   // drop the lock
         OrderAccess::storeload() ;                         // See if we need to wake a successor
         //等待隊列爲空,或者有"醒着的線程”,則不需要去等待隊列喚醒線程了,直接返回即可
         if ((intptr_t(_EntryList)|intptr_t(_cxq)) == 0 || _succ != NULL) {
            TEVENT (Inflated exit - simple egress) ;
            return ;
         }
         TEVENT (Inflated exit - complex egress) ;

 		 //當前線程重新獲取鎖,因爲後序要喚醒隊列
 		 //一旦獲取失敗,說明有線程獲取到鎖了,直接返回即可,不需要獲取鎖再去喚醒線程了
         if (Atomic::cmpxchg_ptr (THREAD, &_owner, NULL) != NULL) {
            return ;
         }
         TEVENT (Exit - Reacquired) ;
      } else {
         if ((intptr_t(_EntryList)|intptr_t(_cxq)) == 0 || _succ != NULL) {
            OrderAccess::release_store_ptr (&_owner, NULL) ;   // drop the lock
            OrderAccess::storeload() ;
            // Ratify the previously observed values.
            if (_cxq == NULL || _succ != NULL) {
                TEVENT (Inflated exit - simple egress) ;
                return ;
            }
             //當前線程重新獲取鎖,因爲後序要喚醒隊列
              //一旦獲取失敗,說明有線程獲取到鎖了,直接返回即可,不需要獲取鎖再去喚醒線程了
            if (Atomic::cmpxchg_ptr (THREAD, &_owner, NULL) != NULL) {
               TEVENT (Inflated exit - reacquired succeeded) ;
               return ;
            }
            TEVENT (Inflated exit - reacquired failed) ;
         } else {
            TEVENT (Inflated exit - complex egress) ;
         }
      }

      guarantee (_owner == THREAD, "invariant") ;

      ObjectWaiter * w = NULL ;
      int QMode = Knob_QMode ;   //根據QMode的不同,會有不同的喚醒策略

      if (QMode == 2 && _cxq != NULL) {
        //QMode==2,_cxq中有優先級更高的線程,直接喚醒_cxq的隊首線程
		.........
          return ;
      }
      //當QMode=3的時候 講_cxq中的數據加入到_EntryList尾部中來 然後從_EntryList開始獲取
      if (QMode == 3 && _cxq != NULL) {   
          .....
          }
		....... //省略
		.......
      //當QMode=4的時候 講_cxq中的數據加入到_EntryList前面來 然後從_EntryList開始獲取
      if (QMode == 4 && _cxq != NULL) {
			......
          }
          
        //批量修改狀態標誌改成TS_ENTER
          ObjectWaiter * q = NULL ;
          ObjectWaiter * p ;
          for (p = w ; p != NULL ; p = p->_next) {
              guarantee (p->TState == ObjectWaiter::TS_CXQ, "Invariant") ;
              p->TState = ObjectWaiter::TS_ENTER ;
              p->_prev = q ;
              q = p ;
          }
        //插到原有的_EntryList前面 從員_EntryList中獲取
          // Prepend the RATs to the EntryList
          if (_EntryList != NULL) {
              q->_next = _EntryList ;
              _EntryList->_prev = q ;
          }
          _EntryList = w ;

      }
	..........
	
   }
}


上述代碼總結如下步驟:

  • 1.將_recursions減1,_owner置空;
  • 2.如果隊列中等待的線程爲空或者_succ不爲空(有"醒着的線程",則不需要去喚醒線程了),直接返回即可;
  • 3.第二條不滿足,當前線程重新獲取鎖,去喚醒線程;
  • 4.喚醒線程,根據QMode的不同,有不同的喚醒策略;

QMode = 2且cxq非空:cxq中有優先級更高的線程,直接喚醒_cxq的隊首線程;
QMode = 3且cxq非空:把cxq隊列插入到EntryList的尾部;
QMode = 4且cxq非空:把cxq隊列插入到EntryList的頭部;
QMode = 0:暫時什麼都不做,繼續往下看;

只有QMode=2的時候會提前返回,等於0、3、4的時候都會繼續往下執行:

這裏的設計精髓是首先就將鎖釋放,然後再去判斷是否有醒着的線程,因爲可能有線程正在嘗試或者自旋獲取鎖,如果有線程活着,需要再讓該線程重新獲取鎖去喚醒線程。

最後通過流程兩張圖把創建ObjectMonitor對象的過程與進入enter方法加鎖與解鎖的過程呈現出來,把我主要流程,更多地代碼細節也可以從源碼的英文註解中得到答案。

5. 總結

通過本篇由淺入深,逐步分析了synchronized從各層角度實現的細節以及原理,我們可以從中學到一些思路,比如鎖的設計,能夠通過CAS,偏向鎖,輕量級鎖等方式來實現的時候,儘量不要升級到重量級鎖,等到競爭太大浪費cpu開銷的時候,才引入重量級鎖;比如synchronized原子性是通過鎖對象保證只有一個線程訪問臨界資源來實現,可見性通過源碼裏的彙編volatile結合硬件底層指令實現,有序性通過源碼底層的讀寫屏障並藉助於硬件指令完成。synchronized底層在鎖優化的時候也用了大量的CAS操作,提升性能;以及等待隊列與阻塞隊列設計如何同步進入管程的設計,這些設計思想也是後面Reentrantlock設計的中引入條件隊列的思想,底層都是相同的,任何應用層的軟件設計都能從底層的設計思想與精髓實現中找到原型與影子,當看到最底層C、C++或者彙編以及硬件指令級別的實現時,一切頂層彷彿就能通透。

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