Java併發—鎖

Java中的鎖

1.Lock接口

鎖是用來控制多個線程訪問共享資源的方式, 一般來說, 一個鎖能夠防止多個線程同時訪問共享資源(但是有些鎖可以允許多個線程併發的訪問共享資源, 比如讀寫鎖). 在Lock接口出現之前, Java程序是靠synchronized關鍵字來實現鎖功能的, 它提供了和synchronized關鍵字類似的同步功能, 只是在使用時需要顯示的獲取和釋放鎖. 雖然它缺少了隱式獲取釋放鎖的便捷性, 但卻擁有了鎖獲取與釋放的可操作性, 可中斷性的獲取鎖以及超時獲取鎖等多種synchronized關鍵字所不具備的同步特性

2.隊列同步器(AQS)

隊列同步器AbstractQueueSynchronizer, 是用來構建鎖或者其他同步組件的基礎框架, 它使用了一個int成員變量表示同步狀態, 通過內置的FIFO隊列來完成資源獲取線程的排隊工作

同步器的主要使用方法是繼承, 子類通過繼承同步器並實現它的抽象方法來管理同步狀態, 在抽象方法的實現過程中免不了要對同步狀態進行更改, 這時就需要使用同步器的3個方法(getState(), setState(int newState), 和 compareAndSetState(int expect, int update))來進行操作, 因爲它們能夠保證狀態的改變是安全的.

同步器是實現鎖(也可以是任意同步組件)的關鍵, 在鎖的實現中聚合同步器, 利用同步器實現鎖的語義. 可以這樣理解二者之間的關係 : 鎖是面向使用者的; 同步器是面向鎖的實現者, 簡化了鎖 的實現方式, 屏蔽了同步狀態管理, 現成的排隊, 等待與喚醒等底層操作

1>AQS的接口與示例

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

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

  • getState() : 獲取當前同步狀態
  • setState() : 設置當前同步狀態
  • compareAndSetState(int expect, int update) : 使用CAS設置當前狀態, 該方法能夠保證狀態設置的原子性
方法名稱 描述
protected boolean tryAcquire(int arg) 獨佔式獲取同步狀態, 實現該方法需要查詢當前狀態並判斷同步狀態是否符合預期, 然後再進行CAS設置同步狀態
protected boolean tryRelease(int arg) 獨佔式釋放同步狀態, 等待獲取同步狀態的線程將有機會獲取同步狀態
protected int tryAcquireShared(int arg) 共享式獲取同步狀態, 返回大於等於0的值, 表示獲取成功 , 反之, 獲取失敗
protected boolean tryReleaseShared(int arg) 共享式釋放同步狀態
protected boolean isHeldExclusively() 當前同步器是否在獨佔模式下被線程佔用, 一般該方法表示是否被當前線程所獨佔

實現自定義同步組件時, 將會調用同步器提供的末班方法

方法名稱 描述
void acquire(int arg) 獨佔式獲取同步狀態, 如果當前線程獲取同步狀態成功, 則由該方法返回, 否則將會進入同步隊列等待. 該方法將會調用重寫的tryAcquire()方法
void acquireInterruptibly(int arg) 與acquire(int arg)相同, 但該方法響應中斷, 當前線程爲獲取到同步狀態而進入同步隊列時, 如果當前線程被中斷, 則會拋出InterruptedExecption並返回
boolean tryAcquireNanos(int arg, long nanos) 在acquireInterruptibly(int arg) 的基礎上增加了超時限制, 如果當前方法在超時時間內沒有獲取到同步鎖, 則返回false, 否則返回true
void acquireShared(int arg) 共享式的獲取同步狀態, 如果當前線程未獲取到同步狀態, 將會進入同步隊列等待, 與獨佔式的主要區別是在同一時刻可以有多個線程獲取到同步狀態
void acquireSharedInterruptibly(int arg) 與acquireSharedy(int arg)相同 , 該方法響應中斷
boolean tryAcquireSharedNanos(int arg, long nanos) 在acquireSharedInterruptibly(int arg)基礎上增加了超時限制
boolean release(int arg) 獨佔式的釋放同步狀態, 該方法會 在釋放同步狀態之後, 將同步隊列中第一個節點包含的線程喚醒
boolean releaseShared(int arg) 共享式的釋放同步狀態
Collection< Thread> getQueueThreads() 獲取等待在同步狀態的線程集合

