Java并發(fā)編程 - ReentrantReadWriteLock

ReentrantReadWriteLock使用示例

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class ReadWriteLockTest {

    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private Lock readLock = lock.readLock();
    private Lock writeLock = lock.writeLock();

    public void read() {
        try {
            readLock.lock();
            try {
                System.out.println("獲得讀鎖 " + Thread.currentThread().getName() + " " + System.currentTimeMillis());
                Thread.sleep(10000);
            } finally {
                readLock.unlock();
            }
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
    }

    public void write() {
        try {
            writeLock.lock();
            try {
                System.out.println("獲得寫鎖 " + Thread.currentThread().getName() + " " + System.currentTimeMillis());
                Thread.sleep(10000);
            } finally {
                writeLock.unlock();
            }
        } catch (InterruptedException ex) {
            ex.printStackTrace();
        }
    }

    public static void main(String[] args) {
        ReadWriteLockTest readWriteLockTest = new ReadWriteLockTest();

        Thread a = new Thread(()->{
            readWriteLockTest.read();
        }, "t-A");

        Thread b = new Thread(()->{
            readWriteLockTest.write();
        }, "t-B");

        a.start();
        b.start();
    }

}

ReentrantReadWriteLock初始化

ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

#ReentrantReadWriteLock構造方法

ReentrantReadWriteLock.java

public ReentrantReadWriteLock() {
    this(false);
}

/**
 * Creates a new {@code ReentrantReadWriteLock} with
 * the given fairness policy.
 *
 * @param fair {@code true} if this lock should use a fair ordering policy
 */
public ReentrantReadWriteLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
    readerLock = new ReadLock(this);
    writerLock = new WriteLock(this);
}

實例化ReentrantReadWriteLock,其內(nèi)部會自動創(chuàng)建ReadLock和WriteLock對象。

ReentrantReadWriteLock.ReadLock

protected ReadLock(ReentrantReadWriteLock lock) {
    sync = lock.sync;
}

ReentrantReadWriteLock.WriteLock

protected WriteLock(ReentrantReadWriteLock lock) {
    sync = lock.sync;
}

讀鎖和寫鎖使用的是同一個同步器。這就意味著state和同步隊列被讀鎖和寫鎖共享。

#同步器靜態(tài)變量

來看同步器中的幾個靜態(tài)變量:

ReentrantReadWriteLock.Sync

static final int SHARED_SHIFT   = 16;
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

初始值如下:

  • SHARED_SHIFT = 16
  • SHARED_UNIT = 65536(1 0000 0000 0000 0000)
  • MAX_COUNT = 65535(1111 1111 1111 1111)
  • EXCLUSIVE_MASK = 65535(1111 1111 1111 1111)

#條件隊列

讀鎖未實現(xiàn)條件隊列,寫鎖實現(xiàn)了條件隊列。

ReentrantReadWriteLock.ReadLock

/**
 * Throws {@code UnsupportedOperationException} because
 * {@code ReadLocks} do not support conditions.
 *
 * @throws UnsupportedOperationException always
 */
public Condition newCondition() {
    throw new UnsupportedOperationException();
}

ReentrantReadWriteLock.WriteLock

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

讀鎖

加鎖流程分析

調(diào)用lock方法進行加鎖:

ReadLock

 /**
 * Acquires the read lock.
 *
 * <p>Acquires the read lock if the write lock is not held by
 * another thread and returns immediately.
 *
 * <p>If the write lock is held by another thread then
 * the current thread becomes disabled for thread scheduling
 * purposes and lies dormant until the read lock has been acquired.
 */
public void lock() {
    sync.acquireShared(1);
}

這個方法的注釋說了:如果有其他線程持有讀鎖,那么當前獲取鎖的這個線程就會等待直到獲取到鎖。

AbstractQueuedSynchronizer.java

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}
@@通過tryAcquireShared獲取令牌

ReentrantReadWriteLock.Sync

