java併發程式設計 | 鎖詳解:AQS,Lock,ReentrantLock,ReentrantReadWriteLock

兩顆西柚發表於2019-04-29

原文:chenmingyu.top/concurrent-…

鎖是用來控制多個執行緒訪問共享資源的方式,java中可以使用synchronizedLock實現鎖的功能

synchronized是java中的關鍵字,隱藏獲取和釋放鎖的過程,Lock是java中的介面,需要主動的獲取鎖和釋放鎖,synchronized是排他鎖,而Lock支援可中斷獲取鎖,超時獲取鎖

Lock提供的介面

public interface Lock {

    /**
     * 獲取鎖,呼叫該方法後當前執行緒獲取鎖,獲取到鎖之後從該方法返回
     */
    void lock();

    /**
     * 可中斷的獲取鎖,在獲取鎖的過程中可以中斷當前執行緒
     */
    void lockInterruptibly() throws InterruptedException;

    /**
     * 嘗試非阻塞的獲取鎖,呼叫方法後立即返回,獲取到鎖則返回true,否則返回false
     */
    boolean tryLock();

    /**
     * 超時獲取鎖,在超時時間內獲取到鎖,在超時時間被中斷,超時時間內為獲取到鎖,三種情況下會從該方法返回
     */
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

    /**
     * 釋放鎖
     */
    void unlock();

    /**
     * 獲取等待通知元件,只有當前執行緒獲取到鎖之後才可以呼叫該元件的wait()方法,釋放鎖
     */
    Condition newCondition();
}
複製程式碼

佇列同步器

佇列同步器AbstractQueuedSynchronizerAQS簡稱同步器)是用來構建鎖或者其他同步元件的基礎框架

java中鎖的實現基本都是通過聚合了一個同步器的子類完成執行緒訪問控制的,同步器是實現鎖的關鍵,可以這麼理解,鎖面向程式設計者,隱藏了實現細節,同步器面向鎖的實現,簡化了鎖的實現方式,遮蔽了同步狀態管理,執行緒排隊,等待與喚醒等底層操作,通過AbstractQueuedSynchronizer我們可以很方便的實現一個鎖

設計原則

同步器的設計基於模板方法模式,提供的模板方法主要包括:獨佔鎖獲取鎖與釋放同步狀態,共享式獲取與釋放同步狀態,獲取同步佇列中等待執行緒情況

獨佔式操作

想要實現一個獨佔式鎖需要重寫以下方法

方法名 描述
void acquire(int arg) 獨佔式獲取同步狀態,同一時刻只能有一個執行緒可以獲取到同步狀態,獲取失敗進入同步佇列等待
void acquireInterruptibly(int arg) 獨佔式獲取同步狀態,響應中斷操作,被中斷時會拋異常並返回
boolean tryAcquireNanos(int arg, long nanosTimeout) 獨佔式獲取同步狀態,響應中斷操作,並且增加了超時限制,如果規定時間沒有獲得同步狀態就返回false,否則返回true
boolean release(int arg) 獨佔式釋放同步狀態,在釋放同步狀態之後,將同步佇列中的第一個節點包含的執行緒喚醒

共享式操作

想要實現一個共享鎖需要重寫以下方法

方法名 描述
void acquireShared(int arg) 共享式獲取同步狀態,同一時刻可以有多個執行緒獲取到同步狀態
void acquireSharedInterruptibly(int arg) 共享式獲取同步狀態,響應中斷操作
boolean tryAcquireSharedNanos(int arg, long nanosTimeout) 共享式獲取同步狀態,響應中斷操作,並且增加了超時限制,如果規定時間沒有獲得同步狀態就返回false,否則返回true
boolean releaseShared(int arg) 共享式釋放同步狀態
獲取同步佇列執行緒資訊
方法名 描述
Collection getQueuedThreads() 獲取同步佇列上的執行緒集合

在這些模板方法中,多次提到了同步佇列,我們看一下AQS是如何實現同步佇列的

首先看下AbstractQueuedSynchronizer的類圖

java併發程式設計 | 鎖詳解:AQS,Lock,ReentrantLock,ReentrantReadWriteLock

Node

Node類是AbstractQueuedSynchronizer類的內部類,同步器依靠內部的一個同步佇列來完成同步狀態的管理,當前執行緒獲取同步狀態失敗的時候,同步器會將當前執行緒及等待資訊構造成一個Node節點加入到同步佇列中

