深入理解Java併發編程之Lock和AQS

本文轉自個人掘金博客:https://juejin.im/post/5ee37be951882543435a2747

本文主要爲《Java併發編程的藝術》第三章的讀書記錄筆記

Lock接口

Lock vs synchronized

Java SE 5之後,併發包中新增了Lock接口(以及相關實現類)用來實現鎖功能,它提供了與synchronized關鍵字類似的同步功能。但是,它們卻有以下不同:

synchronized:使用synchronized的關鍵字將會隱式地獲取和釋放鎖。同時,使用什麼類型的鎖(偏向,輕量級鎖,重量級鎖)以及鎖的具體實現都是由JVM底層實現

Lock接口:Lock接口相關實現類對於鎖的獲取和釋放需要顯示進行。同時,怎樣獲取和釋放是由開發者(包括JDK源碼)自行決定的。這樣的話,它就擁有了鎖獲取與釋放的可操作性、非阻塞地獲取鎖可中斷的獲取鎖以及超時獲取鎖等多種synchronized關鍵字所不具備的同步特性。

Lock接口API

Lock是一個接口,它定義了鎖獲取和釋放的基本操作,Lock的API如下:

  1. void lock():阻塞獲取鎖。如果鎖獲取不到,當前線程就無法進行線程調度只能等待直到獲取到鎖。
  2. void lockInterruptibly() throws InterruptedException:可中斷的阻塞獲取鎖。和lock()方法的不同是該方法在鎖的獲取中可以中斷當前線程。
  3. boolean tryLock():嘗試非阻塞地獲取鎖。調用該方法後立即返回,如果能夠獲取鎖則返回true,否則返回false。
  4. boolean tryLock(long time, TimeUnit unit) throws InterruptedException:阻塞超時獲取鎖。當前線程在以下3種情況會返回:
    • 當前線程在超時時間內獲得了鎖,返回true
    • 當前線程在超時時間內被中斷,拋出異常
    • 超時時間結束,返回false
  5. void unlock():釋放鎖。在Lock接口的實現類中,通常只有擁有鎖的線程才能釋放它。
  6. Condition newCondition():返回一個和當前鎖綁定Condition對象實例。

Lock接口的實現如ReentrantLock基本都是通過聚合了一個 隊列同步器(AQS)的子類來完成線程訪問控制的。

AbstractQueuedSynchronizer(AQS)

抽象類AbstractQueuedSynchronizer提供了一個基礎框架,它可以用來實現阻塞鎖和其他相關的依賴於FIFO等待隊列同步組件(比如Semaphore)。它的實現主要依賴於一個單一的原子int變量值state來表示同步狀態。繼承的子類必須重寫AQS的幾個protected修飾的用來改變同步狀態的方法以及基於這些狀態進行鎖獲取和釋放的方法。AQS類裏面的其他方法主要實現了排隊和阻塞的機制。繼承AQS的子類可能會維護一些其他的狀態值,但是隻能通過getState,setState以compareAndSetState這三個方法來原子地更新管理同步狀態的state值

繼承AQS的子類被推薦定義爲阻塞鎖或者同步組件實現類的靜態內部類。AQS自己本身沒有實現任何同步接口。相反地,它僅僅是定義了若干同步狀態的獲取和釋放方法來供阻塞鎖或者同步組件的使用,來實現子類的公共方法。

AQS既支持獨佔式獲取同步狀態,又支持共享式獲取同步狀態,也支持兩者模式具備。默認情況下是獨佔式獲取同步狀態。

  1. 獨佔模式指的是一旦有一個線程佔有,其他線程便無法佔有。
  2. 共享模式支持的是多線程佔有。

AQS並不關心這些不同模式之間的差異。不同模式下的等待線程共享同一個FIFO隊列。通常,AQS的實現子類要麼只支持獨佔模式,要麼只支持共享模式。但是,也有例外,比如ReadWriteLock兩種都支持。只支持獨佔模式或者是共享模式的子類不需要定義另一種模式的方法

AQS是實現鎖Lock接口或者是同步組件的關鍵,在鎖的實現中聚合AQS,利用AQS實現鎖的語義。可以這樣理解二者之間的關係:

  1. Lock接口是面向使用者的,它定義了使用者與鎖交互的接口(比如可以允許兩個線程並行訪問),隱藏了實現細節
  2. AQS面向的是鎖的實現者,它簡化了鎖的實現方式,屏蔽了同步狀態管理、線程的排隊、等待與喚醒等底層操作。鎖和AQS很好地隔離了使用者和實現者所需關注的領域。

