深入瞭解ReentrantLock


在介紹Lock的時候,首先我們要先了解鎖的釋放和獲取,在java內存中究竟怎麼處理的。

鎖的釋放和獲取的內存語義

當線程釋放鎖時,java內存模型(以下簡稱JMM)會把線程對應的本地內存中的共享變量刷新到主內存中。在這裏插入圖片描述
當線程獲取鎖時,JMM會把該線程對應的本地內存置爲無效,從而使得監視器保護的臨界區代碼必須從主內存中讀取共享變量。
在這裏插入圖片描述
對比鎖釋放-獲取的內存語義與volatile寫-讀的內存語義可以看出:鎖釋放與volatile寫有相同的內存語義;鎖獲取與volatile讀有相同的內存語義。

總結:

  • 線程A釋放一個鎖,實際是線程A向接下來將要獲取這個鎖(線程B)的某個線程發出了(線程A對共享變量修改的)消息;
  • 線程B獲取一個鎖,實際是線程B接收了之前某個線程(線程A)發出的(在釋放這個鎖之前對共享變量所做修改的)消息;
  • 線程A釋放鎖,線程B獲取這個鎖,這個過程實質上是線程A通過主內存向線程B發送消息。

鎖內存語義的實現

藉助ReentrantLock源代碼,來分析鎖內存語義具體的實現機制。

    public void test(){
        int a = 0;
        Lock lock = new ReentrantLock();
        lock.lock();
        try{
            a++;
        }finally {
            lock.unlock();
        }
    }

ReentrantLock中,調用lock()方法獲取鎖;調用unlock()方法釋放鎖。

