天天看點

synchronized實戰

synchronized

synchronized 是最常用的實作同步的手段,在 Java SE 1.6 以及之後的版本,對 synchronized 進行了優化,使 synchronized 整體的性能得到了很大的提升,下面看下 synchronized 的相關實作。

示例

下面是一個基本的使用示例:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * Created by Jikai Zhang on 2017/5/2.
 */
public class SynchronizedTest

    public static int counter = 0;

    public synchronized static void increase() {
        for(int i = 0; i < 10000; i++) {
            counter++;
        }
    }

    public static void main(String[] args) {
        ExecutorService executor = Executors.newFixedThreadPool(100);
        for(int i = 0; i < 10; i ++) {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    increase();
                }
            });
        }
        executor.shutdown();
        while      

synchronized 代碼執行之前,要首先鎖住一個對象,具體為下面三種情況:

* 對于普通方法(非靜态)方法,鎖住的是目前執行個體對象

* 對于靜态方法,鎖住是目前類的 Class 對象

* 對于同步方法塊,鎖住的是 synchronized 括号中配置的對象。

synchronized 關鍵字經過編譯之後,會在同步塊的前後分别形成 monitorenter 和 monitorexit 這兩個位元組碼指令,這兩條指令都需要一個 reference 類型的參數來指明要鎖定和解鎖的對象。當執行 monitorenter 指令時,會首先嘗試擷取對象的鎖,如果對象沒被鎖定,或者目前線程已經擁有了那個對象的鎖,就把鎖的計數器加 1,相應的,在執行 monitorexit 指令時會将鎖的計數器減 1,當計數器為 0 時,鎖就會被釋放。如果擷取鎖失敗,那麼目前線程就要進入阻塞狀态,直到另外一個線程釋放鎖。

Java 為了優化 synchronized 的性能,引入了偏向鎖、輕量級鎖、重量級鎖、自旋鎖等概念。偏向鎖不需要任何同步,它适用于單線程環境,不會有其他線程競争,也就不需要同步。輕量級鎖使用 CAS 指令同步,它适用于雖然多個線程執行,但線程間沒有競争,例如線程 1 在 0-5 時刻内執行,線程 2 在 6-10 時刻内執行,兩者的執行時刻沒有重疊。重量級鎖就是我們通常意義上的鎖,适用于多個線程競争的情況,同一個時間内隻有一個線程獲得鎖,其餘的線程都處于阻塞狀态。

對象頭

我們知道每個 Java 對象都可以作為一個鎖,而 Java 對象将鎖的相關資訊存在了對象頭中。如果目前對象為數組對象,則對象頭由下面 3 部分組成,每部分為一個字長(32位虛拟機長為 32 bits, 64 位虛拟機長尾 64 bits):

* Mark Word:存儲對象的 hashCode 或者鎖資訊

* Class Metadata Address:存儲到象資料類型的指針

* Array Length: 數組的長度

synchronized實戰

如果目前對象是非數組對象,那麼對象頭隻包含 Mark Word 和 Class Metadata Addres。在 Mark Word 中,固定使用最後兩位 bit 來存儲目前對象的鎖标記。而其他位中存儲的内容會随着目前對象的鎖狀态改變而發生改變。下面是對象鎖标志位的幾種狀态:

1. 01 - 未鎖定狀态 / 可擷取偏向鎖狀态

2. 00 - 輕量級鎖定

3. 10 - 重量級鎖定

4. 11 - GC 标記

當鎖标志位為 01 時,Mark Word 用剩餘位中的一個 bit 位來辨別目前鎖處于可擷取偏向鎖狀态還是未鎖定狀态。如果該位為 1 表明現在是處于偏向鎖狀态,如果是 0 表明是未鎖定狀态。

synchronized實戰
synchronized實戰
圖檔來自 Java 并發程式設計的藝術

偏向鎖

擷取