屬性 描述
waitStatus 該執行緒等待狀態,包含如下:
CANCELLED 值為1,表示需要從同步佇列中取消等待
SIGNAL值為-1,表示後繼節點處於等待狀態,如果當前節點釋放同步狀態會通知後繼節點,使得後繼節點的執行緒能夠執行
CONDITION值為-2,表示節點在等待佇列中
PROPAGATE值為-3,表示下一次共享式同步狀態獲取將會無條件傳播下去
INITIAL值為0,表示初始狀態
prev:Node 前驅節點
next:Node 後繼節點
thread:Thread 當前執行緒
nextWaiter:Node 下一個等待節點

可以看到AQS中的節點資訊包含前驅和後繼節點,所以我們知道了AQS的同步佇列是雙向連結串列結構的

AQS

AQS中的幾個重要屬性

屬性 描述
state:int 同步狀態:如果等於0,鎖屬於空閒狀態,如果等於1,標識鎖被佔用,如果大於1,則表示鎖被當前持有的執行緒多次加鎖,即重入狀態
head:Node 佇列的頭節點
tail:Node 佇列的尾節點
unsafe:Unsafe AQS中的cas演算法實現

AQS中提供了三個方法對同步狀態進行操作

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

AQS的基本結構如下圖所示

java併發程式設計 | 鎖詳解:AQS,Lock,ReentrantLock,ReentrantReadWriteLock

在同步器中headtail的節點的引用指向同步佇列的頭,尾節點,這樣在後面操作節點入列和出列的時候只需要操作同步器中的headtail節點就可以

獨佔式鎖

ReentrantLock

ReentrantLock重入鎖,內部AQS的實現是基於獨佔式獲取/釋放同步狀態的。我們學習一下ReentrantLock的實現原理來進一步加深對AQS的理解

重進入是指任意執行緒在獲取到鎖之後能夠再次獲取該鎖而不會被鎖阻塞,它表示一個執行緒可以對資源重複加鎖,同時支援獲取鎖時使用公平鎖還是非公平鎖

例:

/**
 * @author: chenmingyu
 * @date: 2019/4/12 15:09
 * @description: ReentrantLock
 */
public class ReentrantLockTest {

    private static Lock LOCK = new ReentrantLock();

    public static void main(String[] args) {
        Runnable r1 = new TestThread();
        new Thread(r1,"r1").start();
        Runnable r2 = new TestThread();
        new Thread(r2,"r2").start();
    }

    public static class TestThread implements Runnable{
        
        @Override
        public void run() {
            LOCK.lock();
            try {
                System.out.println(Thread.currentThread().getName()+":獲取到鎖 "+LocalTime.now());
                TimeUnit.SECONDS.sleep(3L);
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                LOCK.unlock();
            }
        }
    }
}
複製程式碼

輸出

java併發程式設計 | 鎖詳解:AQS,Lock,ReentrantLock,ReentrantReadWriteLock

只有在r1執行緒釋放鎖之後r2執行緒才獲取到鎖去執行程式碼列印資料

原始碼分析

建立的例項,預設使用非公平鎖,如果需要公平鎖,需要呼叫有參的建構函式

/**
 * 非公平鎖
 * 建立ReentrantLock例項,預設使用非公平鎖
 */
public ReentrantLock() {
    sync = new NonfairSync();
}

/**
 * 公平鎖
 * 建立ReentrantLock例項,fair為true使用公平鎖
 */
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}
複製程式碼

NonfairSyncFairSync都是ReentrantLock類的內部類,繼承自ReentrantLock類的內部類SyncSync類繼承了AbstractQueuedSynchronizer

類圖如下

java併發程式設計 | 鎖詳解:AQS,Lock,ReentrantLock,ReentrantReadWriteLock

獨佔式鎖的獲取

非公平鎖的實現

/**
 * Performs lock.  Try immediate barge, backing up to normal
 * acquire on failure.
 */
final void lock() {
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1);
}
複製程式碼

非公平鎖會在呼叫lock()方法的時候首先呼叫compareAndSetState(0, 1)方法嘗試獲取鎖,如果沒有獲取到鎖則呼叫acquire(1)方法

compareAndSetState(0, 1)方法是一個CAS操作,如過設定成功,則為獲取到同步狀態,並呼叫setExclusiveOwnerThread(Thread.currentThread());方法將當前執行緒設定為獨佔模式同步狀態的所有者

我們所說的獲取同步狀態其實指的就是獲取鎖的狀態,獲取同步狀態成功則加鎖成功

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