protected final int tryAcquireShared(int unused) {
    /*
     * Walkthrough:
     * 1. If write lock held by another thread, fail.
     * 2. Otherwise, this thread is eligible for
     *    lock wrt state, so ask if it should block
     *    because of queue policy. If not, try
     *    to grant by CASing state and updating count.
     *    Note that step does not check for reentrant
     *    acquires, which is postponed to full version
     *    to avoid having to check hold count in
     *    the more typical non-reentrant case.
     * 3. If step 2 fails either because thread
     *    apparently not eligible or CAS fails or count
     *    saturated, chain to version with full retry loop.
     */
    Thread current = Thread.currentThread();
    int c = getState();
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    int r = sharedCount(c);
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        if (r == 0) {
            firstReader = current;
            firstReaderHoldCount = 1;
        } else if (firstReader == current) {
            firstReaderHoldCount++;
        } else {
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                cachedHoldCounter = rh = readHolds.get();
            else if (rh.count == 0)
                readHolds.set(rh);
            rh.count++;
        }
        return 1;
    }
    return fullTryAcquireShared(current);
}

1. 判斷是否有其他線程持有寫鎖

如果有其他線程持有寫鎖,那么獲取讀鎖的當前線程就要被掛起。

這個判斷是下面這段邏輯代碼:

if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
    return -1;

看下exclusiveCount方法:

ReentrantReadWriteLock.Sync

static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

EXCLUSIVE_MASK初始化時已經(jīng)給出:

EXCLUSIVE_MASK = 65535(1111 1111 1111 1111)

假設只有讀鎖,怎么樣保證exclusiveCount的結果為0呢?這就關系到讀鎖設置state的方式。

讀鎖修改state的方法如下:

compareAndSetState(c, c + SHARED_UNIT)

SHARED_UNIT初始化時已經(jīng)給出:

SHARED_UNIT  = 65536(1 0000 0000 0000 0000)

現(xiàn)在我們假設有A、B、C三個線程,我們看一下執(zhí)行這個設置后state的值是多少:

線程A執(zhí)行完: 1 0000 0000 0000 0000
線程B執(zhí)行完:10 0000 0000 0000 0000
線程C執(zhí)行完:11 0000 0000 0000 0000

現(xiàn)在回到exclusiveCount方法:

static int exclusiveCount(int c) { return c & 65535(1111 1111 1111 1111); }

上面例子中可以看到state每次執(zhí)行完后16位都是0,而65535二進制是16位1,&之后返回值為0。

讀鎖修改state規(guī)則:加鎖加65536(1 0000 0000 0000 0000),解鎖減65536(1 0000 0000 0000 0000)。

保證后16位為0

那么如果有寫鎖,怎么保證這里的返回不為0?其實也沒那么復雜,只要保證寫鎖加鎖對state設值之后后16位不為0就行了。

 setState(c + acquires);// acquires為1

寫鎖修改state規(guī)則:加鎖加1,解鎖減1。

讀鎖請求加鎖只要觀察到state的二進制數(shù)后16位不全為0,則說明有寫鎖操作了。

如果當前線程觀察到有其他線程正持有寫鎖,則停止tryAcquireShared執(zhí)行,當前線程會被加入到同步隊列中。

2. 獲取讀鎖被持有數(shù)量

上面我們說過讀鎖加鎖每次state都會加65536(1 0000 0000 0000 0000),那么state如何反應讀鎖被持有的數(shù)量呢?通過下面的方法獲取:

int r = sharedCount(c);

ReentrantReadWriteLock.Sync