AQS的接口與實例

AQS的接口

同步器的設計是基於模板模式的,也就是說,使用者需要繼承AQS並重寫指定的方法,隨後將AQS組合在自定義同步組件的實現中,並調用AQS提供的模板方法,而這些模板方法將會調用使用者重寫的方法。

重寫AQS指定的方法時,需要使用AQS提供的如下3個原子操作方法來訪問或修改同步狀態:

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

繼承AQS必須重寫的方法如下所示:

  1. protected boolean tryAcquire(int arg):獨佔式獲取同步狀態,實現該方法需要查詢當前狀態並判斷同步狀態是否符合預期,然後再進行CAS設置同步狀態。
  2. protected boolean tryRelease(int arg):獨佔式釋放同步狀態,等待獲取同步狀態的線程將有機會獲取同步鎖。
  3. protected int tryAcquireShared(int arg):共享式的獲取同步狀態,返回大於等於0的值,表示鎖獲取成功,反之,獲取失敗
  4. protected boolean tryReleaseShared(int arg):共享式釋放同步狀態。
  5. protected boolean isHeldExclusively():當前AQS是否在獨佔模式下被線程佔用,一般該方法表示是否被當前線程所獨佔。

繼承AQS實現自定義同步組件時,將會調用AQS提供的模板方法。這些模板方法如下所示,注意到這些方法都是final修飾,表示這些方法不能被重寫。

  1. public final void acquire(int arg): 獨佔式的獲取同步狀態,如果當前線程獲取同步狀態成功,則由該方法返回。否則,將會進入同步隊列等待,該方法將會調用重寫的tryAcquire(int arg)。
  2. public final void acquireInterruptibly(int arg): 與acquire(int arg)相同,但是該方法響應中斷,當前線程未獲取同步狀態而進行同步隊列中,如果當前線程被中斷,則該方法會拋出InterruptedException並返回。
  3. public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException:在acquireInterruptibly(int arg)基礎上增加了超時限制,如果當前線程在超時時間內沒有獲取同步狀態,那麼將會返回false,如果獲取到了返回true。
  4. public final boolean release(int arg):獨佔式的釋放同步隊列,該方法會在釋放同步狀態之後,將同步隊列中的第一個節點包含的線程喚醒。
  5. public final void acquireShared(int arg):共享式的獲取同步狀態,如果當前線程未獲取到同步狀態,將會進入同步隊列等待,與獨佔式獲取的主要區別是在同一時刻可以有多少個線程獲取到同步狀態。
  6. public final void acquireSharedInterruptibly(int arg):與acquireShared(int arg)相同,該方法響應中斷。
  7. public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException:在acquireSharedInterruptibly(int arg)基礎上增加了超時限制。
  8. public final boolean releaseShared(int arg):共享式的釋放同步狀態。
  9. public final Collection getQueuedThreads():獲取等待在同步隊列上的線程集合。

同步器提供的模板方法基本上分爲3類:獨佔式獲取與釋放同步狀態共享式獲取與釋放同步狀態查詢同步隊列中的等待線程情況。自定義同步組件將使用同步器提供的模板方法來實現自己的同步語義。

一個簡單的繼承AQS的獨佔鎖Mutex

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

class Mutex implements Lock {
    // 僅需要將操作代理到Sync上即可
    private final Sync sync = new Sync();

    public void lock() {
        sync.acquire(1);
    }

    public boolean tryLock() {
        return sync.tryAcquire(1);
    }

    public void unlock() {
        sync.release(1);
    }

    public Condition newCondition() {
        return sync.newCondition();
    }

    public boolean isLocked() {
        return sync.isHeldExclusively();
    }

    public boolean hasQueuedThreads() {
        return sync.hasQueuedThreads();
    }

    public void lockInterruptibly() throws InterruptedException {
        sync.acquireInterruptibly(1);
    }