同步器提供的模板方法基本上分爲3類, 獨佔式獲取與釋放同步狀態, 共享式獲取與釋放同步狀態和查詢同步隊列中的等待線程情況.

2>AQS的實現分析

**a.同步隊列 : **

同步器依賴內部的同步隊列(一個FIFO雙向隊列)來完成同步狀態的管理, 當前線程獲取同步狀態失敗時, 同步器會將當前線程以及等待狀態等信息構造成爲一個節點(Node), 並將其加入同步隊列, 同時會阻塞當前線程, 當同步狀態釋放時, 會把首節點中的線程喚醒, 使其再次嘗試獲取同步狀態

同步隊列中的節點(Node)用來保存獲取同步狀態失敗的線程引用, 等待狀態以及前驅和後繼節點

屬性類型與名稱 描述
int waitStatus 1.CANCELLED,值爲1, 由於同步隊列中等待的線程等待超時或者被中斷, 需要從同步隊列中取消等待, 節點進入該狀態將不會變化 ; 2.SIGNAL, 值爲-1, 後繼節點的線程處於等待狀態,而當前節點的線程如果釋放了同步狀態或被取消, 將會通知後繼節點, 使後集節點的線程得以運行; 3.CONDITION, 值爲-2, 節點在等待隊列中, 節點線程登載在Condition上, 當其他線程對Condition調用了signal()方法後, 該節點將會從等待隊列中移動到同步隊列中, 加入到對同步狀態的獲取中 4.PROPAGATE , 值爲-3, 表示下一次共享式同步狀態獲取將無條件地被傳播下去; 5.INITIAL, 值爲0, 初始狀態
Node prev 前驅節點, 當前節點加入同步隊列時設置
Node next 後繼節點
Node nextWaiter 等待隊列中的後繼節點, 如果當前節點是共享的, 那麼這個字段將是一個SHARED常量, 也就是說節點類型(共享和獨佔)和等待隊列中的後繼節點共用一個字段
Thread thread 獲取同步狀態的線程

節點是構成同步隊列的接觸, 同步器擁有首節點(head)和尾節點(tail), 沒有成功獲取同步狀態的線程會爲節點加入該隊列的尾部, 同步隊列的基本結構如圖:

在這裏插入圖片描述

同步器包含了兩個節點類型的引用, 一個指向頭結點, 而另一個指向尾節點. 一個線程如果沒有獲取到同步狀態, 要放入同步隊列時, 需要保證線程安全, 因此同步器提供了一個基於CAS的設置尾節點的方法 : compareAndSetTail(Node expect, Node update), 它需要傳遞當前線程"認爲"的尾節點和當前節點, 只有設置成功後, 當前節點才正式 與之前的節點建立關聯

同步隊列遵循FIFO, 首節點是獲取同步狀態成功的節點, 首節點的線程在釋放同步狀態的時, 會喚醒其後繼節點 , 而後繼節點將會在獲取同步狀態成功時將自己設置爲首節點.

設置首節點是通過獲取同步狀態成功的線程完成的, 由於只有一個線程能成功獲取到同步狀態, 因此設置頭結點的方法不需要使用CAS來保證, 只需要將首節點設置爲原首節點的後繼節點並斷開首節點的next引用即可

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

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

	//同步器的acquire方法
	public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

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

	private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // 嘗試在尾部添加
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {//CAS更新尾部節點
                pred.next = node;
                return node;
            }
        }
        //隊列如果沒有初始化,
        enq(node);
        return node;
    }
	private Node enq(final Node node) {
        for (;;) {//自旋進行CAS操作, 一直到更新成功
            Node t = tail;
            if (t == null) { // 要先進行初始化
                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);
        }
    }

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

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

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