static int sharedCount(int c)    { return c >>> SHARED_SHIFT;// SHARED_SHIFT = 16 }

還是拿上面那個例子:

線程A執(zhí)行完: 1 0000 0000 0000 0000 // 無符號右移16位 = 0000 0000 0000 00001 = 1
線程B執(zhí)行完:10 0000 0000 0000 0000// 無符號右移16位 = 00 0000 0000 0000 0010 = 2
線程C執(zhí)行完:11 0000 0000 0000 0000// 無符號右移16位 = 00 0000 0000 0000 0011 = 3

3. 判斷讀是否已經(jīng)阻塞

代碼如下:

readerShouldBlock() 

線程能執(zhí)行到這里,說明當前沒有其他線程持有寫鎖。那么線程是否就可以繼續(xù)執(zhí)行呢?不一定,還要根據(jù)上面的方法來判斷。

我們可以看到readerShouldBlock是定義在Sync中的抽象方法,具體邏輯由子類來實現(xiàn)。子類有FairSync和NonfairSync,公平和非公平模式,所以兩種模式下會有不同的處理結果。

#公平模式

ReentrantReadWriterLock.FairSync

final boolean readerShouldBlock() {            
    return hasQueuedPredecessors();
}

hasQueuedPredecessors在AbstractQueuedSynchronizer中定義:

AbstractQueuedSynchronizer

public final boolean hasQueuedPredecessors() {
    // The correctness of this depends on head being initialized
    // before tail and on head.next being accurate if the current
    // thread is first in queue.
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

公平模式說明:公平模式下,請求讀鎖的線程要檢查同步隊列,如果同步隊列不為空,并且head節(jié)點的后繼節(jié)點代表的線程不是當前線程,那么就阻塞當前線程。

這里邏輯跟我們ReentrantLock的公平模式一樣,公平模式下新來的線程需要檢查同步隊列是否有比它等待獲取鎖時間更長的線程存在。

#非公平模式

ReentrantReadWriteLock.Nonfair

final boolean readerShouldBlock() {
    /* As a heuristic to avoid indefinite writer starvation,
     * block if the thread that momentarily appears to be head
     * of queue, if one exists, is a waiting writer.  This is
     * only a probabilistic effect since a new reader will not
     * block if there is a waiting writer behind other enabled
     * readers that have not yet drained from the queue.
     */
    return apparentlyFirstQueuedIsExclusive();
}

apparentlyFirstQueuedIsExclusive在AbstractQueuedSynchrinizer中定義:

AbstractQueuedSynchrinizer.java

/**
 * Returns {@code true} if the apparent first queued thread, if one
 * exists, is waiting in exclusive mode.  If this method returns
 * {@code true}, and the current thread is attempting to acquire in
 * shared mode (that is, this method is invoked from {@link
 * #tryAcquireShared}) then it is guaranteed that the current thread
 * is not the first queued thread.  Used only as a heuristic in
 * ReentrantReadWriteLock.
 */
final boolean apparentlyFirstQueuedIsExclusive() {
    Node h, s;
    return (h = head) != null &&
        (s = h.next)  != null &&
        !s.isShared()         &&
        s.thread != null;
}

在ReentrantLock非公平模式下新來的線程可以闖入式的獲取鎖而不用像公平模式那樣去關心同步隊列。

但是ReentrantReadWriteLock的非公平模式的機制有些不同,因為讀寫互斥,如果請求讀鎖的當前線程發(fā)現(xiàn)同步隊列的head節(jié)點的下一個節(jié)點非互斥等待的節(jié)點,那么就說明有一個線程在等待獲取寫鎖(爭搶寫鎖失敗,被放入到同步隊列中),那么請求讀鎖的線程就要阻塞。

非公平模式說明:非公平模式下請求讀鎖的線程要檢查是否有其他線程正嘗試獲取讀鎖(同步隊列head的后繼節(jié)點為互斥等待模式的節(jié)點)。

公平模式 vs 非公平模式:公平模式只要判斷有比當前線程等待獲取鎖(讀鎖或?qū)戞i)時間更長的線程存在,則阻塞;非公平模式判斷是否有等待獲取寫鎖的線程存在,如果有,則阻塞。

4. 當前讀鎖總數(shù)判斷

r < MAX_COUNT

MAX_COUNT = 65535,什么時候會達到最大值?請求讀鎖的線程獲得讀鎖后不釋放,直到請求次數(shù)達到最大值。

5. 通過CAS更新state值

compareAndSetState(c, c + SHARED_UNIT)

這個1中已經(jīng)說明了。

6. 執(zhí)行if塊邏輯

3,4,5步條件都滿足,則進入if語句塊進行處理。

#6.1 處理第一次獲取讀鎖的線程

if (r == 0) {
    firstReader = current;
    firstReaderHoldCount = 1;
}

ReentrantReadWriteLock.Sync

/**
 * firstReader is the first thread to have acquired the read lock.
 * firstReaderHoldCount is firstReader's hold count.
 *
 * <p>More precisely, firstReader is the unique thread that last
 * changed the shared count from 0 to 1, and has not released the
 * read lock since then; null if there is no such thread.
 *
 * <p>Cannot cause garbage retention unless the thread terminated
 * without relinquishing its read locks, since tryReleaseShared
 * sets it to null.
 *
 * <p>Accessed via a benign data race; relies on the memory
 * model's out-of-thin-air guarantees for references.
 *
 * <p>This allows tracking of read holds for uncontended read
 * locks to be very cheap.
 */
private transient Thread firstReader = null;
private transient int firstReaderHoldCount;

#6.2 處理第一次獲取讀鎖的線程的重入

else if (firstReader == current) {
    firstReaderHoldCount++;
}

#6.3 處理非第一次獲取讀鎖的線程

6.1、6.2條件不滿足則繼續(xù)執(zhí)行:

 else {
    HoldCounter rh = cachedHoldCounter;
    if (rh == null || rh.tid != getThreadId(current))
        cachedHoldCounter = rh = readHolds.get();
    else if (rh.count == 0)
        readHolds.set(rh);
    rh.count++;
}

我們需要為每一個線程記錄它持有讀鎖的次數(shù),代碼中通過ThreadLocal來解決這樣的需求。

線程和它獲取讀鎖次數(shù)的關聯(lián),通過HoldCounter包裝起來:

AbstractQueuedSynchronizer.Sync

/**
 * A counter for per-thread read hold counts.
 * Maintained as a ThreadLocal; cached in cachedHoldCounter
 */
static final class HoldCounter {
    int count = 0;
    // Use id, not reference, to avoid garbage retention
    final long tid = getThreadId(Thread.currentThread());
}

HoldCounter對象通過ThreadLocalHoldCounter對象進行操作,ThreadLocalHoldCounter繼承自ThreadLocal。

ReentrantReadWriteLock.Sync

static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> {
    public HoldCounter initialValue() {
        return new HoldCounter();
    }
}

這里涉及到ThreadLocal的使用,這里就不多講,線程有關HoldCounter的內(nèi)部數(shù)據(jù)如下:

Thread-HoldCounter.png

cachedHoldCounter做緩存使用。

6執(zhí)行完后,返回1,表示線程獲取讀鎖成功。

7. 執(zhí)行fullTryAcquireShared

3,4,5步條件不能全滿足,則執(zhí)行fullTryAcquireShared。

可以看到上面我們設置state的時候使用了CAS,不過因為讀鎖是共享鎖,所以可能會有其他線程也在執(zhí)行CAS操作(獲取寫鎖的線程也可能會與它搶著修改state),那么當前線程執(zhí)行CAS就會失敗,像其他共享模式的代碼我們可以看到通常會自旋處理CAS的設置,而我們上面的代碼卻沒有。所以為了處理這一情況,當CAS設置失敗,那么就執(zhí)行fullTryAcquireShared方法,fullTryAcquireShared會采用自旋的方式,fullTryAcquireShared的代碼有很多與tryAcquireShared重復,這里就不做解釋。

  • @@doAcquireShared

回到acquireShared方法:

AbstractQueuedSynchronizer.java

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

當線程獲得讀鎖失敗,那么就會執(zhí)行doAcquireShared方法,執(zhí)行這個會使得線程在同步隊列中已共享模式等待。具體參考共享鎖的分析。

參考:Java并發(fā)編程 - 共享鎖

2. 解鎖流程分析

ReentrantReadWriteLock.java

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

AbstractQueuedSynchronizer.java

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

ReentrantReadWriteLock.Sync

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))
            // Releasing the read lock has no effect on readers,
            // but it may allow waiting writers to proceed if
            // both read and write locks are now free.
            return nextc == 0;
    }
}