線程獲得偏向鎖的前提條件是目前對象處于可擷取偏向鎖的狀态,什麼意思呢,就是說目前對象的鎖狀态标志位為 01,并且偏向鎖标志位為 1,如果偏向鎖标志位為 0,則線程無法獲得偏向鎖,隻能擷取輕量級鎖。當線程擷取鎖時,如果發現對象處于可擷取偏向鎖的狀态,會首先檢視對象的 Mark Word 中是否儲存了目前線程的 ID(見上面對象頭處于不同鎖狀态時的資料分布圖),如果發現儲存了目前線程 ID,說明目前線程已經獲得了偏向鎖,那麼就直接執行同步塊中的代碼。如果發現沒有儲存目前線程 ID,那麼嘗試使用 CAS 操作将目前線程 ID 寫入 Mark Word 中,從上面的圖示中,我們知道如果沒有其他線程獲得偏向鎖,那麼 Mark Word 的前 25 位就是儲存的對象的 hashCode,如果線程發現對象中的 Mark Word 的前 25 位就是儲存的對象的 hashCode,那麼 CAS 操作就執行成功,如果發現不是 hashCode,說明之前已經有線程擷取了偏向鎖,證明系統中至少有兩個線程在執行,那麼此時就要撤銷偏向鎖。是以我們說線程擷取了偏向鎖,就等同于下面兩個條件都成立:

  1. 目前對象處于可擷取鎖的狀态,鎖狀态标志位為 01,并且偏向鎖标志位為 1
  2. 線程通過 CAS 操作成功将線程 ID 寫入 Mark Word 中

是以我們說隻有第一個通路鎖對象的線程才有機會獲得對象的偏向鎖。偏向鎖是不會主動釋放的,隻有出現了競争才會釋放偏向鎖。

釋放

偏向鎖釋放分為兩種情況,如果擷取偏向鎖的線程不處于運作狀态,就将對象置為無鎖狀态(偏向鎖标志位置為 0,線程 ID 置為空),此時另外一個線程隻能嘗試擷取線程的輕量級鎖。如果擷取偏向鎖的線程正在執行,會挂起運作線程,然後将對象置為輕量級鎖狀态(鎖狀态标志位置為 00),随後再恢複挂起的線程,将偏向鎖更新為輕量級鎖,此時另外一個線程可以通過自旋嘗試獲得鎖,當自旋到一定次數仍然擷取不到輕量級鎖,對象鎖就會由輕量級鎖更新為重量級鎖,擷取不到鎖的線程就會被阻塞。

輕量級鎖

擷取

輕量級鎖通過 CAS 操作進行同步,适用于有多個線程執行但不存在競争或者競争很少的情況。當代碼執行到同步塊時,發現對象處于無鎖狀态(鎖狀态标志位為 01,偏向鎖标志位為 0),那麼虛拟機就首先在目前線程的堆棧中建立一個名為鎖記錄(Lock Record)的空間,用于存儲對象目前的 Mark Word 的拷貝(官方為這份拷貝加了一個 Displaced 字首,即 Displaced Mark Word,這裡儲存拷貝是為了釋放鎖時将資料還原回來),然後虛拟機使用 CAS 操作将對象的 Mark Word 更新為指向 Lock Record 的指針(CAS 保證了即便多個線程競争,也隻有一個能更新成功),如果更新動作成功了,那麼目前線程就擷取到了該對象的鎖,同時會将 Mark Word 中的鎖标志位置為 00。是以,我們說線程擷取到了輕量級鎖,就等同于下面條件成立:

目前線程成功将 Mark Word 中的内容(除了最後兩位)修改為了指向線程堆棧中鎖記錄的指針

如果線程更新 Mark Word 失敗,會嘗試自旋來獲得鎖(就是執行循環,不斷嘗試擷取的鎖),當自旋擷取鎖失敗了之後,對象鎖就由輕量級鎖更新為重量級鎖,失敗線程就會被阻塞。同時,Mark Word 的記錄值會修改為指向互斥量(重量級鎖)的指針,Mark Word 的鎖記錄标志位也會置為 10.

釋放

輕量級的解鎖過程也是通過 CAS 操作來完成的,如果對象的 Mark Word 中仍然指向目前線程堆棧中的 Lock Record,就使用 CAS 操作将 Mark Word 的備份值複制回去,如果複制成功,就将鎖狀态的标志位置為 01,如果複制失敗,說明目前的輕量級鎖已經膨脹為重量級鎖了,那麼在釋放鎖的同時,要喚醒正在等待的線程。

如果存在大量的競争,除了互斥量的開銷,還額外發生了 CAS 操作,是以有競争的情況下,輕量級鎖反而更慢。

下面是鎖轉換的狀态圖:

synchronized實戰

自旋鎖