1. 程式人生 > >多執行緒程式設計(3)——synchronized原理以及使用

多執行緒程式設計(3)——synchronized原理以及使用

一、物件頭

  通常在java中一個物件主要包含三部分:

  • 物件頭 主要包含GC的狀態、、型別、類的模板資訊(地址)、synchronization狀態等,在後面介紹。

  • 例項資料:程式程式碼中定義的各種型別的欄位內容。

  • 對齊資料:物件的大小必須是 8 位元組的整數倍,此項根據情況而定,若物件頭和例項資料大小正好是8的倍數,則不需要對齊資料,否則大小就是8的差數。

先看下面的例項、程式的輸出以及解釋。

/*需提前引入jar包
<!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core 解析java物件佈局 -->
        <dependency>
            <groupId>org.openjdk.jol</groupId>
            <artifactId>jol-core</artifactId>
            <version>0.9</version>
        </dependency>
​
*/
//Java物件以8個位元組對其,不夠則使用對其資料
public class Student {
    private int id;       // 4位元組
    private boolean sex;  // 1位元組
    public Student(int id, boolean sex){
        this.id = id;
        this.sex = sex;
    }
}
public class Test01 {
    public static void main(String[] args) {
        Student stu = new Student(6, true);
        //計算物件hash,底層是C++實現,不需要java去獲取,如果此處不呼叫,則後面的hash值不會去計算
        System.out.println("hashcode: " + stu.hashCode());  
        System.out.println(ClassLayout.parseInstance(stu).toPrintable());
    }
}
/* output
hashcode: 523429237
com.thread.synchronizeDemo.Student object internals:
OFFSET SIZE TYPE DESCRIPTION        VALUE
 0     4    (object header)    01 75 e5 32 (00000001 01110101 11100101 00110010) (853898497)
4      4     (object header)    1f 00 00 00 (00011111 00000000 00000000 00000000) (31)
8      4     (object header)    43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
12     4       int Student.id                                6
16     1   boolean Student.sex                               true
17     7           (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total
​
備註:上述程式碼在64位的機器上執行,此時
物件頭佔  (4+4+4)*8 = 96 位(bit)
例項資料  (4+1)*8 = 40 位(bit)
對齊資料  7*8 = 56 位(bit) 因為Java物件以8個位元組對其的方式,需補7byte去對齊
*/

   下面主要陳述對物件頭的解釋,內容從hotspot官網摘抄下來的資訊:

object header

  Common structure at the beginning of every GC-managed heap object. (Every oop points to an object header.) Includes fundamental information about the heap object's layout, type, GC state, synchronization state, and identity hash code. Consists of two words. In arrays it is immediately followed by a length field. Note that both Java objects and VM-internal objects have a common object header format.

mark word

  The first word of every object header. Usually a set of bitfields including synchronization state and identity hash code. May also be a pointer (with characteristic low bit encoding) to synchronization related information. During GC, may contain GC state bits.

klass pointer

  The second word of every object header. Points to another object (a metaobject) which describes the layout and behavior of the original objec

  由此可知,物件頭主要包含GC的狀態(用4位表示——表示範圍0-15,用來記錄GC年齡,這也就是為什麼物件在survivor中從from區到to區來回轉換15次後轉入到老年代tenured區)、型別、類的模板資訊(地址)、synchronization 狀態等,由兩個字組成mark word和klass pointer(類元素據信息地址,具體資料通常在堆的方法區中,即8位元組,但有時候會有一些優化設定,會開啟指標壓縮,將代表klass pointer的8位元組變成4位元組大小,這也是為什麼在上述程式碼中物件頭大小是(8+4)byte,而不是16byte。)。本節最主要介紹物件頭的mark word這部分。關於物件頭中每部分bit所代表的意義可以檢視hotspot原始碼中程式碼的注,這段註釋是從openjdk中拷貝的。

JVM和hotspot、openjdk的區別

JVM是一種產品的規範定義,hotspot(Oracle公司)是對該規範實現的產品,還有遵循這些規範的其他產品,比如J9(IBM開發的一個高度模組化的JVM)、Zing VM等。

