Java-多執行緒中的鎖

2020-10-14 11:00:30


當一個資料被多個執行緒所共同使用,且執行緒並行執行時,我們需要保證保證該資料的準確性,既一個執行緒對資料的操作不會對另一個執行緒產生不合理的影響。
實現的手段基本上是對資料加鎖,當執行緒要對資料進行操作時必須獲得鎖後再進行操作。鎖可分為樂觀鎖和悲觀鎖。

樂觀鎖

樂觀鎖,總是樂觀地假設最好的情況,每次去拿資料的時候都認為別人不會修改這個資料,所以不會上鎖,只會要對資料進行更新時判斷一下在此期間(拿到資料到更新的期間)別人有沒有去更改這個資料,可以使用版本號機制和CAS演演算法實現。

CAS

  • CAS(Compare And Swap)是一種常見的「樂觀鎖」,大部分的CPU都有對應的組合指令,它有三個運算元:記憶體地址V,舊值A,新值B。只有當前記憶體地址V上的值是A,B才會被寫到V上,否則操作失敗。
  • Java從5.0開始引入了對CAS的支援,與之對應的是 java.util.concurrent.atomic 包下的AtomicInteger、AtomicReference等類,它們提供了基於CAS的讀寫操作和並行環境下的記憶體可見性。

以AtomicInteger為例看看底層是怎麼進行操作的

AtomicInteger integer=new AtomicInteger(123);
int a=integer.addAndGet(321);//+321
System.out.println(a);//結果為444

上面編寫了一個範例,建立一個AtomicInteger物件,呼叫它的addAndGet方法,此方法是加上一個數並返回相加後的結果。然後我們來看看這個方法的原始碼。

public final int addAndGet(int delta) {
    return U.getAndAddInt(this, VALUE, delta) + delta;
}

可以看到這個方法的返回值呼叫了U(Unsafe物件)的getAndInt方法來獲取當前物件在記憶體中的值
下面是getAndInt方法的原始碼

@HotSpotIntrinsicCandidate
public final int getAndAddInt(Object o, long offset, int delta) {
    int v;
    do {
        v = getIntVolatile(o, offset);//獲取物件中offset偏移地址對應的整型field的值
    } while (!weakCompareAndSetInt(o, offset, v, v + delta));
    return v;
}

可以看到邏輯就是若weakCompareAndSetInt的返回值為false則不斷的獲取整形值field
下面是weakCompareAndSetInt的原始碼

@HotSpotIntrinsicCandidate
public final boolean weakCompareAndSetInt(Object o, long offset,
                                          int expected,
                                          int x) {
    return compareAndSetInt(o, offset, expected, x);//比較當前記憶體中的值和期望值x是否相等
}

悲觀鎖

總是假設最壞的情況,每次去拿資料的時候都認為別人會修改,所以每次在拿資料的時候都會上鎖,這樣別人想拿這個資料就會阻塞直到它拿到鎖。Java中synchronized和ReentrantLock等獨佔鎖就是悲觀鎖思想的實現。

synchronized

Java中的關鍵字,是由JVM來維護的。是JVM層面的鎖。
是非公平鎖。

  • 同步程式碼塊
    sychronized可用於修飾一個程式碼塊,當執行緒想要執行程式碼塊中的程式碼時必須先取得鎖物件
    格式:synchronized(鎖物件){…}
  • 同步方法
    在方法的返回值前加上synchronized關鍵字,執行緒想要執行改方法必須獲得鎖。
    修飾實體方法:獲得的鎖預設是this(當前物件)。
    修飾靜態方法:獲得的鎖預設是當前類。

synchronized的侷限性

如果獲取鎖的執行緒由於要等待一些原因(比如呼叫sleep方法)被阻塞了,但是又沒有釋放鎖,其他執行緒便只能乾巴巴地等待。
當有多個執行緒讀寫檔案時,讀操作和寫操作會發生衝突現象,寫操作和寫操作會發生衝突現象,但是讀操作和讀操作不會發生衝突現象。採用synchronized則會導致一個執行緒在進行讀操作,其他執行緒會等待此執行緒讀完。
綜上所述,下synchronized十分的影響效率,上述的這些問題通過使用Lock可以解決。

Lock

是JDK5以後才出現的介面。使用Lock是呼叫對應的API。是API層面的鎖
在建立物件時從構造方法傳入true可建立公平鎖,不傳入預設是不公平鎖。
相較synchronized的自動獲得和釋放鎖,Lock需要手動獲得和釋放鎖。

  • 獲取鎖
    • Lock()方法
      就是用來獲取鎖。如果鎖已被其他執行緒獲取,則進行等待。
    • tryLock()方法
      此表示用來嘗試獲取鎖,如果獲取成功,則返回true,如果獲取失敗(即鎖已被其他執行緒獲取),則返回false,這個方法會立即返回true或false。在拿不到鎖時不會一直在那等待。
    • tryLock(long time, TimeUnit unit)方法
      與tryLock()方法類似,區別在於這個方法在拿不到鎖時會等待一定的時間,在時間期限之內如果還拿不到鎖,就返回false。如果如果一開始拿到鎖或者在等待期間內拿到了鎖,則返回true。
    • lockInterruptibly()方法
      此方法優先考慮響應中斷,而不是響應鎖的獲取。也就是說如果執行緒獲取不到鎖則可以通過呼叫interrupt()方法中斷執行緒。
  • 獲取鎖
    • unLock()方法

Lock是一個介面,一般使用它的實現類ReentrantLock建立物件來獲取和釋放鎖。