1. 程式人生 > 其它 >Java之synchronized詳解

Java之synchronized詳解

前言

本文將對常用的synchronized圍繞常見的一些問題進行展開。以下為我們將圍繞的問題:

  • 樂觀鎖和悲觀鎖?
  • synchronized的底層是怎麼實現的?
  • synchronized可重入是怎麼實現的?
  • synchronized鎖升級?
  • synchronized是公平鎖還是非公平鎖?
  • synchronized和volatile對比有什麼區別?
  • synchronized在使用時有何需要注意事項?

注意:下面都是在JDK1.8中進行的。

樂觀鎖和悲觀鎖?

關於樂觀鎖和悲觀鎖的定義和使用場景,可以看《Mysql InnoDB之各類鎖》中,本質都是一樣的,這裡就不再贅述。

關於悲觀鎖,下面再進行介紹,synchronized和Lock都屬於悲觀鎖,下面我們來具體看看樂觀鎖。

樂觀鎖的實現-CAS

樂觀鎖的核心就是CAS(Compare And Swap-比較與交換,是一種不搶佔的同步方式),是一種無鎖演算法。CAS演算法涉及三個運算元:

  • 需要讀寫的記憶體值V。
  • 進行比較的值A。
  • 要寫入的新值B。

當前僅噹噹前記憶體值V等於值A時,才進行寫入新值B,有人會問我在比較相等後的同時更新了值V咋辦?寫入的新值B不是覆蓋了別人剛寫入的值嗎?是的比較和寫入需要保證是一個原子操作,這裡通過CPU的cmpxchg指令,去比較暫存器中的A和記憶體中的值V,如果相等的話就寫入B,如果不等的話就值V賦值給暫存器中的值A,如果想繼續自旋就繼續不想繼續可以丟擲相應錯誤。

下面我們來看看常見的AtomicInteger是如何自旋的。

AtomicInteger

一個可以被原子更新的int值,關於原子變數屬性描述具體可以參考{@link java.util.concurrent.automic}包。AutomicInteger用於原子遞增計數器等應用程式中,不能被使用替代Integer。然而這個類擴充套件了Number允許被一些處理數值的工具或者公共程式碼統一訪問。

欄位和建構函式

package java.util.concurrent.atomic;

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // 設定使用Unsafe.compareAndSwapInt進行更新。
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    // 獲得value物件記憶體分佈中的偏移量用於找到value
    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    // 保證記憶體可見效和禁止指令重排。
    private volatile int value;

    public AtomicInteger(int initialValue) {
        value = initialValue;
    }

    /**
     * 初始值為0
     */
    public AtomicInteger() {
    }

incrementAndGet

/** 
 * 以原子方式將當前值遞增1 
 * @return 更新後的值 
 */
public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
複製程式碼

var1:為AtomicInteger物件,用於unsafe結合valueOffset獲得物件中的最新的value。
var2:value值在AtomicInteger物件中偏移量。
var4:增加的值為1。

package sun.misc;
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        //獲得AutomicInteger的value值
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

樂觀鎖的缺點

  • 如果併發比較高,CAS一直比較自旋,將會一直佔用CPU,如果自旋的執行緒多了CPU就會飆升。
  • 只能保證一個共享變數的原子操作。對一個共享變數執行操作時,CAS能保證原子操作,但是對多個共享變數操作時,CAS時無法保證操作的原子性的。
    • java從1.5開始JDK提供了AtomicReference類來保證引用之間的原子性,可以把多個變數放在一個物件中來進行CAS操作。

synchronized的底層是怎麼實現的?

sychronized是通過物件頭部的Mark Word中的鎖標識+monitor實現的。

java物件頭

物件頭由Mark Word和Klass組成,在沒有壓縮指標的時候都佔8個位元組。

  • Mark Word:標記欄位-執行時資料,如雜湊碼、GC資訊以及鎖資訊。
  • Klass:物件鎖代表的類的元資料指標。

