Java并發(fā)——Java中的鎖

一 Lock接口

Lock是一個(gè)接口用來實(shí)現(xiàn)鎖功能,它提供了和synchronized關(guān)鍵字相似的同步功能,只是在使用的時(shí)候需要顯式調(diào)用。Lock的使用很簡(jiǎn)單,代碼如下:

Lock lock = new ReentrantLock();
lock.lock();
try {
    // dosomething            
} finally {
    lock.unlock();
}

在finally中釋放鎖的目的是保證正在獲取鎖之后,最終能夠釋放鎖。
Lock接口提供的synchronized關(guān)鍵字所不具備的主要特性如下表所示:

Lock接口提供的synchronized關(guān)鍵字不具備的主要特性

Lock是一個(gè)接口,它定義了鎖獲取和釋放的基本操作,Lock的API如表所示:

Lock的API

二 隊(duì)列同步器

隊(duì)列同步器AbstractQueuedSynchronizer(以下簡(jiǎn)稱同步器),是用來構(gòu)建鎖或者其他同步組件的基礎(chǔ)框架,它使用了一個(gè)int成員變量表示同步狀態(tài),通過內(nèi)置的FIFO隊(duì)列來完成資源獲取線程的排隊(duì)工作。

同步器的主要使用方式是繼承,子類通過繼承同步器并實(shí)現(xiàn)它的抽象方法來管理同步狀態(tài),在抽象方法的實(shí)現(xiàn)過程中免不了要對(duì)同步狀態(tài)進(jìn)行更改,這時(shí)就需要使用同步器提供的3個(gè)方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))來進(jìn)行操作,因?yàn)樗鼈兡軌虮WC狀態(tài)的改變是安全的。

鎖和同步器的關(guān)系是:鎖是面向使用者的,它定義了使用者與鎖交互的接口(比如可以允許兩個(gè)線程并行訪問),隱藏了實(shí)現(xiàn)細(xì)節(jié);同步器面向的是鎖的實(shí)現(xiàn)者,它簡(jiǎn)化了鎖的實(shí)現(xiàn)方式,屏蔽了同步狀態(tài)管理、線程的排隊(duì)、等待與喚醒等底層操作。

1 隊(duì)列同步器的接口與示例

重寫同步器指定的方法時(shí),需要使用同步器提供的如下3個(gè)方法來訪問或修改同步狀態(tài)。

  • getState():獲取當(dāng)前同步狀態(tài)。
  • setState(int newState):設(shè)置當(dāng)前同步狀態(tài)。
  • compareAndSetState(int expect,int update):使用CAS設(shè)置當(dāng)前狀態(tài),該方法能夠保證狀態(tài)設(shè)置的原子性。

同步器可重寫的方法與描述如表所示:

同步器可重寫的方法

同步器的模板方法如表所示:

同步器的模板方法

2 同步器的實(shí)現(xiàn)分析

同步隊(duì)列

同步器依賴內(nèi)部的同步隊(duì)列(一個(gè)FIFO雙向隊(duì)列)來完成同步狀態(tài)的管理,當(dāng)前線程獲取同步狀態(tài)失敗時(shí),同步器會(huì)將當(dāng)前線程以及等待狀態(tài)等信息構(gòu)造成為一個(gè)節(jié)點(diǎn)(Node)并將其加入同步隊(duì)列,同時(shí)會(huì)阻塞當(dāng)前線程,當(dāng)同步狀態(tài)釋放時(shí),會(huì)把首節(jié)點(diǎn)中的線程喚醒,使其再次嘗試獲取同步狀態(tài)。

同步隊(duì)列中的節(jié)點(diǎn)用來保存獲取同步狀態(tài)失敗的線程應(yīng)用、等待狀態(tài)以及前驅(qū)和后繼節(jié)點(diǎn),節(jié)點(diǎn)的屬性描述如下表所示:

節(jié)點(diǎn)的屬性描述

節(jié)點(diǎn)是構(gòu)成同步隊(duì)列的基礎(chǔ),同步器擁有首節(jié)點(diǎn)(head)和尾節(jié)點(diǎn)(tail),沒有成功獲取同步狀態(tài)的線程將會(huì)成為節(jié)點(diǎn)并加入該隊(duì)列的尾部,同步隊(duì)列的結(jié)構(gòu)如下圖所示:

同步隊(duì)列的基本結(jié)構(gòu)

設(shè)置尾節(jié)點(diǎn)
同步器包含了兩個(gè)節(jié)點(diǎn)類型的引用,一個(gè)指向頭節(jié)點(diǎn),而另一個(gè)指向尾節(jié)點(diǎn)。
試想一下,當(dāng)一個(gè)線程成功地獲取了同步狀態(tài)(或者鎖),其他線程將無法獲取到同步狀態(tài),轉(zhuǎn)而被構(gòu)造成為節(jié)點(diǎn)并加入到同步隊(duì)列中,而這個(gè)加入隊(duì)列的過程必須要保證線程安全,因此同步器提供了一個(gè)基于CAS的設(shè)置尾節(jié)點(diǎn)的方法:compareAndSetTail(Node expect,Node update)。

節(jié)點(diǎn)加入同步隊(duì)列