這個方法主要作用就是重設state的值并減計數(shù)值。

這里注意一下:nextc == 0,只有讀鎖全部釋放之后,才會通知同步隊列中等待的獲取寫鎖而被阻塞的線程。

  • @@doReleaseShared
    與@@doAcquireShared一樣,涉及到共享鎖模式的實現(xiàn)原理,這里也不做說明。

參考:Java并發(fā)編程 - 共享鎖

寫鎖

加鎖流程分析

ReentrantReadWriteLock.java

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

AbstractQueuedSynchronizer.java

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

ReentrantReadWriteLock.Sync

protected final boolean tryAcquire(int acquires) {
    /*
     * Walkthrough:
     * 1. If read count nonzero or write count nonzero
     *    and owner is a different thread, fail.
     * 2. If count would saturate, fail. (This can only
     *    happen if count is already nonzero.)
     * 3. Otherwise, this thread is eligible for lock if
     *    it is either a reentrant acquire or
     *    queue policy allows it. If so, update state
     *    and set owner.
     */
    Thread current = Thread.currentThread();
    int c = getState();
    int w = exclusiveCount(c);
    if (c != 0) {
        // (Note: if c != 0 and w == 0 then shared count != 0)
        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;
}

這段邏輯的描述的第一條:

If read count nonzero or write count nonzero and owner is a different thread, fail.

如果讀的數(shù)量非零或者寫的數(shù)量非零并且寫鎖持有者不是當前持有者那么就失敗。

現(xiàn)在來看看上面說的控制是怎么實現(xiàn)的。

#第1種:讀的數(shù)量非零,失敗

int c = getState();
int w = exclusiveCount(c);

讀的數(shù)量非零,則說明已經(jīng)有線程持有了讀鎖,根據(jù)我們上面讀鎖獲取鎖的分析,state的值就是每次加上65536,比如:

線程A執(zhí)行完: 1 0000 0000 0000 0000
線程B執(zhí)行完:10 0000 0000 0000 0000
線程C執(zhí)行完:11 0000 0000 0000 0000

如果state的值是這樣的,這時候通過exclusiveCount獲取寫鎖的數(shù)量,

static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

得到的就是0。

下面有判斷:

if (w == 0)
    return false;

w == 0, 失敗。

讀鎖先被某線程請求獲得到,并且未釋放,其他線程就無法請求獲得寫鎖。

#第2種:寫的數(shù)量非零并且寫鎖持有者不是當前持有者那么就失敗

寫鎖是互斥的。

當寫鎖已經(jīng)被其他線程持有,當前線程進入此方法,會觀察到c!=0&&w!=0,但是exclusiveOwnerThread不是當前線程,則無法獲得讀鎖。

if (current != getExclusiveOwnerThread())
    return false;

看代碼注釋第二條:

If count would saturate, fail. (This can only happen if count is already nonzero.)

鎖不被別同一個線程無限制地請求,而沒有任何地釋放。

if (w + exclusiveCount(acquires) > MAX_COUNT)
    throw new Error("Maximum lock count exceeded");

上面我們分析的是已經(jīng)有線程對state修改的情況(讀鎖被搶到或?qū)戞i被搶到)。