    public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }

    // 靜態內部類,自定義同步器
    private static class Sync extends AbstractQueuedSynchronizer {
        // 是否處於佔用狀態
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }

        // 當狀態爲0的時候獲取鎖
        public boolean tryAcquire(int acquires) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        // 釋放鎖,將狀態設置爲0
        protected boolean tryRelease(int releases) {
            if (getState() == 0) throw new
                    IllegalMonitorStateException();
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        // 返回一個Condition,每個condition都包含了一個condition隊列
        Condition newCondition() {
            return new ConditionObject();
        }
    }
}

可以發現:

  1. 獨佔鎖Mutex的內部類Sync只實現了AQS獨佔模式的相關方法,因爲它只使用了獨佔模式。
  2. 獨佔鎖Mutex實現了Lock接口的所有方法。

下面在給出這個獨佔鎖的使用Demo,可以發現它的實現沒問題,確實完成了和synchronized關鍵字一樣的同步功能。

    /**
     * 輸出:
     * Running Thread-1
     * Exit Thread-1
     * Running Thread-2
     * Exit Thread-2
     */
    @Test
    public void testCase01() throws InterruptedException {
        Mutex mutex = new Mutex();

        Runnable r = () -> {
            mutex.lock();
            System.out.println("Running " + Thread.currentThread().getName());

            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (Exception e) {
                e.printStackTrace();
            }
            mutex.unlock();
            System.out.println("Exit " + Thread.currentThread().getName());
        };

        Thread thread1 = new Thread(r, "Thread-1");
        Thread thread2 = new Thread(r, "Thread-2");
        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();
    }

我在寫這個例子的時候,自己有個疑問。就是,Thread2在阻塞獲取鎖又獲取不到的時候,它的線程狀態的什麼?此時,MainThread的狀態是Waiting(在join的邏輯裏面),Thread1的線程狀態的是Timed_Waiting(正在sleep)。但是,Thread2的線程狀態呢?線程的狀態參考深入理解Java併發編程之線程Thread

如果,這裏不是用的自己實現的獨佔鎖Mutex,而是用的synchronized,這個時候Thread2的狀態明顯是blocked

爲了回答這個問題,我把sleep的時間設置爲666666,同時通過jstack dump出線程狀態,很清楚的發現Thread2的狀態爲waiting,結果如下。

"Thread-2" #15 prio=5 os_prio=0 tid=0x00000000299cd000 nid=0x9498 waiting on condition [0x000000002b81e000]
   java.lang.Thread.State: WAITING (parking)
	at sun.misc.Unsafe.park(Native Method)
	- parking to wait for  <0x00000007177a70f0> (a concurrent.aqs.Mutex$Sync)
	at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.parkAndCheckInterrupt(AbstractQueuedSynchronizer.java:836)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireQueued(AbstractQueuedSynchronizer.java:870)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.acquire(AbstractQueuedSynchronizer.java:1199)
	at concurrent.aqs.Mutex.lock(Mutex.java:17)
	at concurrent.aqs.MutexTest.lambda$testCase01$0(MutexTest.java:17)
	at concurrent.aqs.MutexTest$$Lambda$1/385337537.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:748)

AQS的實現分析

接下來將從實現角度分析AQS是如何完成線程同步的,主要包括:同步隊列、獨佔式同步狀態獲取與釋放、共享式同步狀態獲取與釋放以及超時獲取同步狀態等AQS的核心數據結構與模板方法。

同步隊列

AQS依賴內部的同步隊列(一個FIFO雙向隊列)來完成同步狀態的管理:

  1. 當前線程獲取同步狀態失敗時,同步器會將當前線程以及等待狀態等信息構造成爲一個節點(Node)並將其加入同步隊列,同時會阻塞當前線程(waiting)。
  2. 當前節點同步狀態釋放時,會把後續節點中的線程喚醒,使其再次嘗試獲取同步狀態。

同步隊列是由一個Node節點的雙向鏈表實現的,Node節點數據接口如下:

volatile int waitStatus Node節點等待狀態,包含如下狀態:

  1. Cancelled,值爲1,表示這個Node節點包裹的線程已經被取消。由於在同步隊列中等待的線程等待超時或者被中斷,需要從同步隊列中取消等待,節點進入該狀態將不會變化。
  2. Signal,值爲-1,該狀態的Node節點的後繼節點的線程處於等待狀態,如果當前節點的線程釋放了同步狀態或者被取消,將會通知後繼節點,使後繼節點的線程得以運行。
  3. Condition,值爲-2,節點在等待隊列中,該狀態的節點表示節點包裹的線程等待在Condition上,當其他線程對Condition調用了signal()方法後,該節點將會從等待隊列中轉移到同步隊列(尾部)中,加入到對同步狀態的獲取中。
  4. Propagate,值爲-3,表示共享式同步狀態將會被無條件的傳播到其他節點,該狀態只會被用來設置共享模式的AQS的head節點。