acquire(1)方法是提供的模板方法,呼叫tryAcquire(arg)acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

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

tryAcquire(arg)方法呼叫的是子類的實現,NonfairSynctryAcquire方法

protected final boolean tryAcquire(int acquires) {
    return nonfairTryAcquire(acquires);
}
複製程式碼

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()) {
        /**
         * 首先根據`getState()`方法獲取同步狀態,如果等於0嘗試呼叫`compareAndSetState(0, 	            * acquires)`方法獲取同步狀態,如果設定成功則獲取同步狀態成功,設定當前執行緒為獨佔模式同步狀態的          * 所有者
         */
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}
複製程式碼
  1. 根據getState()方法獲取同步狀態,如果等於0嘗試呼叫compareAndSetState(0, acquires)方法獲取同步狀態,如果設定成功則獲取同步狀態成功,設定當前執行緒為獨佔模式同步狀態的所有者
  2. 如果當前執行緒等於獨佔式同步狀態所有者的執行緒,那麼就將state+1,表示當前執行緒多次加鎖

如果tryAcquire(arg) 返回false,表示沒有獲取到同步狀態,即沒有拿到鎖,所以需要呼叫 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法將當前執行緒加入到同步佇列中,並且以死迴圈的方式獲取同步狀態,如果獲取不到則阻塞節點中的執行緒,而被阻塞的執行緒只能通過前驅節點的出隊,或者阻塞執行緒被中斷來實現喚醒

addWaiter(Node.EXCLUSIVE)方法的作用就是構造同步佇列的節點資訊,然後加入到同步佇列尾部

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;
}

複製程式碼

首先呼叫Node類的構造方法建立一個例項,tailAQS中佇列的尾節點

如果tail節點不為空,將例項的前驅節點置為tail指向的節點,然後呼叫compareAndSetTail(pred, node)方法,compareAndSetTail(pred, node)方法呼叫unsafe.compareAndSwapObject(this, tailOffset, expect, update),此方法是一個CAS操作,不可中斷,用來保證節點能夠被執行緒安全的新增,設定成功後,將節點tail的後繼節點指向當前例項,以此來實現將當前例項加入到同步佇列尾部

如果tail節點等於空或者compareAndSetTail(pred, node)設定失敗,則會呼叫enq(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;
            }
        }
    }
}

複製程式碼

在這個方法中利用for迴圈構造了一個死迴圈,如果當前AQStail節點為空,則證明當前同步佇列中沒有等待的執行緒,也就是沒有節點,呼叫compareAndSetHead(new Node())方法構造了一個頭節點,然後迴圈呼叫compareAndSetTail(t, node)將當前例項加入到佇列的尾部,如果失敗就一直呼叫,直到成功為止

java併發程式設計 | 鎖詳解:AQS,Lock,ReentrantLock,ReentrantReadWriteLock

在呼叫addWaiter(Node mode)方法後會呼叫acquireQueued(final Node node, int arg)方法,作用是在每個節點進入到同步佇列中後就進入了一個自旋的狀態,通過校驗自己的前驅節點是否是頭節點,並且是否獲取到同步狀態為條件進行判斷,如果滿足條件則從自旋中退出,負責一直自旋

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);
    }
}

複製程式碼

方法內也是一個for的死迴圈,通過node.predecessor()方法獲取傳入的Node例項的前驅節點並與AQShead節點進行比較,如果相等,則嘗試獲取同步狀態獲取鎖,如果獲取成功就呼叫setHead(node);方法將當前Node例項節點設定為head節點,將原來head節點的後繼節點置為null,有助於GC回收

setHead(node);

private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}

複製程式碼

如果傳入的Node例項的前驅節點與AQShead節點不相等或者獲取同步狀態失敗,則呼叫shouldParkAfterFailedAcquire(p, node)parkAndCheckInterrupt()方法

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don't park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

複製程式碼

通過CAS操作,設定節點的前驅節點等待狀態為Node.SIGNAL,如果設定失敗,返回false,因為外層是死迴圈,會重複當前方法直到設定成功

parkAndCheckInterrupt()方法呼叫LookSupport.park()阻塞執行緒,然後清除掉中斷標識

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

複製程式碼

acquireQueued(addWaiter(Node.EXCLUSIVE), arg)方法返回後,呼叫selfInterrupt(),將執行緒中斷

公平鎖的實現

在瞭解acquire(1);方法的作用之後,在理解公平鎖的實現就容易了