ReentrantLock的實現依賴於Java同步器框架AbstractQueuedSynchronizer(簡稱之爲AQS,AQS使用一個整型的volatile變量(命名爲state)來維護同步狀態,volatile修飾的state保證了變量可見性,volatile變量是ReentrantLock內存語義實現的關鍵。

AQS

AQS是用來構建鎖或者其他同步組件的基礎框架,它使用了一個int成員變量表示同步狀態,通過內置的FIFO隊列來完成資源獲取線程的排隊工作。
AQS的主要使用方式是繼承,子類通過繼承AQS並實現它的抽象方法來管理同步狀態,在抽象方法的實現過程中免不了要對同步狀態進行更改,這時就需要使用同步器提供的3個方法getState()setState(int newState)compareAndSetState(int expect,int update)來進行操作,因爲它們能夠保證狀態的改變是安全的。子類推薦被定義爲自定義同步組件的靜態內部類,同步器自身沒有實現任何同步接口,它僅僅是定義了若干同步狀態獲取和釋放的方法來供自定義同步組件使用,AQS既可以支持獨佔式地獲取同步狀態,也可以支持共享式地獲取同步狀態,這樣就可以方便實現不同類型的同步組件(ReentrantLockReentrantReadWriteLockCountDownLatch等)。
AQS是實現鎖(也可以是任意同步組件)的關鍵,在鎖的實現中聚合同步器,利用AQS實現鎖的語義。可以這樣理解二者之間的關係:鎖是面向使用者的,它定義了使用者與鎖交互的接口(比如可以允許兩個線程並行訪問),隱藏了實現細節;AQS面向的是鎖的實現者,它簡化了鎖的實現方式,屏蔽了同步狀態管理、線程的排隊、等待與喚醒等底層操作。鎖和AQS很好地隔離了使用者和實現者所需關注的領域。
AQS的設計是基於模板方法模式的,寫同步器指定的方法時,需要使用同步器提供的如下3個方法來訪問或修改同步狀態

  1. getState():獲取當前同步狀態。
  2. setState(int newState):設置當前同步狀態。
  3. compareAndSetState(int expect,int update):使用CAS設置當前狀態,該方法能夠保證狀態設置的原子性。

AQS的模板方法提供了獨佔式獲取與釋放同步狀態、共享式獲取與釋放同步狀態。

獨佔鎖

獨佔鎖就是在同一時刻只能有一個線程獲取到鎖,而其他獲取鎖的線程只能處於同步隊列中等待,只有獲取鎖的線程釋放了鎖,後繼的線程才能夠獲取鎖。

我們下邊要說的無論是公平鎖或非公平鎖,都是通過ReentrantLock去定義的,。都是獨佔式鎖。

lock() 獲取鎖(公平鎖)

ReentrantLock分爲公平鎖和非公平鎖,也就是FairSyncNonfairSync,同時繼承自Sync

  • 如果鎖未被另一個線程持有,則獲取該鎖並立即返回,將鎖持有計數設置爲1。
  • 如果當前線程已經持有鎖,那麼持有計數將增加1,並且方法立即返回。
  • 如果鎖被另一個線程持有,則當前線程將因線程調度而禁用,並處於休眠狀態,直到獲得鎖爲止,此時鎖持有計數設置爲1。
	//公平鎖的同步對象
    static final class FairSync extends Sync {
        final void lock() {
            acquire(1);
        }
    }

Sync繼承AQS,而公平鎖FairSync繼承自Sync。

acquire(int arg) 主要負責同步狀態獲取、節點構造、加入同步隊列以及在同步隊列中自旋等待的相關工作。其主要邏輯是:首先調用自定義AQS實現的tryAcquire(int arg)方法,該方法保證線程安全的獲取同步狀態,如果同步狀態獲取失敗,則構造同步節點(獨佔式Node.EXCLUSIVE,同一時刻只能有一個線程成功獲取同步狀態)並通過addWaiter(Node node)方法將該節點加入到同步隊列的尾部,最後調用acquireQueued(Node node,int arg)方法,使得該節點以“死循環”的方式獲取同步狀態。如果獲取不到則阻塞節點中的線程,而被阻塞線程的喚醒主要依靠前驅節點的出隊或阻塞線程被中斷來實現。

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

通過調用AQS的acquire(int arg)方法可以獲取同步狀態,該方法對中斷不敏感,也就是由於線程獲取同步狀態失敗後進入同步隊列中,後續對線程進行中斷操作時,線程不會從同步隊列中移出。

    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            //Node爲空時,頭部添加
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                //尾部添加
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

上述代碼compareAndSetTail(Node expect,Node update)方法來確保節點能夠被線
程安全添加。在enq(final Node node)方法中,AQS通過“死循環”來保證節點的正確添加,在“死循環”中只有通過CAS將節點設置成爲尾節點之後,當前線程才能從該方法返回,否則,當前線程不斷地嘗試設置。

節點進入同步隊列之後,就進入了一個自旋的過程,每個節點(或者說每個線程)都在自省地觀察,當條件滿足,獲取到了同步狀態,就可以從這個自旋過程中退出,否則依舊留在這個自旋過程中(並會阻塞節點的線程)

公平鎖的tryAcquire方法,真正的加鎖
除非遞歸調用或沒有服務生或是第一個,否則不要授予訪問權限。

    protected final boolean tryAcquire(int acquires) {
          final Thread current = Thread.currentThread();
          //獲取鎖的開始,首先讀通過volatile修飾的state變量
          int c = getState();
          if (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;
      }

AQS的acquireQueued方法

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

acquireQueued(final Node node,int arg)方法中,當前線程在“死循環”中嘗試獲取同步狀態,而只有前驅節點是頭節點才能夠嘗試獲取同步狀態。
原因:

  1. 頭節點是成功獲取到同步狀態的節點,而頭節點的線程釋放了同步狀態之後,將會喚醒其後繼節點,後繼節點的線程被喚醒後需要檢查自己的前驅節點是否是頭節點。
  2. 維護同步隊列的FIFO原則

由於非首節點線程前驅節點出隊或者被中斷而從等待狀態返回,隨後檢查自
己的前驅是否是頭節點,如果是則嘗試獲取同步狀態。
acquire(int arg)方法調用流程,同時也是獨佔式同步狀態獲取流程,大致是這樣的:
在這裏插入圖片描述
前驅節點爲頭節點且能夠獲取同步狀態的判斷條件和線程進入等待狀態是獲
取同步狀態的自旋過程。當同步狀態獲取成功之後,當前線程從acquire(int arg)方法返回,如果對於鎖這種併發組件而言,代表着當前線程獲取了鎖

同步狀態
private volatile int state;

使用公平鎖時,加鎖方法lock()調用流程:

  1. ReentrantLock:lock()
  2. FairSync:lock()
  3. AbstractQueuedSynchronizer:acquire(int arg)
  4. ReentrantLock:tryAcquire(int acquires)

unlock()解鎖

當前線程獲取同步狀態並執行了相應邏輯之後,就需要釋放同步狀態,使得後續節點能夠繼續獲取同步狀態。通過調用AQS的release(int arg)方法可以釋放同步狀態,該方法在釋放了同步狀態之後,會喚醒其後繼節點(進而使後繼節點重新嘗試獲取同步狀態)

//釋放鎖
public class ReentrantLock implements Lock, java.io.Serializable{
    public void unlock() {
       sync.release(1);
    }
}
  • 如果當前線程是這個鎖的持有者,那麼計數遞減。
  • 如果當前計數爲零,則釋放鎖。
  • 如果當前線程不是此線程的持有者,則拋異常IllegalMonitorStateException
//以獨佔的方式釋放
//tryRelease()方法如果返回true,則通過解鎖一個或多個線程來實現。
public final boolean release(int arg) {
     if (tryRelease(arg)) {
         Node h = head;
         if (h != null && h.waitStatus != 0)
             unparkSuccessor(h);
         return true;
     }
     return false;
 }

該方法執行時,會喚醒頭節點的後繼節點線程,unparkSuccessor(Node node)方法使用LockSupport來喚醒處於等待狀態的線程

 //釋放鎖
 protected final boolean tryRelease(int releases) {
 	//volatile修飾state 減去需要釋放的鎖
     int c = getState() - releases;
     if (Thread.currentThread() != getExclusiveOwnerThread())
         throw new IllegalMonitorStateException();
     boolean free = false;
     //等於0,說明沒有需要同步的,鎖釋放成功
     if (c == 0) {
         free = true;
         setExclusiveOwnerThread(null);
     }
     //釋放鎖最後,寫volatile變量state
     setState(c);
     return free;
 }

在使用公平鎖時,解鎖方法unlock()調用過程:

  1. ReentrantLock:unlock()
  2. AbstractQueuedSynchronizer:release(int arg)
  3. Sync:tryRelease(int releases)

公平鎖在釋放鎖的最後寫volatile變量state,在獲取鎖時首先讀這個volatile變量。根據volatilehappens-before規則,釋放鎖的線程在寫volatile變量之前可見的共享變量,在獲取鎖的線程讀取同一個volatile變量後將立即變得對獲取鎖的線程可見。

非公平鎖的釋放和公平鎖是一樣的,我們來看下非公平鎖的獲取。

lock() 獲取鎖(非公平鎖)

    static final class NonfairSync extends Sync {
    
        final void lock() {
        	//通過CAS,期望值爲0 ,更新值 1
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

        protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
    }
     final boolean nonfairTryAcquire(int acquires) {
         final Thread current = Thread.currentThread();
         int c = getState();
         if (c == 0) {
             if (compareAndSetState(0, acquires)) {
                 setExclusiveOwnerThread(current);
                 return true;
             }
         }
         else if (current == getExclusiveOwnerThread()) {
             int nextc = c + acquires;
             if (nextc < 0) // overflow
                 throw new Error("Maximum lock count exceeded");
             setState(nextc);
             return true;
         }
         return false;
     }

如果當前狀態值等於期望值,則以原子方式將同步狀態設置爲給定的更新值。

    protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

使用非公平鎖時,加鎖方法lock()調用流程

  1. ReentrantLock:lock()
  2. NonfairSync:lock()
  3. AbstractQueuedSynchronizer:compareAndSetState(int expect,int update)

編譯器不會對volatile讀與volatile讀後面的任意內存操作重排序;編譯器不會對volatile寫與volatile寫前面的任意內存操作重排序。組合這兩個條件,意味着爲了同時實現volatile讀和volatile寫的內存語義,編譯器不能對CAS與CAS前面和後面的任意內存操作重排序。

公平鎖和非公平鎖的區別

現在對公平鎖和非公平鎖做個總結。

  • 公平性與否是針對獲取鎖而言的,如果一個鎖是公平的,那麼鎖的獲取順序就應該符合請求的絕對時間順序,也就是FIFO。
  • 公平鎖和非公平鎖釋放時,最後都要寫一個volatile變量state。
  • 公平鎖獲取時,首先會去讀volatile變量。
  • 非公平鎖獲取時,首先會用CAS更新volatile變量,這個操作同時具有volatile讀和volatile寫的內存語義,只要CAS設置同步狀態成功,則表示當前線程獲取了鎖,而公平鎖則不同。
  • 公平性鎖每次都是從同步隊列中的第一個節點獲取到鎖,而非公平性鎖會出現一個線程連續獲取鎖的情況。
  • 公平性鎖保證了鎖的獲取按照FIFO原則,而代價是進行大量的線程切換。非公平性鎖雖然可能造成線程“飢餓”,但極少的線程切換,保證了其更大的吞吐量。

公平鎖tryAcquire,非公平鎖nonfairTryAcquire,唯一不同的位置爲判斷條件多了hasQueuedPredecessors()方法,即加入了同步隊列中當前節點是否有前驅節點的判斷,如果該方法返回true,則表示有線程比當前線程更早地請求獲取鎖,因此需要等待前驅線程獲取並釋放鎖之後才能繼續獲取鎖。

ReentrantLock的分析可以看出,鎖釋放-獲取的內存語義的實現至少有下面兩種
方式。

  1. 利用volatile變量的寫-讀所具有的內存語義。
  2. 利用CAS所附帶的volatile讀和volatile寫的內存語義。

tryLock()

獲取鎖(如果有)並立即返回true。 如果鎖不可用,則此方法將立即返回值false

    public boolean tryLock() {
        return sync.nonfairTryAcquire(1);
    }
    
    final boolean nonfairTryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            if (compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }

該方法增加了再次獲取同步狀態的處理邏輯:通過判斷當前線程是否爲獲取鎖的線程來決定獲取操作是否成功,如果是獲取鎖的線程再次請求,則將同步狀態值進行增加並返回true,表示獲取同步狀態成功。

成功獲取鎖的線程再次獲取鎖,只是增加了同步狀態值,這也就要求ReentrantLock在釋放同步狀態時減少同步狀態值,釋放鎖時tryRelease()方法,上邊已經貼過源碼

    protected final boolean tryRelease(int releases) {
        int c = getState() - releases;
        if (Thread.currentThread() != getExclusiveOwnerThread())
            throw new IllegalMonitorStateException();
        boolean free = false;
        if (c == 0) {
            free = true;
            setExclusiveOwnerThread(null);
        }
        setState(c);
        return free;
    }

如果該鎖被獲取了n次,那麼前(n-1)次tryRelease(int releases)方法必須返回false,而只有同步狀態完全釋放了,才能返回true。可以看到,該方法將同步狀態是否爲0作爲最終釋放的條件,當同步狀態爲0時,將佔有線程設置爲null,並返回true,表示釋放成功。

獨佔式同步狀態獲取和釋放總結

在獲取同步狀態時,AQS維護一個同步隊列,獲取狀態失敗的線程都會被加入到隊列中並在隊列中進行自旋;移出隊列(或停止自旋)的條件是前驅節點爲頭節點且成功獲取了同步狀態。在釋放同步狀態時,AQS調用tryRelease(int arg)方法釋放同步狀態,然後喚醒頭節點的後繼節點。

共享式鎖

上邊我們提到獨佔式鎖和共享式鎖,講了那麼多,現在知道了ReentrantLock是獨佔式鎖,那什麼是共享式鎖呢?它又是這麼獲取和釋放的呢?

在java併發編程中,ReentrantReadWriteLock爲共享式鎖,我們一般也稱爲讀寫鎖。
但也僅僅ReadLock 是共享鎖,而WriteLock 依舊是獨佔鎖

讀鎖是一個支持重進入的共享鎖,它能夠被多個線程同時獲取,在沒有其他寫線程訪問(或者寫狀態爲0)時,讀鎖總會被成功地獲取,而所做的也只是(線程安全的)增加讀狀態。如果當前線程已經獲取了讀鎖,則增加讀狀態。如果當前線程在獲取讀鎖時,寫鎖已被其他線程獲取,則進入等待狀態。

    ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
    ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();

共享式獲取與獨佔式獲取最主要的區別在於:同一時刻能否有多個線程同時獲取到同步狀態。以文件的讀寫爲例,如果一個程序在對文件進行讀操作,那麼這一時刻對於該文件的寫操作均被阻塞,而讀操作能夠同時進行。寫操作要求對資源的獨佔式訪問,而讀操作可以是共享式訪問,兩種不同的訪問模式在同一時刻對文件或資源的訪問情況。
在這裏插入圖片描述
左半部分,共享式訪問資源時,其他共享式的訪問均被允許,而獨佔式訪問被
阻塞,右半部分是獨佔式訪問資源時,同一時刻其他訪問均被阻塞。

共享鎖獲取

獨佔式獲取鎖是acquire(int arg)方法,共享式鎖是acquireShared(int arg)

    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }

tryAcquireShared(int unused)方法中,如果其他線程已經獲取了寫鎖,則當前線程獲取讀鎖失敗,進入等待狀態。如果當前線程獲取了寫鎖或者寫鎖未被獲取,則當前線程(線程安全,依靠CAS保證)增加讀狀態,成功獲取讀鎖。

獨佔鎖是tryAcquire,而共享鎖是tryAcquireShared,同時它也分爲公平鎖和非公平鎖,他們的區別也是hasQueuedPredecessors()方法,我們就已公平鎖爲例吧

  	protected int tryAcquireShared(int acquires) {
        for (;;) {
            if (hasQueuedPredecessors())
                return -1;
            int available = getState();
            int remaining = available - acquires;
            if (remaining < 0 ||
                compareAndSetState(available, remaining))
                return remaining;
        }
    }

acquireShared(int arg)方法中,AQS調用tryAcquireShared(int arg)方法嘗試獲取同步狀態,tryAcquireShared(int arg)方法返回值爲int類型,當返回值大於等於0時,表示能夠獲取到同步狀態。因此,在共享式獲取的自旋過程中,成功獲取到同步狀態並退出自旋的條件就是tryAcquireShared(int arg)方法返回值大於等於0。

    private void doAcquireShared(int arg) {
    	//創建共享Node
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    if (r >= 0) {
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

在doAcquireShared(int arg)方法的自旋過程中,如果當前節點的前驅爲頭節點時,嘗試獲取同步狀態,如果返回值大於等於0,表示該次獲取同步狀態成功並從自旋過程中退出。

共享鎖的釋放

    public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
    }

該方法在釋放同步狀態之後,將會喚醒後續處於等待狀態的節點。對於能夠支持多個線程同時訪問的併發組件(比如Semaphore),它和獨佔式主要區別在於tryReleaseShared(int arg)方法必須確保同步狀態(或者資源數)線程安全釋放,一般是通過循環和CAS來保證的,因爲釋放同步狀態的操作會同時來自多個線程

參考書籍:java併發編程的藝術

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章