這些數值的設置只是爲了簡化使用。非負的數值節點表示該節點不用被signal。因此,大多數情況下,不會關心這些具體數值,只會關心數值的符號。

volatile Node prev

前驅節點,當節點從隊列尾部加入同步隊列時被設置。

volatile Node next 後繼節點

Node nextWaiter 等待隊列(等待在ConditionObject上的線程節點隊列)中的後繼節點。

  • 如果當前節點是獨佔的,這個值指向等待隊列中的下一個節點。

  • 如果當前節點是共享的,那麼這個字段將是一個SHARED常量。

volatile Thread thread Node節點鎖包裹的線程。

Node節點是構成AQS同步隊列以及等待隊列的基礎,其中同步隊列的基本結構如下圖所示:

 

image.png

 

 

  1. AQS擁有同步同步隊列的頭節點(head)和尾結點(tail)的引用(head和tail爲AQS的成員變量)

  2. 當一個線程成功獲取了同步狀態(或者鎖),其他線程將無法獲得從而被構造成爲Node節點加入同步隊列尾部。

  3. 加入隊列的過程必須要保證線程安全(可能有多個線程都沒獲取到同步狀態需要加入同步隊列),因此AQS提供了一個基於CAS的設置尾節點的方法:compareAndSetTail(Node expect,Node update),它需要傳遞當前線程“認爲”的尾節點和當前節點,只有設置成功後,當前節點才正式與之前的尾節點建立關聯。這個過程如下圖所示。

 

image.png

 

 

  1. 同步隊列遵循FIFO,首節點是獲取同步狀態成功的節點,首節點的線程在釋放同步狀態時,將會喚醒後繼節點,而後繼節點將會在獲取同步狀態成功時將自己設置爲首節點,該過程如下圖所示。設置首節點是通過獲取同步狀態成功的線程來完成的,由於只有一個線程能夠成功獲取到同步狀態,因此設置頭節點的方法並不需要使用CAS來保證,它只需要將首節點設置成爲原首節點的後繼節點並斷開原首節點的next引用即可。

 

image.png

 

 

獨佔式同步狀態獲取與釋放

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

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

上述代碼主要完成了同步狀態獲取、節點構造、加入同步隊列以及在同步隊列中自旋等待的相關工作,其主要邏輯如下:

  1. 調用自定義同步器實現的tryAcquire(int arg)方法,該方法保證線程安全的獲取同步狀態

  2. 如果同步狀態獲取失敗,則構造同步節點(獨佔式Node.EXCLUSIVE,同一時刻只能有一個線程成功獲取同步狀態)並通過addWaiter(Node node)方法將該節點加入到同步隊列的尾部

  3. 最後調用acquireQueued(Node node,int arg)方法,使得該節點以“死循環”的方式獲取同步狀態。如果獲取不到則阻塞節點中的線程,而被阻塞線程的喚醒主要依靠前驅節點的出隊並喚醒當前節點阻塞線程被中斷來實現。

下面逐個分析相關函數。

    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }
    
    private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }
    
    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);
        }
    }
  1. 這裏通過使用compareAndSetTail(Node expect,Node update)方法來確保節點能夠被線程安全添加。用**CAS保證線程安全的原因是由於可能有多個線程調用tryAcquire(int arg)**方法獲取同步狀態失敗而添加到隊列尾部。
  2. 在enq(final Node node)方法中,同步器通過“死循環”來保證節點的正確添加,在“死循環”中只有通過CAS將節點設置成爲尾節點之後,當前線程才能從該方法返回,否則,當前線程不斷地嘗試設置。可以看出,enq(final Node node)方法將併發添加節點的請求通過CAS變得“串行化”了。
  3. 節點進入同步隊列之後,就進入了一個自旋的過程,每個節點(或者說每個線程)都在自旋地觀察,當條件滿足,獲取到了同步狀態,就可以從這個自旋過程中退出,否則依舊留在這個自旋過程中(並會阻塞節點的線程)。這部分對應acquireQueued()函數。

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

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

 