設(shè)置首節(jié)點(diǎn)
同步隊(duì)列遵循FIFO,首節(jié)點(diǎn)是獲取同步狀態(tài)成功的節(jié)點(diǎn),首節(jié)點(diǎn)的線程在釋放同步狀態(tài)時(shí),將會(huì)喚醒后繼節(jié)點(diǎn),而后繼節(jié)點(diǎn)將會(huì)在獲取同步狀態(tài)成功時(shí)將自己設(shè)置為首節(jié)點(diǎn),該過程如圖所示:

設(shè)置首節(jié)點(diǎn)

設(shè)置首節(jié)點(diǎn)是通過獲取同步狀態(tài)成功的線程來完成的,由于只有一個(gè)線程能
夠成功獲取到同步狀態(tài),因此設(shè)置頭節(jié)點(diǎn)的方法并不需要使用CAS來保證,它只需要將首節(jié)點(diǎn)設(shè)置成為原首節(jié)點(diǎn)的后繼節(jié)點(diǎn)并斷開原首節(jié)點(diǎn)的next引用即可。

同步器的模板方法提供了三種不同的鎖獲取與釋放方法:獨(dú)占式、共享式以及獨(dú)占式超時(shí),下面分別講述這三種方法獲取與釋放鎖的過程。

(1)獨(dú)占式同步狀態(tài)的獲取與釋放

通過調(diào)用同步器的acquire(int arg)方法可以獲取同步狀態(tài),該方法對(duì)中斷不敏感,也就是由于線程獲取同步狀態(tài)失敗后進(jìn)入同步隊(duì)列中,后續(xù)對(duì)線程進(jìn)行中斷操作時(shí),線程不會(huì)從同步隊(duì)列中移出,該方法代碼如下所示。

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}
  1. 調(diào)用自定義同步器實(shí)現(xiàn)的tryAcquire(int arg)方法,該方法保證線程安全的獲取同步狀態(tài)
  2. 如果同步狀態(tài)獲取失敗,則構(gòu)造同步節(jié)點(diǎn)(獨(dú)占式Node.EXCLUSIVE,同一時(shí)刻只能有一個(gè)線程成功獲取同步狀態(tài))并通過addWaiter(Node node)方法將該節(jié)點(diǎn)加入到同步隊(duì)列的尾部
  3. 調(diào)用acquireQueued(Node node,int arg)方法,使得該節(jié)點(diǎn)以“死循環(huán)”的方式獲取同步狀態(tài)。

節(jié)點(diǎn)的構(gòu)造和添加至尾部代碼如下:

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)) {
            pred.next = node;
            return node;
        }
    }
    // CAS添加失敗,則調(diào)用enq()方法以死循環(huán)的方式保證節(jié)點(diǎn)的正確添加
    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;
            }
        }
    }
}

節(jié)點(diǎn)進(jìn)入同步隊(duì)列之后,就進(jìn)入了一個(gè)自旋的過程,每個(gè)節(jié)點(diǎn)(或者說每個(gè)線程)都在自省地觀察,當(dāng)條件滿足,獲取到了同步狀態(tài),就可以從這個(gè)自旋過程中退出,否則依舊留在這個(gè)自旋過程中(并會(huì)阻塞節(jié)點(diǎn)的線程),如下代碼所示:

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (; ; ) {
            // 獲取node的前驅(qū)節(jié)點(diǎn)
            final Node p = node.predecessor();
            // 如果node的前驅(qū)節(jié)點(diǎn)是首節(jié)點(diǎn),那么嘗試獲取鎖
            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);
    }
}

只有前驅(qū)節(jié)點(diǎn)是頭節(jié)點(diǎn)才能夠嘗試獲取同步狀態(tài),這是為什么?原因有兩個(gè):

  • 頭節(jié)點(diǎn)是成功獲取到同步狀態(tài)的節(jié)點(diǎn),而頭節(jié)點(diǎn)的線程釋放了同步狀態(tài)之后,將會(huì)喚醒其后繼節(jié)點(diǎn),后繼節(jié)點(diǎn)的線程被喚醒后需要檢查自己的前驅(qū)節(jié)點(diǎn)是否是頭節(jié)點(diǎn)。
  • 維護(hù)同步隊(duì)列的FIFO原則。
節(jié)點(diǎn)自旋獲取同步狀態(tài)

acquire()方法調(diào)用流程:

獨(dú)占式同步狀態(tài)獲取流程

當(dāng)前線程獲取同步狀態(tài)并執(zhí)行了相應(yīng)邏輯之后,就需要釋放同步狀態(tài),使得后續(xù)節(jié)點(diǎn)能夠繼續(xù)獲取同步狀態(tài)。通過調(diào)用同步器的release(int arg)方法可以釋放同步狀態(tài),該方法在釋放了同步狀態(tài)之后,會(huì)喚醒其后繼節(jié)點(diǎn)(進(jìn)而使后繼節(jié)點(diǎn)重新嘗試獲取同步狀態(tài))。該方法代碼如下所示:

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            // 使用LockSupport來喚醒等待狀態(tài)的線程
            unparkSuccessor(h);
        return true;
    }
    return false;
}

(2)共享式同步狀態(tài)的獲取與釋放

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

public final void acquireShared(int arg) {
    // 嘗試獲取鎖,若失敗則調(diào)用doAcquireShared()
    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 (;;) {
            // 如果前驅(qū)節(jié)點(diǎn)是首節(jié)點(diǎn),則嘗試獲取鎖
            final Node p = node.predecessor();
            if (p == head) {
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null;
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

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

public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容