在這裏插入圖片描述

前驅節點爲頭結點且能夠獲取同步狀態的判斷條件和線程進入等待狀態是獲取同步狀態的自旋過程. 當同步狀態獲取成功後, 當線程從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來喚醒處於等待狀態的線程

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

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

共享式獲取與獨佔式最主要的區別在於同一時刻能否有多個線程同步獲取到同步狀態.

//同步器的acquireShared和doAcquireShared方法
	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);
        }
    }

在acquireShared(int arg)方法中, 同步器調用tryAcquireShared(int arg)方法嘗試獲取同步狀態, tryAcquireShared(int arg)方法返回值爲int類型, 當返回值大於等於0時, 表示能夠獲取到同步狀態, 因此在共享式獲取的自旋過程中, 成功獲取到同步狀態並退出自旋的條件就是tryAcquireShared(int arg)方法返回值大於等於0. 可以看到, 在都AcquireShared(int arg)方法的自旋中, 如果當前節點的前驅爲頭結點時, 嘗試獲取同步狀態, 如果返回值大於等於0, 表示該次獲取同步狀態成功並從自旋過程中退出

調用releaseShared(int arg)方法可以釋放同步狀態

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

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

d.獨佔式超時獲取同步狀態

通過調用同步器的doAcquireNanos(int arg, long nanosTimeout)方法可以超過獲取同步狀態, 即在指定時間段內獲取同步狀態, 如果獲取到同步狀態則返回true, 否則返回false.

該方法在自旋過程中, 當節點的前驅節點爲頭節點時嘗試獲取同步狀態, 如果獲取成功則從該方法返回, 這個過程和獨佔式同步獲取的過程類似, 但是在同步狀態獲取失敗的處理上有所不同. 如果當前線程獲取失敗, 則哦按段是否超時, 如果沒有超時, 重新計算超時間隔, 然後使線程等待.

過程如下圖所示

在這裏插入圖片描述

3.重入鎖(ReentrantLock)

重入鎖(ReentrantLock), 顧名思義就是支持重進入的鎖, 它表示能夠支持一個線程對資源的重複加鎖, 除此之外, 該鎖還支持獲取所的公平性和非公平性選擇

ReentrantLock雖然沒能像synchronized關鍵字一樣隱式的重進入, 但是在調用lock()方法時, 已經獲取到鎖的線程, 能夠再次調用lock方法獲取鎖而不會被阻塞

公平性 : 如果在絕對時間上, 先對鎖進行獲取的請求一定先被滿足, 那麼這個鎖就是公平的, 反之是不公平的

1>實現重進入

爲了實現重進入, 要實現下面兩個問題 :

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

ReentrantLock是通過組合自定義同步器來實現鎖的獲取與釋放, 以非公平性的爲例

	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()) {
                //當前線程就是佔據的線程, state值增加
                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;
        }

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

2>公平與非公平鎖的區別

對於非公平鎖, 只要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, 則表示該線程比當前線程更早地請求獲取鎖, 因此需要等待前驅線程獲取鎖並釋放鎖之後才能繼續獲取鎖

對於公平鎖, 每次都是從同步隊列中的第一個節點獲取到鎖, 而非公平鎖會出現一個線程連續獲取鎖, 這是因爲當一個線程請求鎖時, 只要獲取了同步狀態即成功獲取所. 所以剛釋放鎖的線程再次獲取鎖的機率很大, 使得其他線程只能在同步隊列中等待

