1. 程式人生 > >Java併發學習筆記 —— 淺析Java中的鎖

Java併發學習筆記 —— 淺析Java中的鎖

引言

在多執行緒環境下,為了保證共享變數的原子性操作,我們需要鎖來保證資源的獨佔;在資料庫連線等資源不足的情況下,我們需要控制獲取連線的資源數以防出現異常;還有一些情況下,我們需要多個執行緒任務完成的條件滿足後再繼續程式……在以上的種種情況,我們都需要使用鎖,讓我們的程式按照我們的預期執行。

本篇文章主要分為三個部分,第一個部分簡單介紹目前Java中鎖的區別,第二部分會介紹最簡單的鎖,也是Java的保留字——Synchronized。第三部分則會介紹基於同步器的鎖,也就是java.util.concurrent包含的鎖。

Java中鎖的分類

獨佔鎖與共享鎖

獨佔的意思是隻是否只能有一個執行緒擁有鎖,當鎖被執行緒獨佔了之後,其它再想要獲取鎖的執行緒就只能等待。等待的機制有自旋等待以及阻塞等待。自旋等待的時候,執行緒會不斷迴圈查詢是否滿足獲取鎖的條件,這樣消耗CPU但是卻節省了執行緒上下文切換所需的時間,一般在同步塊執行速度較短時選擇使用。而阻塞等待則會導致執行緒從執行態轉變成堵塞態。

共享鎖則允許多個執行緒同時獲取鎖,一般在併發讀的時候採用共享鎖。當然也可以使用共享鎖來控制當前併發執行緒的數量,如引言中提到的在資料庫連線有限的情況下,就可以控制併發執行緒數與資料庫連線數一致。

可重入鎖和不可重入鎖

根據是否可以多次獲得同一個鎖,又有可重入鎖與不可重入鎖的區分。

Synchronized —— 不可重入的獨佔鎖

用法及簡介

Synchronized做為Java的保留字,有以下兩種用法。

  • 在方法上使用Synchronized

    public synchronized void sync() {
        int a = 1;
    }
    
  • 在特定的程式碼區域中使用Synchronized

    public void syncInBlock() {
        synchronized(this) {
            int a = 1;
        }
    }
    

但無論哪個地方使用Synchronized,其實都是在一個特定的物件上加鎖。如果是在靜態方法上加鎖,則是對Class物件進行了加鎖,而在方法上或者是方法腫的同步塊上加鎖,則是對相應的例項加鎖。

加鎖原理

我們將上述程式碼的Java檔案編譯後再使用javap -verbose *>*.txt命令進行反編譯,可以得到以下的位元組碼命令。通過位元組碼命令,我們能夠更加清晰地看到synchronized的加鎖原理。

public class com.java.SampleSynchronized
  minor version: 0
  major version: 49
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Class              #2             // com/java/SampleSynchronized
   #2 = Utf8               com/java/SampleSynchronized
   #3 = Class              #4             // java/lang/Object
   #4 = Utf8               java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Methodref          #3.#9          // java/lang/Object."<init>":()V
   #9 = NameAndType        #5:#6          // "<init>":()V
  #10 = Utf8               LineNumberTable
  #11 = Utf8               LocalVariableTable
  #12 = Utf8               this
  #13 = Utf8               Lcom/java/SampleSynchronized;
  #14 = Utf8               sync
  #15 = Utf8               a
  #16 = Utf8               I
  #17 = Utf8               syncInBlock
  #18 = Utf8               SourceFile
  #19 = Utf8               SampleSynchronized.java
{
  public com.java.SampleSynchronized();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #8                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/java/SampleSynchronized;

  public synchronized void sync();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED //因為Synchronized在方法上使用,因此方法的ACC_SYNCHRONIZED標誌位被設定成1
    Code:
      stack=1, locals=2, args_size=1
         0: iconst_1
         1: istore_1
         2: return
      LineNumberTable:
        line 5: 0
        line 6: 2
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       3     0  this   Lcom/java/SampleSynchronized;
            2       1     1     a   I

  public void syncInBlock();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter //加鎖,然後進入同步區
         4: iconst_1
         5: istore_2
         6: aload_1
         7: monitorexit //正常執行完畢,解鎖
         8: goto          14 //程式返回
        11: aload_1
        12: monitorexit //異常程式碼處理,執行緒丟擲異常的時候,釋放鎖
        13: athrow
        14: return
      Exception table:
         from    to  target type
             4     8    11   any
            11    13    11   any
      LineNumberTable:
        line 9: 0
        line 10: 4
        line 9: 6
        line 12: 14
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      15     0  this   Lcom/java/SampleSynchronized;
}

從上面我們可以看出,對於使用在方法上的sychronized,在方法的flags上會多出一個ACC_SYNCHRONIZED的flag,以標誌這個方法是必須要同步訪問的。而在方法內宣告的同步塊,則會有monitorentermonitorexit來完成加鎖和解鎖。