image.png

 

 

在上圖中,由於非首節點線程前驅節點出隊或者被中斷而從等待狀態返回,隨後檢查自己的前驅是否是頭節點,如果是則嘗試獲取同步狀態。可以看到節點和節點之間在循環檢查的過程中基本不相互通信,而是簡單地判斷自己的前驅是否爲頭節點,這樣就使得節點的釋放規則符合FIFO,並且也便於對過早通知的處理(過早通知是指前驅節點不是頭節點的線程由於中斷而被喚醒即僞喚醒)。

獨佔式同步狀態獲取流程,也就是acquire(int arg)方法調用流程如下圖所示:

 

image.png

 

 

在上圖中,前驅節點爲頭節點且能夠獲取同步狀態的判斷條件和線程進入等待狀態是獲取同步狀態的自旋過程。當同步狀態獲取成功之後,當前線程從acquire(int arg)方法返回,如果對於鎖這種併發組件而言,代表着當前線程獲取了鎖。

獨佔式同步狀態釋放

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

    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來喚醒處於等待狀態的線程。

獨佔式AQS高度總結

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

共享式同步狀態獲取與釋放

#####共享式同步狀態獲取 共享式獲取與獨佔式獲取最主要的區別在於同一時刻能否有多個線程同時獲取到同步狀態。以文件的讀寫爲例

  1. 如果一個程序在對文件進行讀操作,那麼這一時刻對於該文件的寫操作均被阻塞,而讀操作能夠同時進行。
  2. 寫操作要求對資源的獨佔式訪問,而讀操作可以是共享式訪問,兩種不同的訪問模式在同一時刻對文件或資源的訪問情況,如下圖所示:

 

image.png

 

 

上圖中:

  1. 左半部分,共享式訪問資源時,其他共享式的訪問均被允許,而獨佔式訪問被阻塞。

  2. 右半部分是獨佔式訪問資源時,同一時刻其他訪問均被阻塞。

通過調用同步器的acquireShared(int arg)方法可以共享式地獲取同步狀態,該方法代碼如下所示:

    public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
    }
    
    private void doAcquireShared(int arg) {
        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);
        }
    }
  1. 在acquireShared(int arg)方法中,同步器調用tryAcquireShared(int arg)方法嘗試獲取同步狀態,tryAcquireShared(int arg)方法返回值爲int類型,當返回值大於等於0時,表示能夠獲取到同步狀態。
  2. 因此,在共享式獲取的自旋過程中,成功獲取到同步狀態並退出自旋的條件:當前節點的前序是頭結點且tryAcquireShared(int arg)方法返回值大於等於0

#####共享式同步狀態釋放 與獨佔式一樣,共享式獲取也需要釋放同步狀態,通過調用releaseShared(int arg)方法可以釋放同步狀態,該方法代碼如下所示:

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

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

獨佔式超時獲取同步狀態

通過調用同步器的doAcquireNanos(int arg,long nanosTimeout)方法可以超時獲取同步狀態,即在指定的時間段內獲取同步狀態,如果獲取到同步狀態則返回true,否則,返回false。該方法提供了傳統Java同步操作(比如synchronized關鍵字)所不具備的特性。

響應中斷的獲取同步狀態

在Java 5之前,當一個線程獲取不到鎖而被阻塞在synchronized之外時,對該線程進行中斷操作,此時該線程的中斷標誌位會被修改,但線程依舊會阻塞在synchronized上,等待着獲取鎖。在Java 5中,同步器提供了acquireInterruptibly(int arg)方法,這個方法在等待獲取同步狀態時,如果當前線程被中斷,會立刻返回,並拋出InterruptedException。