鎖標誌位+是否是偏向鎖(biased_lock)共同表示物件的幾種狀態

monitor

synchronized通過Monitor來實現執行緒同步和協作。

  • 同步依賴的是作業系統的Mutex(互斥鎖量)只有擁有互斥量的執行緒才能進入臨界區,不擁有的只能阻塞等待,會維護一個阻塞的佇列。
  • 協作依賴的是synchronized持有的物件,物件可以讓多個執行緒方便同步,還可以通過物件呼叫wait方法釋放鎖讓執行緒進入等待佇列,等其他執行緒呼叫物件的notify和notifyAll方法進行喚醒可以重新獲取鎖。

Monitor用來進行監聽鎖的持有和排程鎖的持有的。持有的物件可以理解為鎖的一個媒介,可以使用它方便操作同步和協作。

具體例子可以參考《Thread原始碼閱讀及相關問題》中的例子。

monitor這套監聽鎖和排程鎖包括使用的互斥量其實都是比較消耗資源的,所以使用它的成為“重量級鎖”。JDK 6中為了減少獲得鎖和釋放鎖帶來的效能消耗,引入了“偏向鎖”和“輕量級鎖”,下面我們分別會進行分配介紹。

無鎖

當物件頭中鎖標誌位為01,是否偏向鎖為0時表示使無鎖的狀態。想要在無鎖的時候實現同步可以使用上面樂觀鎖中實現-CAS。

偏向鎖

偏向鎖是一個鎖優化的產物,在物件頭中進行標記,表示有執行緒進入了臨界區,在只有一個執行緒訪問的時候既不用使用CAS也不用引入較重的monitor。

執行緒不會主動釋放偏向鎖,只有遇到其他執行緒進嘗試競爭偏向鎖時,需要等待全域性安全點(在這個時間點上沒有執行的位元組碼),它會首先暫停擁有偏向鎖的的執行緒,判斷它是否還活著,如果死亡了就恢復到無鎖狀態其他執行緒就可以佔用,如果還在臨界區就對鎖進行升級成"輕量鎖"。

每個執行緒進入臨界區的時候都會檢視物件頭鎖標識是否是偏向鎖,是偏向鎖的話,會判斷當前執行緒和物件頭中的執行緒id是同一個執行緒則直接進入,如果不是則進行CAS看是不是能比較替換成功(防止馬上就釋放了),如果沒成功就會暫停持有偏向鎖的執行緒,看執行緒是否已經不再用鎖了,如果沒用就釋放,給新進來的執行緒佔用,如果在用就進行鎖升級生成輕量鎖"。

可以通過JVM引數關閉偏向鎖:-XX:-UseBiasedLocking=false,那麼程式會預設進入輕量級鎖狀態。 筆者思考

可以發現在只有一個執行緒進入臨界區的時候確實能避免使用互斥量帶來的開銷,但是可以發現執行緒不會主動釋放偏向鎖。為啥不當有偏向鎖的時候離開臨界區進行釋放?還要等其他執行緒來的時候要等全域性點的時候嘗試對執行緒暫停之後再看該執行緒持有鎖的狀態?這些疑問我們考慮不全,可能是設計的問題,也可能是因為一些其他原因,我們對JVM原始碼不夠熟悉的情況下會比較費解。或許後面迭代會進行優化。

所以我們只需要明白一點:偏量鎖是一種鎖的優化,它本質上不是鎖,只是物件頭中進行了標記,如果沒有多執行緒併發訪問臨界區的時候可以減少開銷,如果出現多併發的時候會進行升級。

輕量級鎖

輕量鎖發生在偏向鎖升級或者-XX:-UseBiasedLocking=false的時候,執行緒在執行同步塊之前,JVM會現在當前執行緒的棧幀中建立用於儲存鎖記錄的空間,並將物件頭中的Mark Word複製到鎖記錄中,官方稱為Displaces Mark Word。然後執行緒嘗試使用CAS將對頭像中的Mark Word替換為之指向鎖記錄的指標。如果成功,當前執行緒獲得鎖,如果失敗將通過自旋來進行同步。