final void lock() {
    acquire(1);
}

複製程式碼

對比非公平鎖的實現少了一步上來就獲取同步狀態的操作,其餘操作跟非公平鎖的實現一樣

公平鎖與非公平鎖總結

  1. 公平鎖,在加鎖之前如果有同步對列,則加入到同步佇列尾部
  2. 非公平鎖,在加鎖之前不管有沒有同步佇列,先嚐試獲取同步狀態,獲取不到在加入到同步佇列尾部
  3. 非公平鎖比公平鎖效率要高很多,公平鎖保證了同步狀態的獲取按照FIFO原則,代價是需要進行大量的執行緒切換,而非公平鎖情況下,當前執行緒在釋放了同步狀態之後再次獲取到同步狀態的記錄非常大,可以減少大量的執行緒切換,但是可能會出現在同步佇列中的某個執行緒一直獲取不到鎖的情況

獨佔式獲取鎖的流程

java併發程式設計 | 鎖詳解:AQS,Lock,ReentrantLock,ReentrantReadWriteLock

獨佔式鎖的釋放

ReentrantLockunlock()方法實際呼叫的AQSrelease(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;
}

複製程式碼

首先呼叫tryRelease(arg)釋放同步狀態

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,如果此時c==0則釋放鎖,將當前獨佔式鎖的擁有執行緒置為null,然後設定state為0

然後呼叫unparkSuccessor(Node node)方法喚醒後繼節點的執行緒

private void unparkSuccessor(Node node) {

    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        /*
         * 喚醒後繼節點的執行緒
         */
        LockSupport.unpark(s.thread);
}

複製程式碼

總結一下獨佔式獲取鎖和釋放鎖的過程:

  1. 獲取鎖的時候,首先會獲取同步狀態,如果獲取成功則加鎖成功,如果獲取失敗,將當前執行緒資訊構造成節點資訊並則加入到AQS維護的同步佇列的尾部,並且開始自旋,跳出自旋的條件就是前驅節點為AQS的頭節點並且獲取到了同步狀態,此時將節點移除同步佇列
  2. 釋放鎖的時候,首先會釋放同步狀態,然後喚醒節點的後繼節點
  3. 一個執行緒N次加鎖之後,在釋放鎖的時候需要釋放N次,之後才會被別的執行緒獲取到鎖
自己實現一個獨佔式鎖

在瞭解了ReentrantLock的實現原理之後,我們就可以仿照著自己去實現一個自定義獨佔式鎖了

步驟

  1. 建立一個LockTest類,實現Lock介面,重寫必要的介面
  2. LockTest類裡建立一個內部類Sync,繼承AQS,因為要實現獨佔式鎖,所以重寫tryAcquire(int arg)tryRelease(int arg)方法就可以了

LockTest程式碼

/**
 * @author: chenmingyu
 * @date: 2019/4/11 15:11
 * @description: 自定義獨佔式鎖
 */
public class LockTest implements Lock{

    private final Sync SYNC = new Sync();

    public static class Sync extends AbstractQueuedSynchronizer{