獨佔式超時獲取同步狀態過程可以被視作響應中斷獲取同步狀態過程的“增強版”,doAcquireNanos(int arg,long nanosTimeout)方法在支持響應中斷的基礎上,增加了超時獲取的特性。針對超時獲取,主要需要計算出需要睡眠的時間間隔nanosTimeout,爲了防止過早通知,nanosTimeout計算公式爲:nanosTimeout = deadline - System.nanoTime(),其中deadline爲計算的最晚喚醒時間,如果nanosTimeout大於0則表示超時時間未到,需要繼續睡眠nanosTimeout納秒,反之,表示已經超時,該方法代碼如下:

    private boolean doAcquireNanos(int arg, long nanosTimeout)
            throws InterruptedException {
        if (nanosTimeout <= 0L)
            return false;
        final long deadline = System.nanoTime() + nanosTimeout;
        final Node node = addWaiter(Node.EXCLUSIVE);
        boolean failed = true;
        try {
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return true;
                }
                nanosTimeout = deadline - System.nanoTime();
                if (nanosTimeout <= 0L)
                    return false;
                if (shouldParkAfterFailedAcquire(p, node) &&
                    nanosTimeout > spinForTimeoutThreshold)
                    LockSupport.parkNanos(this, nanosTimeout);
                if (Thread.interrupted())
                    throw new InterruptedException();
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }
  1. 該方法在自旋過程中,當節點的前驅節點爲頭節點時嘗試獲取同步狀態,如果獲取成功則從該方法返回,這個過程和獨佔式同步獲取的過程類似。

  2. 如果當前線程獲取同步狀態失敗,則判斷是否超時(nanosTimeout小於等於0表示已經超時),如果沒有超時,重新計算超時間隔nanosTimeout,然後使當前線程等待nanosTimeout納秒(當已到設置的超時時間,該線程會從LockSupport.parkNanos(Objectblocker,long nanos)方法返回)。

  3. 如果nanosTimeout小於等於spinForTimeoutThreshold(1000納秒)時,將不會使該線程進行超時等待,而是進入快速的自旋過程。原因在於,非常短的超時等待無法做到十分精確,如果這時再進行超時等待,相反會讓nanosTimeout的超時從整體上表現得反而不精確。因此,在超時非常短的場景下,同步器會進入無條件的快速自旋。(這種情況下線程的阻塞和喚醒的耗時可能大於nanosTimeout)。

獨佔式超時獲取同步態的流程如下圖所示:

 

image.png

 

 

自定義同步組件-TwinsLock

這裏設計一個同步工具:該工具在同一時刻,只允許至多兩個線程同時訪問,超過兩個線程的訪問將被阻塞,我們將這個同步工具命名爲TwinsLock。

訪問模式

TwinsLock能夠在同一時刻支持多個線程的訪問,這顯然是共享式訪問,因此,需要使用同步器提供的acquireShared(int args)方法等和Shared相關的方法,這就要求TwinsLock必須重寫tryAcquireShared(int args)方法和tryReleaseShared(int args)方法,這樣才能保證同步器的共享式同步狀態的獲取與釋放方法得以執行。

資源數

TwinsLock在同一時刻允許至多兩個線程的同時訪問,表明同步資源數爲2,這樣可以設置初始狀態status爲2,當一個線程進行獲取,status減1,該線程釋放,則status加1,狀態的合法範圍爲0、1和2,其中0表示當前已經有兩個線程獲取了同步資源,此時再有其他線程對同步狀態進行獲取,該線程只能被阻塞。在同步狀態變更時,需要使用compareAndSet(int expect,int update)方法做原子性保障。

TwinsLock

通過實現Lock接口以及組合繼承AQS的子類來實現這個自定義的同步組件。一般情況下,繼承AQS的子類會被定義爲這個同步組件類的靜態內部類。

public class TwinsLock implements Lock {
    private final Sync sync = new Sync(2);

    private static final class Sync extends AbstractQueuedSynchronizer {
        Sync(int count) {
            if (count <= 0) {
                throw new IllegalArgumentException("Count must be larger than zero!");
            }
            setState(count);
        }

        @Override
        protected int tryAcquireShared(int reduceCount) {
            for (;;) {
                int current = getState();
                int newCount = current - reduceCount;
                if (newCount < 0 || compareAndSetState(current, newCount)) {
                    return newCount;
                }
            }
        }

        @Override
        protected boolean tryReleaseShared(int returnCount) {
            for (;;) {
                int current = getState();
                int newCount = current + returnCount;
                if (compareAndSetState(current, newCount)) {
                    return true;
                }
            }
        }

        Condition newCondition() {
            return new ConditionObject();
        }
    }

    @Override
    public void lock() {
        sync.acquireShared(1);
    }

    @Override
    public void unlock() {
        sync.releaseShared(1);
    }
    // 其他接口方法略
}