重量級鎖

重量鎖的鎖標誌位為10,就是上面介紹的monitor機制,開銷最大。

synchronized鎖升級?

如上面的synchronized的底層實現章節。

synchronized可重入是怎麼實現的?

我們先用程式碼證明下:

public class SynchronizedReentrantTest extends Father {
    public synchronized void doSomeThing1() {
        System.out.println("doSomeThing1");
        doSomeThing2();
    }

    public synchronized void doSomeThing2() {
        System.out.println("doSomeThing2");
        super.fatherDoSomeThing();
    }

    public static void main(String[] args) {
        SynchronizedReentrantTest synchronizedReentrantTest = new SynchronizedReentrantTest();
        synchronizedReentrantTest.doSomeThing1();
    }
}

class Father {
    public synchronized void fatherDoSomeThing() {
        System.out.println("fatherDoSomeThing");
    }
}
複製程式碼

輸出:

doSomeThing1
doSomeThing2
fatherDoSomeThing

說明synchronized是可重入的。

重量級鎖使用的是monitor物件中的計數字段來實現的,偏向鎖應該沒有隻有表示當前被那個執行緒持有,輕量鎖在每次進入的時候都會新增一個Lock Record來表示鎖的重入次數。

筆者思考

為啥偏向鎖不記錄重入次數,重入的時候只需要看是否是當前執行緒,物件頭中沒有地方存放次數,所以偏向鎖不會主動釋放(應該是判斷巢狀臨界區比較麻煩),需要另外一個執行緒來判斷當前執行緒是否活躍死亡了才釋放還會嘗試暫停持有的執行緒。這點其實不如輕量級鎖和重量級鎖。

synchronized是公平鎖還是非公平鎖?

非公平的,直接下面打飯的例子:

import lombok.SneakyThrows;

public class SyncUnFairLockTest {
    //食堂
    private static class DiningRoom {
        //獲取食物
        @SneakyThrows
        public void getFood() {
            System.out.println(Thread.currentThread().getName() + ":排隊中");
            synchronized (this) {
                System.out.println(Thread.currentThread().getName() + ":@@@@@@打飯中@@@@@@@");
                Thread.sleep(200);
            }
        }
    }

    public static void main(String[] args) {
        DiningRoom diningRoom = new DiningRoom();
        //讓5個同學去打飯
        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                diningRoom.getFood();
            }, "同學編號:00" + (i + 1)).start();
        }
    }
}

輸出:

同學編號:001:排隊中
同學編號:001:@@@@@@打飯中@@@@@@@
同學編號:005:排隊中
同學編號:003:排隊中
同學編號:004:排隊中
同學編號:002:排隊中
同學編號:002:@@@@@@打飯中@@@@@@@
同學編號:004:@@@@@@打飯中@@@@@@@
同學編號:003:@@@@@@打飯中@@@@@@@
同學編號:005:@@@@@@打飯中@@@@@@@

注意到這裡我加了sleep,因為對於公平鎖來說無所謂,先來的肯定先執行,但是非公平鎖時後面來的執行緒會先進行嘗試獲得鎖,獲取不到再進入佇列,這樣就能避免同一進入佇列再被CPU喚醒,能提高效率,但是非公平鎖會出現餓死的情況。

synchronized和volatile對比有什麼區別?

都能保證可見效,synchronized因為是鎖所以能保證原子性。

可見效主要指的是執行緒共享時工作記憶體和主記憶體能否及時同步。

JMM關於synchronized的兩個規定:

  • 執行緒解鎖前,必須把共享變數的最新值重新整理到主記憶體中。
  • 執行緒加鎖時,將清空工作記憶體中共享變數的值,從而使變數共享時,需要從主記憶體中重新讀取最新的值。