實際上,monitor也就是加在物件上的鎖,如果執行monitorenter的時候發現無法獲取物件上的鎖,執行緒就會對鎖進行等待。而具體的加鎖/等待方式又會因為鎖的型別不同而有所不同,包括有偏向鎖,輕量級鎖和重量級鎖。對於這些鎖的實現在這裡就不展開描述了。

ReentrantLock——使用同步器的鎖

除了Synchronized之外,JDK還提供了可重入的獨佔鎖——ReentrantLock以及可重入的共享鎖——ReentrantReadWriteLock。因為這兩個鎖的實現都是基於同步器的,因此這裡以ReentrantLock為例,介紹基於同步器的鎖。

基本用法

ReentrantLock實現了java.util.concurrent.locks.Lock介面。其主要方法如下所示:

public void lock(){} //獲取鎖,若不成功執行緒會被阻塞

public boolean tryLock() {} //嘗試獲取鎖,獲取成功返回true,獲取失敗返回false,不阻塞執行緒

public void unlock() {} //釋放鎖

public final boolean isFair() {} //公平鎖還是非公平鎖,此處暫時略過

對於鎖的使用者而言,我們只要瞭解了這些介面,就能夠使用鎖了。最簡單的用法如下所示:

ReentrantLock lock = new ReentrantLock();

//執行緒阻塞式獲取鎖  
try{
    lock.lock();
    ...sync code...
} finally {
    lock.unlock();
}

//執行緒非阻塞式獲取鎖
try{
    while(lock.tryLock()){}
} finally {
    lock.unlock();
}

加鎖原理

通過檢視ReentrantLock的原始碼,我們能夠發現,在ReentrantLock中,還存在著一個內部抽象類Sync,而其具體實現則有FairSyncNonfairSyncSync繼承了AbstractQueuedSynchronizer。而AbstractQueuedSynchronizer就是我們常說的同步器。

當我們檢視加鎖以及釋放鎖的原始碼的時候,不難發現,所有的加鎖/釋放鎖的操作,最後都會委託給同步器。也就是說,同步器才是真正加鎖的實現者。=

具體呼叫過程(加鎖)如下圖所示:

Created with Raphaël 2.1.0lock.locksync.locksync.acquire(1)sync.tryAcquire(1)get the lockaddToWaitingQueueto be waked upinterrupt threadyesnoyesno

而觀察AbstractQueuedSynchronizer我們可以發現,在加鎖過程中,除了tryAcquire方法之外,其它方法都被final修飾,或者為私有方法。

事實上,抽象同步器已經實現了同步器大部分的功能,包括執行緒的等待與喚醒操作,同步狀態原子更新等。而我們需要做的,就是在獲取鎖以及釋放鎖的時候,完成對同步狀態的管理

同步狀態是如何管理的呢?是通過同步器中的一個volatile變數,state來管理的。要修改/獲取state的值,有三個非常重要的方法。

protected final int getState()//獲取state的值

protected final void setState() //非原子性地修改state的值

protected final boolean compareAndSetState() //原子性修改state的值,適用於多執行緒競爭時

state可以表明當前有多少個執行緒擁有鎖,也可以表明當前還有多少個執行緒可以申請鎖,這一切,都取決於我們對同步器中兩個關鍵方法,tryAcqure以及tryRelease的實現方式。

然後,我們以ReentrantLock為例,研究其中重寫的tryAcquire方法。

    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {//c為0,說明當前沒有執行緒獲得鎖
            if (!hasQueuedPredecessors()  //確保沒有執行緒在排隊
                && compareAndSetState(0, acquires)) { //原子性修改同步器狀態
                setExclusiveOwnerThread(current); //設定當前執行緒為獨佔執行緒
                return true; //加鎖成功
            }
        }
        else if (current == getExclusiveOwnerThread()) { //若擁有鎖的執行緒為當前執行緒,則修改同步器狀態,並返回加鎖成功
            int nextc = c + acquires;
            if (nextc < 0)
                throw new Error("Maximum lock count exceeded");
            setState(nextc); //因為是當前執行緒擁有鎖,所以可以直接修改同步器狀態(其它執行緒此時已經不可能修改同步器狀態,不會出現衝突)
            return true;
        }
        return false; //加鎖失敗
    }

在上面的程式碼中我們能夠知道,state為0則表示當前沒有執行緒獲取鎖,而state大於0的時候,則代表有執行緒持有鎖,因為ReentrantLock是可重入的獨佔鎖,因此當持鎖執行緒再申請鎖時,state也會進行加1。

以上,就是同步器加鎖的具體原理。使用同步器實現具體功能的還有CountDownLatchCyclicBarrierSemaphore

小結

本篇文章簡單介紹了Java中的鎖,包括鎖的不同類別,以及基於關鍵字Synchronized的鎖和基於同步器的鎖,並且分析了他們的實現原理。

參考文獻