        @Override
        protected boolean tryAcquire(int arg) {
            if(compareAndSetState(0,1)){
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        @Override
        protected boolean tryRelease(int arg) {
            if(getState()<1){
                throw new IllegalMonitorStateException("釋放同步狀態不可小於1");
            }
            int c = getState() - arg;
            if (c == 0) {
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return true;
        }
    }

    @Override
    public void lock() {
        SYNC.acquire(1);
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        SYNC.acquireInterruptibly(1);
    }

    @Override
    public boolean tryLock() {
        return SYNC.tryAcquire(1);
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

    @Override
    public void unlock() {
        SYNC.release(1);
    }

    @Override
    public Condition newCondition() {
        return null;
    }
}

複製程式碼
驗證
/**
 * @author: chenmingyu
 * @date: 2019/4/12 15:09
 * @description: LockTest
 */
public class ReentrantLockTest {

    private static Lock LOCKTEST = new LockTest();

    public static void main(String[] args) {
        Runnable r1 = new TestThread();
        new Thread(r1,"LockTest 1").start();
        Runnable r2 = new TestThread();
        new Thread(r2,"LockTest 2").start();
    }

    public static class TestThread implements Runnable{

        @Override
        public void run() {
            LOCKTEST.lock();
            try {
                System.out.println(Thread.currentThread().getName()+":獲取到鎖 "+LocalTime.now());
                TimeUnit.SECONDS.sleep(3L);
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                LOCKTEST.unlock();
            }
        }
    }
}

複製程式碼

輸出

java併發程式設計 | 鎖詳解:AQS,Lock,ReentrantLock,ReentrantReadWriteLock

共享式鎖

讀寫鎖

ReentrantReadWriteLock是讀寫鎖的實現,實現ReadWriteLock介面

ReentrantReadWriteLock內部同樣維護這一個Sync內部類,實現了AQS,通過重寫對應方法實現讀鎖和寫鎖

現在已經知道了同步狀態是由AQS維護的一個整型變數state,獨佔式鎖獲取到鎖時會對其進行加1,支援重入,而讀寫鎖ReentrantReadWriteLock在設計的時候也是通過一個整型變數進行讀鎖的同步狀態和寫鎖的同步狀態維護,在一個變數上維護兩種狀態就需要對整型變數進行按位分割,一個int型別的變數包含4個字元,一個字元8個bit,就是32bit,在ReentrantReadWriteLock中,高16位表示讀,低16位表示寫

寫鎖的獲取

讀寫鎖中的寫鎖,支援重進入的排它鎖

重寫ReentrantReadWriteLock的內部類Sync中的tryAcquire(int acquires)方法

protected final boolean tryAcquire(int acquires) {
    
    Thread current = Thread.currentThread();
    int c = getState();
    int w = exclusiveCount(c);
    /*
     * 1,如果同步狀態c不等於0,代表著有讀鎖或者寫鎖
     */
    if (c != 0) {
        // 2,如果c不等於0,w寫鎖的同步狀態為0,切當前執行緒不是持有鎖的執行緒,返回false
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        if (w + exclusiveCount(acquires) > MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // Reentrant acquire
        setState(c + acquires);
        return true;
    }
    if (writerShouldBlock() ||
        !compareAndSetState(c, c + acquires))
        return false;
    setExclusiveOwnerThread(current);
    return true;
}

複製程式碼

解讀

如果存在讀鎖,寫鎖不能被獲取,必須要等到其他讀執行緒釋放讀鎖,才可以獲取到寫鎖,這麼做的原因是要確保寫鎖做的操作對讀鎖可見,如果寫鎖被獲取,則其他讀寫執行緒的後續訪問均會被阻塞

寫鎖的釋放

讀寫鎖中的讀鎖,支援重進入的共享鎖

寫鎖的釋放與獨佔式鎖釋放過程相似,每次都是減少寫鎖的同步狀態,直到為0時,表示寫鎖已被釋放

讀鎖的獲取與釋放

讀鎖是一個支援重入的共享鎖,重寫ReentrantReadWriteLock的內部類Sync中的tryAcquireShared(int unused)方法

protected final boolean tryReleaseShared(int unused) {
    Thread current = Thread.currentThread();
    if (firstReader == current) {
        // assert firstReaderHoldCount > 0;
        if (firstReaderHoldCount == 1)
            firstReader = null;
        else
            firstReaderHoldCount--;
    } else {
        HoldCounter rh = cachedHoldCounter;
        if (rh == null || rh.tid != getThreadId(current))
            rh = readHolds.get();
        int count = rh.count;
        if (count <= 1) {
            readHolds.remove();
            if (count <= 0)
                throw unmatchedUnlockException();
        }
        --rh.count;
    }
    for (;;) {
        int c = getState();
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

複製程式碼

如果其他執行緒獲取了寫鎖,則當前執行緒獲取讀鎖狀態失敗進入等待狀態,如果當前執行緒獲取了寫鎖或者寫鎖未被獲取,則當前執行緒獲取同步狀態成功,獲取到讀鎖

釋放讀鎖的時候就是每次釋放都會對同步狀態進行-1,直到為0時,表示讀鎖已被釋放

鎖降級

鎖降級是指將寫鎖降級為讀鎖,這個過程就是當前執行緒已經獲取到寫鎖的時候,在獲取到讀鎖,隨後釋放寫鎖的過程,這麼做的目的為的就是保證資料的可見性

當前執行緒A獲取到寫鎖後,對資料進行修改,之後在獲取到讀鎖,然後釋放寫鎖,完成鎖降級,這時候執行緒A還沒釋放讀鎖,別的執行緒就無法獲取到寫鎖,就無法對數進行修改,以此來保證資料的可見性

參考:java併發程式設計的藝術

推薦:

java併發程式設計 | 執行緒詳解

相關文章