非公平鎖可能使線程"餓死", 但其又被設定成默認的實現, 因爲公平鎖雖然保證了鎖的獲取按照FIFO原則, 但代價是進行大量的線程切換. 非公平鎖雖然可能造成線程"飢餓", 但極少的線程切換, 保證了其更大的吞吐量

4.讀寫鎖

之前提到的鎖基本都是排他鎖, 這些鎖在同一時刻只允許一個線程進行訪問, 而讀寫鎖在同一時刻可以允許多個讀線程訪問, 但是在寫線程訪問時, 所有的讀線程和寫線程均被阻塞. 讀寫鎖維護了一對鎖, 一個讀鎖和一個寫鎖, 通過分離讀鎖和寫鎖, 使得併發性相比一般的排他鎖有了很大的提升

一般情況下, 讀寫鎖的性能都會比排他鎖好, 因爲大多數場景是讀多於寫的,這時候, 讀寫鎖可以提供更好的併發性和吞吐量, Java併發包提供讀寫鎖的實現是ReentrantReadWriteLock

1>讀寫鎖的接口與示例

ReadWriteLock僅定義了獲取讀鎖和寫鎖的兩個方法, 即readLock()方法和WriteLock()方法, 而其實現, ReentrantReadWriteLock, 除了接口方法之外, 還提供了一些便於外界監控其內部工作狀態的方法

方法名稱 描述
int getReadLockCount() 返回當前讀鎖被獲取的次數.該次數不等於獲取讀鎖的線程數. 例如一個線程重進入了n次讀鎖,那麼該方法返回n
int getReadHoldCount() 返回當前線程獲取讀鎖的次數, 使用ThreadLocal保存當前線程獲取的次數
boolean isWriteLocked() 判斷寫鎖是否被獲取
int getWriteHoldCount 返回當前寫鎖被獲取的次數

2>讀寫鎖的實現分析

  • 1.讀寫狀態的設計

    讀寫鎖同樣依賴自定義同步器來實現同步功能, 而讀寫狀態就是其同步器的同步狀態. 在ReentrantLock中, 同步狀態表示鎖被一個線程重複獲取的次數, 而讀寫鎖的自定義同步器需要在同步狀態(一個整型變量)上維護多個讀線程和寫線程的狀態, 使得該狀態的設計成爲讀寫鎖實現的關鍵

    讀寫鎖將變量分成了兩個部分, 高16位表示讀, 低16位表示寫狀態

    讀寫鎖通過位運算來確定讀和寫的各自狀態, 假設當前同步狀態爲S, 則寫狀態等於S & 0x0000FFFF, 讀狀態等於S >>> 16

  • 2.寫鎖的獲取與釋放

    寫鎖是一個支持重進入的排他鎖. 如果當前線程已經獲取了寫鎖, 則增加寫狀態. 若當前線程在獲取寫鎖時, 讀鎖已被獲取(讀狀態不爲0)或者該線程不是已經獲取寫鎖的線程, 則當前線程進入等待狀態

    寫鎖的釋放與ReentrantLock的釋放過程基本類似, 每次釋放均減少寫狀態, 當寫狀態爲0時表示寫鎖已被釋放, 從而等待的讀寫線程能夠繼續訪問讀寫鎖

  • 3.讀鎖的獲取與釋放

    讀鎖是一個支持重進入的共享鎖, 它能夠被多個線程同時獲取, 在沒有其他寫線程訪問時, 讀鎖總會成功的獲取, 而所做的也只是增加讀狀態, 如果當前線程獲得了讀鎖, 則增加讀狀態(依靠CAS安全增加讀狀態).讀鎖的每次釋放也是線程安全的, 均減少讀狀態

  • 4.鎖降級

    鎖降級指的是寫鎖降級成讀鎖, 如果當前線程擁有寫鎖, 然後將其釋放, 最後再獲取讀鎖, 這種分段完成的過程不能稱之爲鎖降級. 鎖降級是指把持住當前的寫鎖, 再獲取到讀鎖, 之後釋放寫鎖的過程