在上述實例中,

  1. TwinsLock實現了Lock接口,提供了面向使用者的接口,使用者調用lock()方法獲取鎖,隨後調用unlock()方法釋放鎖,而同一時刻只能有兩個線程同時獲取到鎖。
  2. TwinsLock同時包含了一個自定義同步器Sync,而該同步器面向線程訪問和同步狀態控制。以共享式獲取同步狀態爲例:同步器會先計算出獲取後的同步狀態,然後通過CAS確保狀態的正確設置,tryAcquireShared(int reduceCount)方法返回值大於等於0時,當前線程才獲取同步狀態,對於上層的TwinsLock而言,則表示當前線程獲得了鎖。

AQS作爲一個橋樑,連接線程訪問以及同步狀態控制等底層技術與不同併發組件(比如Lock、CountDownLatch等)的接口語義

TwinsLock測試Demo

在測試用例中,定義了工作者線程Worker,該線程在執行過程中獲取鎖,當獲取鎖之後使當前線程睡眠1秒(並不釋放鎖),隨後打印當前線程名稱,最後再次睡眠1秒並釋放鎖,測試代碼如下:

public class TwinsLockTest {
    Lock lock = new TwinsLock();

    @Test
    public void testCase01() throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            Worker w = new Worker();
            Thread thread = new Thread(w, "Worker" + i);
            thread.start();
        }

        // 每隔1秒換行
        for (int i = 0; i < 10; i++) {
            Thread.sleep(1000);
            System.out.println();
        }
    }

    class Worker implements Runnable {
        @Override
        public void run() {
            while (true) {
                lock.lock();
                try {
                    Thread.sleep(1000);
                    System.out.println(Thread.currentThread().getName());
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
                break;
            }
        }
    }
}

運行可以發現輸出結果如下,同一時刻只有兩個線程能夠獲取到鎖,這表明TwinsLock可以按照預期正確工作。

Worker0
Worker1

Worker3
Worker2

Worker4
Worker5

Worker7
Worker6

Worker9
Worker8

可重入鎖ReentranLock

可重入性

可重入鎖ReentrantLock,顧名思義,就是支持重進入的鎖,它表示該鎖能夠支持一個線程對 資源的重複加鎖。簡單地說,重進入是指任意線程在獲取到鎖之後能夠再次獲取該鎖而不會被鎖所阻塞。

synchronized關鍵字隱式的支持重進入,比如一個synchronized修飾的遞歸方法,在方法執行時,執行線程在獲取了鎖之後仍能連續多次地獲得該鎖。而本文之前的例子Mutex卻不支持可重入性。如果某個線程獲取了鎖,會在下一次獲取鎖時出現阻塞自己的情況。

可重入性的實現有兩個問題:

  1. 線程再次獲取鎖。鎖需要去識別獲取鎖的線程是否爲當前佔據鎖的線程,如果是,則再次成功獲取。
  2. 鎖的最終釋放。線程重複n次獲取了鎖,隨後在第n次釋放該鎖後,其他線程能夠獲取到該鎖。鎖的最終釋放要求鎖對於獲取進行計數自增,計數表示當前鎖被重複獲取的次數,而鎖被釋放時,計數自減,當計數等於0時表示鎖已經成功釋放

ReentrantLock是通過組合自定義繼承AQS的子類以及實現Lock接口來實現鎖的獲取與釋放,以非公平性(默認的)實現爲例,獲取同步狀態的代碼如下:

        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在釋放同步狀態時減少同步狀態值,該方法的代碼如下:

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

公平獲取鎖 vs 非公平獲取鎖

公平性與否是針對獲取鎖而言的,如果一個鎖是公平的,那麼鎖的獲取順序就應該符合請求的絕對時間順序,也就是FIFO。

上面介紹的nonfairTryAcquire(int acquires)方法,對於非公平鎖,只要CAS設置同步狀態成功,則表示當前線程獲取了鎖,而公平鎖則不同,公平獲取鎖的代碼如下所示:

        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            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;
        }

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

公平與非公平獲取鎖的區別

  1. tryAcquire中鎖獲取的條件:非公平鎖只要CAS設置同步狀態成功,公平鎖需要CAS設置同步狀態成功+當前節點是同步隊列首節點。
  2. 性能: 公平性鎖保證了鎖的獲取按照FIFO原則,而代價是進行大量的線程切換非公平性鎖雖然可能造成線程“飢餓”,但極少的線程切換,保證了其更大的吞吐量
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章