現(xiàn)在來分析state=0的情況。

會有這種情況:同時有一個線程在獲取讀鎖,另一個線程在獲取寫鎖。但是兩者都還沒成功修改state,這時候state = 0,如何處理呢?

看下面代碼:

if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
    return false;

條件判斷1:判斷寫是否應該被阻塞

和上面說的readerShouldBlock一樣,也分為公平模式和非公平模式。

#非公平模式

ReentrantReadWriteLock.NonfairSync

final boolean writerShouldBlock() {
    return false; // writers can always barge
}

#公平模式

ReentrantReadWriteLock.FairSync

final boolean writerShouldBlock() {
    return hasQueuedPredecessors();
}

條件判斷2:判斷CAS設置是否成功設置

這個很好理解了,搶奪設置CAS失敗,那么說明有其他線程在對state操作,不論它是獲取讀鎖的線程還是獲取寫鎖的線程。

  • @@acquireQueued

這個在ReentrantLock的講解中已經(jīng)分析過,這里不做分析。

解鎖流程分析

ReentrantReadWriteLock.java

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

ReentrantReadWriteLock.Sync

protected final boolean tryRelease(int releases) {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    int nextc = getState() - releases;
    
    if (free)
        setExclusiveOwnerThread(null);
    setState(nextc);
    return free;
}

這個方法比較簡單,不做分析。

boolean free = exclusiveCount(nextc) == 0,寫鎖完全釋放才會喚醒同步隊列中的等待線程。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內(nèi)容