5.LockSupport工具

LockSupport定義了一組以park開頭的方法來阻塞當前線程, 以及unpark(Thread thread)方法來喚醒一個被阻塞的線程

方法名稱 描述
void park() 阻塞當前線程, 如果調用unpark(Thread thread)方法或者當前線程被中斷, 才能從park()方法返回
void parkNanos(long nanos) 阻塞當前線程, 最長不超過nanos納秒, 返回條件在park()的基礎上加上了超時返回
void parkUntil(long deadline) 阻塞當前線程, 知道deadline時間
void unpark(Thread thread) 喚醒處於阻塞狀態的thread

6.Condition接口

任意一個Java對象, 都有一組監視器方法(定義在java.lang.Object上), 主要包括wait(), wait(long timeout), notify(), notifyAll()方法, 這些方法與synchronized同步關鍵字配合, 可以實現等待/通知模式. Condition接口也提供了類似Object的監視器方法, 與Lock配合可以實現等待/通知模式, 但是這兩者在使用方式以及功能特性上還是有差別的

當調用await()方法後, 當前線程會釋放鎖並在此等待, 而其他線程調用Condition對象的signal方法, 通知當前線程後, 當前線程才從await()方法返回. 並且已經在返回前獲取了鎖

獲取一個Condition必須通過Lock的newCondition()方法.

Condition的實現分析

ConditionObject是同步器AQS的內部類, 因爲Condition的操作需要獲取到相關聯的鎖

  • 1.等待隊列

    等待隊列是一個FIFO的隊列, 在隊列的每個節點都包括了一個線程引用, 該線程就是在Condition對象上等待的線程. 如果一個線程調用了Condition.await()方法, 那麼該線程會釋放鎖, 構造成節點加入等待隊列, 並 進入等待狀態. 其實同步隊列和等待隊列猴子那個的節點類型都是同步器的靜態內部類Node

    一個Condition包含一個等待隊列, Condition擁有首節點(firstWaiter)和尾節點(lastWaiter). 當線程調用Condition.await()方法, 將會以當前線程構造節點並將節點從尾部加入等待隊列, 如圖所示

在這裏插入圖片描述

在Object的監視器模型上, 一個對象擁有一個同步隊列和等待隊列. 而併發包中的Lock(同步器)擁有一個同步隊列和多個等待隊列

  • 2.等待

    調用Condition的await()方法. 會使當前線程進入等待隊列並釋放鎖, 同時線程狀態變爲等待狀態. 當從await()方法返回時, 當前線程一定獲取了Condition相關聯的鎖. 從隊列的角度來看, 調用await()方法時, 相當於同步隊列的首節點(獲取了鎖的節點), 移動到Condition的等待隊列中

    調用該方法的線程成功獲取了鎖, 也就是同步隊列的首節點, 該方法會將當前線程構造成節點並加入等待隊列中, 然後釋放同步狀態, 喚醒同步隊列的後繼節點, 然後當前線程會進入等待狀態

    當等待隊列的節點被喚醒, 則喚醒節點的線程開始嘗試獲取同步狀態. 如果不是通過其他線程調用Condition.signal()方法喚醒, 而是對等待線程進行中斷, 則會拋出InterruptedException

  • 3.通知

    調用Condition的signal()方法, 將會喚醒在等待隊列中等待時間最長的節點(首節點), 在喚醒節點之前, 會將節點移到同步隊列中.

    調用該方法的前置條件是當前線程獲取了鎖,接着獲取等待隊列的首節點, 將其移動到同步隊列並使用LockSupport喚醒節點中的線程

    被喚醒後的線程, 將從await()方法的while循環中退出, 進而調用同步器的acquireQueued()方法加入到獲取同步狀態的競爭中

    成功獲取同步狀態之後, 被喚醒的線程將從先前調用的await()方法返回, 此時線程已經成功的獲取了鎖

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