openjdk是一個hotspot專案的大部分原始碼(可以通過編譯後變成.exe檔案),hotspot小部分程式碼Oracle並未公佈

// openjdk-8-src-b132-03_mar_2014\openjdk\hotspot\src\share\vm\oops\markOop.hpp
/*
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)
​
*/

可以看到在32位機器和64位機器中,物件的佈局的差異還是很大的,本文主要 敘述64位機器下的佈局,其實兩者無非是位數不同而已,大同小異。在64位機器用64位(8byte)表示Mark Word,首先前25位(0-25)是未被使用,接下來31位表示hash值,然後是物件分代年齡大小,最後Synchronized的鎖資訊,分為兩部分,共3bit,如下表,鎖的嚴格性依次是鎖、偏向鎖、輕量級鎖、重量級鎖。

 

關於鎖的一些解釋

無鎖

  無鎖沒有對資源進行鎖定,所有的執行緒都能訪問並修改同一個資源,但同時只有一個執行緒能修改成功。

偏向鎖

  引入偏向鎖是為了在無多執行緒競爭的情況下,一段同步程式碼一直被一個執行緒所訪問因為輕量級鎖的獲取及釋放依賴多次CAS原子指令,而偏向鎖只需要在置換ThreadID的時候依賴一次CAS原子指令,當由另外的執行緒所訪問,偏向鎖就會升級為輕量級鎖。

輕量級鎖 

  當鎖是偏向鎖的時候,被另外的執行緒所訪問,偏向鎖就會升級為輕量級鎖,其他執行緒會通過自旋的形式嘗試獲取鎖,不會阻塞,從而提高效能。輕量級鎖所適應的場景是執行緒交替執行同步塊的情況,如果存在同一時間訪問同一鎖的情況,就會導致輕量級鎖膨脹為重量級鎖。

重量級鎖

  依賴於作業系統Mutex Lock所實現的鎖,JDK中對Synchronized做的種種優化,其核心都是為了減少這種重量級鎖的使用。JDK1.6以後,為了減少獲得鎖和釋放鎖所帶來的效能消耗,提高效能,引入了“輕量級鎖”和“偏向鎖”。  

GC

  這並不是鎖的狀態,而是GC標誌,等待GC回收。

現在開始從程式層面分析前面程式的物件頭的佈局資訊,在此之前需要知道的是,在windows中對於資料的儲存採用的是小端儲存,所以要反過來讀

大端模式——是指資料的高位元組儲存在記憶體的低地址中,而資料的低位元組儲存在記憶體的高地址中,這樣的儲存模式有點兒類似於把資料當作字串順序處理:地址由小向大增加,而資料從高位往低位放;這和我們的閱讀習慣一致。

小端模式——是指資料的高位元組儲存在記憶體的高地址中,而資料的低位元組儲存在記憶體的低地址中,這種儲存模式將地址的高低和資料位權有效地結合起來,高地址部分權值高,低地址部分權值低。 一般在網路中用的大端;本地用的小端;

執行程式如下,可以看到對應的hashcode值被打印出來:

public static void main(String[] args) {
     Student stu = new Student(6, true);
    //Integer.toHexString()此方法返回的字串表示的無符號整數引數所表示的值以十六進位制
     System.out.println("hashcode: " + Integer.toHexString(stu.hashCode()));
     System.out.println(ClassLayout.parseInstance(stu).toPrintable());
}
/*
hashcode: 1f32e575
com.thread.synchronizeDemo.Student object internals:
 OFFSET  SIZE      TYPE DESCRIPTION                               VALUE
      0     4           (object header)                           01 75 e5 32 (00000001 01110101 11100101 00110010) (853898497)
      4     4           (object header)                           1f 00 00 00 (00011111 00000000 00000000 00000000) (31)
      8     4           (object header)                           43 c1 00 20 (01000011 11000001 00000000 00100000) (536920387)
     12     4       int Student.id                                6
     16     1   boolean Student.sex                               true
     17     7           (loss due to the next object alignment)
Instance size: 24 bytes
Space losses: 0 bytes internal + 7 bytes external = 7 bytes total

//前8個位元組反過來看,可以看出物件頭的hash是1f32e575,同時是無鎖的狀態00000001
*/

 二、Monitor

       可以把它理解為一個同步工具(資料結構),也可以描述為一種同步機制,通常被描述為一個物件。每個物件都存在著一個 monitor 與之關聯,物件與其 monitor 之間的關係有存在多種實現方式,如monitor可以與物件一起建立銷燬或當執行緒試圖獲取物件鎖時自動生成,但當一個 monitor 被某個執行緒持有後,它便處於鎖定狀態(每一個執行緒都有一個可用 monitor record 列表)[具體可以看參考資料5]。需要注意的是這種監視器鎖是發生在物件的內部鎖已經變成重量級鎖的時候。

/*  openjdk-8-src-b132-03_mar_2014\openjdk\hotspot\src\share\vm\runtime\ObjectMonitor.hpp
// 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; _owner = NULL; _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; } */

  Monitor的實現主要藉助三個結構去完成多執行緒的併發操作——_owner、_WaitSet 、_EntryList。當多個執行緒同時訪問由synchronized修飾的物件、類或一段同步程式碼時,首先會進入_EntryList 集合,如果某個執行緒取得了_owner的所有權,該執行緒就可以去執行,如果該執行緒呼叫了wait()方法,就會放棄_owner的所有權,進入等待狀態,等下一次喚醒。如下圖(圖片摘自參考資料5)。


 三、synchronized的用法

     synchronized修飾方法和修飾一個程式碼塊類似,只是作用範圍不一樣,修飾程式碼塊是大括號括起來的範圍,而修飾方法範圍是整個函式。其中synchronized(this) 與synchronized(class) 之間的區別有以下五點要注意:

    1、對於靜態方法,由於此時物件還未生成,所以只能採用類鎖;

    2、只要採用類鎖,就會攔截所有執行緒,只能讓一個執行緒訪問。

    3、對於物件鎖(this),如果是同一個例項,就會按順序訪問,但是如果是不同例項,就可以同時訪問。

   4、如果物件鎖跟訪問的物件沒有關係,那麼就會都同時訪問。

   5、當一個執行緒訪問object的一個synchronized(this)同步程式碼塊時,另一個執行緒仍然可以訪問該object中的非synchronized(this)同步程式碼塊。

當然,Synchronized也可修飾一個靜態方法,而靜態方法是屬於類的而不屬於物件的,所以synchronized修飾的靜態方法鎖定的是這個類的所有物件。關於如下synchronized的用法,我們經常會碰到的案例:

public class Thread5 implements Runnable {
    private static int count = 0;
    public synchronized static void add() {
        count++;
    }
    @Override
    public void run() {
        for (int i = 0; i < 1000000; i++) {
            synchronized (Thread5.class){
                count++;
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        ExecutorService es = Executors.newFixedThreadPool(10);
        for (int i = 0; i < 20; i++) {
            es.execute(new Thread5());
        }
        es.shutdown();
        es.awaitTermination(6, TimeUnit.SECONDS);
        System.out.println(count);
    }
}
/* 類鎖
  20000000
  */

而一旦換成物件鎖,不同例項,就可以同時訪問。則會出錯:

public void run() {
        for (int i = 0; i < 1000000; i++) {
            synchronized (this){
                count++;
            }
        }
}
/* 物件鎖
 10746948
*/

這是因為靜態變數並不屬於某個例項物件,而是屬於類所有,所以對某個例項加鎖,並不會改變count變數髒讀和髒寫的情況,還是造成結果不正確。

 

參考資料

  1. 目前主流的 Java 虛擬機器有哪些?

  2. 物件佈局的各部分介紹——HotSpot Glossary of Terms

  3. 不可不說的Java“鎖”事

  4. Synchronized的一些東西

  5. 深入理解Java併發之synchronized實現原理