Java并发编程笔记——J.U.C之locks框架:基于AQS的读写锁(5)

Java并发编程笔记——J.U.C之locks框架:基于AQS的读写锁(5)_第1张图片

一、本章概述

AQS系列的前四个章节,已经分析了AQS的原理,本章将会从ReentrantReadWriteLock出发,给出其内部利用AQS框架的实现原理。

ReentrantReadWriteLock(以下简称RRW),也就是读写锁,是一个比较特殊的同步器,特殊之处在于其对同步状态State的定义与ReentrantLock、CountDownLatch都很不同

通过RRW的分析,我们可以更深刻的了解AQS框架的设计思想,以及对“什么是资源?如何定义资源是否可以被访问?”这一命题有更深刻的理解。

二、本章示例

和之前的章节一样,本章也通过示例来分析RRW的源码。

假设现在有4个线程,ThreadA、ThreadB、ThreadC、ThreadD。
ThreadA、ThreadB、ThreadD为读线程,ThreadC为写线程:

初始时,构造RRM对象:
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock();

//ThreadA调用读锁的lock()方法

//ThreadB调用读锁的lock()方法

//ThreadC调用写锁的lock()方法

//ThreadD调用读锁的lock()方法

三、RRW的公平策略原理

1. RRW对象的创建

和ReentrantLock类似,ReentrantReadWriteLock的构造器可以选择公平/非公平策略(默认为非公平策略),RRW内部的FairSyncNonfairSync是AQS的两个子类,分别代表了实现公平策略和非公平策略的同步器:

public ReentrantReadWriteLock() {
    this(false);
}
public ReentrantReadWriteLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
    readerLock = new ReadLock(this);
    writerLock = new WriteLock(this);
}

ReentrantReadWriteLock提供了方法,分别获取读锁/写锁:

public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }

ReentrantReadWriteLock.ReadLockReentrantReadWriteLock.WriteLock其实就是两个实现了Lock接口的内部类:

public static class ReadLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = -5992448646407690164L;
    private final Sync sync;
    protected ReadLock(ReentrantReadWriteLock lock) {
        sync = lock.sync;
    }
public static class WriteLock implements Lock, java.io.Serializable {
    private static final long serialVersionUID = -4992448646407690164L;
    private final Sync sync;
    protected WriteLock(ReentrantReadWriteLock lock) {
        sync = lock.sync;
    }

2. ThreadA调用读锁的lock()方法

读锁其实是一种共享锁,实现了AQS的共享功能API,可以看到读锁的内部就是调用了AQS的acquireShared方法,该方法前面几章我们已经见过太多次了:

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

关键来看下ReentrantReadWriteLock是如何实现tryAcquireShared方法的:
读锁获取成功的条件如下:

  1. 写锁没有被其它线程占用(可被当前线程占用,这种情况属于锁降级)
  2. 等待队列中的队首没有其它线程(公平策略)
  3. 读锁重入次数没有达到最大值
  4. CAS操作修改同步状态值State成功
protected final int tryAcquireShared(int unused) {
  
    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)) {    //CAS操作,读锁重入次数加1
        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);
}

如果CAS操作失败,会调用fullTryAcquireShared方法,自旋修改State值:

//对tryAcquireShared方法的补充,以防tryAcquireShared方法的CAS操作失败
final int fullTryAcquireShared(Thread current) {
    HoldCounter rh = null;
    for (;;) {    //自旋操作
        int c = getState();
        if (exclusiveCount(c) != 0) {    //写锁被占用
            if (getExclusiveOwnerThread() != current)    //写锁占用者不是当前线程
                return -1;
            // else we hold the exclusive lock; blocking here
            // would cause deadlock.
        } else if (readerShouldBlock()) {    //判断当前线程是否需要等待
            // Make sure we're not acquiring read lock reentrantly
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
            } else {
                if (rh == null) {
                    rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current)) {
                        rh = readHolds.get();
                        if (rh.count == 0)
                            readHolds.remove();
                    }
                }
                if (rh.count == 0)
                    return -1;
            }
        }
        if (sharedCount(c) == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            if (sharedCount(c) == 0) {
                firstReader = current;
                firstReaderHoldCount = 1;
            } else if (firstReader == current) {
                firstReaderHoldCount++;
            } else {
                if (rh == null)
                    rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                else if (rh.count == 0)
                    readHolds.set(rh);
                rh.count++;
                cachedHoldCounter = rh; // cache for release
            }
            return 1;
        }
    }
}

ThreadA调用完lock方法后,等待队列结构如下:
Java并发编程笔记——J.U.C之locks框架:基于AQS的读写锁(5)_第2张图片

此时:
写锁数量:0
读锁数量:1

3. ThreadB调用读锁的lock()方法

由于读锁是共享锁,且此时写锁未被占用,所以此时ThreadB也可以拿到读锁:
ThreadB调用完lock方法后,等待队列结构如下:
Java并发编程笔记——J.U.C之locks框架:基于AQS的读写锁(5)_第3张图片

此时:
写锁数量:0
读锁数量:2

4. ThreadC调用写锁的lock()方法

写锁其实是一种独占锁,实现了AQS的独占功能API,可以看到写锁的内部就是调用了AQS的acquire方法,该方法前面几章我们已经见过太多次了:

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

关键来看下ReentrantReadWriteLock是如何实现tryAcquire方法的,并没有什么特别,就是区分了两种情况:

  1. 当前线程已经持有写锁
  2. 写锁未被占用
protected final boolean tryAcquire(int acquires) {
    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))    //CAS更新同步状态值
        return false;
    setExclusiveOwnerThread(current);
    return true;
}

ThreadC调用完lock方法后,由于存在使用中的读锁,所以会调用acquireQueued并被加入等待队列,这个过程就是独占锁的请求过程AQS独占功能剖析,等待队列结构如下:
Java并发编程笔记——J.U.C之locks框架:基于AQS的读写锁(5)_第4张图片

此时:
写锁数量:0
读锁数量:2

5. ThreadD调用读锁的lock()方法

这个过程和ThreadA和ThreadB几乎一样,读锁是共享锁,可以重复获取,但是有一点区别:
由于等待队列中已经有其它线程(ThreadC)排在当前线程前,所以readerShouldblock方法会返回true,这是公平策略的含义。

protected final int tryAcquireShared(int unused) {
     
    Thread current = Thread.currentThread();
    int c = getState();
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)    //如果写锁被占用,且占用者不是当前线程,则获取失败
        return -1;
    int r = sharedCount(c);    //读锁重入次数
    if (!readerShouldBlock() &&    //判断当前线程需要等待(仅当等待队列中有其它线程处于队首位置时,需要等待)——这里返回了true,表示读线程需要等待
        r < MAX_COUNT &&    //判断读锁是否重入次数达到最大
        compareAndSetState(c, c + SHARED_UNIT)) {    //CAS操作,读锁重入次数+1
        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);
}

虽然获取失败了,但是后续调用fullTryAcquireShared方法,自旋修改State值,正常情况下最终修改成功,代表获取到读锁:

final int fullTryAcquireShared(Thread current) {
  
    HoldCounter rh = null;
    for (;;) {
        int c = getState();
        if (exclusiveCount(c) != 0) {
            if (getExclusiveOwnerThread() != current)
                return -1;
            // else we hold the exclusive lock; blocking here
            // would cause deadlock.
        } else if (readerShouldBlock()) {
            // Make sure we're not acquiring read lock reentrantly
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
            } else {
                if (rh == null) {
                    rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current)) {
                        rh = readHolds.get();
                        if (rh.count == 0)
                            readHolds.remove();
                    }
                }
                if (rh.count == 0)
                    return -1;
            }
        }
        if (sharedCount(c) == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            if (sharedCount(c) == 0) {
                firstReader = current;
                firstReaderHoldCount = 1;
            } else if (firstReader == current) {
                firstReaderHoldCount++;
            } else {
                if (rh == null)
                    rh = cachedHoldCounter;
                if (rh == null || rh.tid != getThreadId(current))
                    rh = readHolds.get();
                else if (rh.count == 0)
                    readHolds.set(rh);
                rh.count++;
                cachedHoldCounter = rh; // cache for release
            }
            return 1;
        }
    }
}

最终等待队列结构如下:
Java并发编程笔记——J.U.C之locks框架:基于AQS的读写锁(5)_第5张图片

此时:
写锁数量:0
读锁数量:3

6. ThreadA释放读锁

内部就是调用了AQS的releaseShared方法,该方法前面几章我们已经见过太多次了:

public void unlock() {
    sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

关键来看下ReentrantReadWriteLock是如何实现tryReleaseShared方法的,没什么特别的,就是将读锁数量减1:

//尝试第一次释放读锁,仅当读锁数量为0时,返回true
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 (;;) {    //同步状态值-1
        int c = getState();
        int nextc = c - SHARED_UNIT;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}
注意:
HoldCounter是个内部类,通过与 ThreadLocal结合使用保存每个线程的持有读锁数量,其实是一种优化手段。
static final class HoldCounter {
    int count = 0;
    // Use id, not reference, to avoid garbage retention
    final long tid = getThreadId(Thread.currentThread());
}

/**
 * ThreadLocal subclass. Easiest to explicitly define for sake
 * of deserialization mechanics.
 */
static final class ThreadLocalHoldCounter
    extends ThreadLocal {
    public HoldCounter initialValue() {
        return new HoldCounter();
    }
}
此时:
写锁数量:0
读锁数量:2

7. ThreadB释放读锁

和ThreadA的释放完全一样,此时:

写锁数量:0
读锁数量:1

8. ThreadD释放读锁

和ThreadA的释放几乎一样,不同的是此时读锁数量为0,tryReleaseShared方法返回true:

public void unlock() {
    sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
        doReleaseShared();//这里释放锁
        return true;
    }
    return false;
}
此时:
写锁数量:0
读锁数量:0

Java并发编程笔记——J.U.C之locks框架:基于AQS的读写锁(5)_第6张图片

因此,会继续调用doReleaseShared方法,doReleaseShared方法之前在讲AQS共享功能原理时已经阐述过了,就是一个自旋操作:

private void doReleaseShared() {
    for (;;) {
        Node h = head;
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            if (ws == Node.SIGNAL) {
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))    //将头结点的等待状态置为0,表示将要唤醒后继节点
                    continue;            // loop to recheck cases
                unparkSuccessor(h);    //唤醒后继节点
            }
            else if (ws == 0 &&
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                continue;                // loop on failed CAS
        }
        if (h == head)                   // loop if head changed
            break;
    }
}

该操作会将ThreadC唤醒:
Java并发编程笔记——J.U.C之locks框架:基于AQS的读写锁(5)_第7张图片

9. ThreadC从原阻塞处继续向下执行

ThreadC从原阻塞处被唤醒后,进入下一次自旋操作,然后调用tryAcquire方法获取写锁成功,并从队列中移除:

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

等待队列最终状态:
Java并发编程笔记——J.U.C之locks框架:基于AQS的读写锁(5)_第8张图片

此时:
写锁数量:1
读锁数量:0

10. ThreadC释放写锁

其实就是独占锁的释放,在AQS独占功能中,已经阐述过了,不再赘述。

补充一点:如果头结点后面还有等待的共享结点,会以 传播的方式依次唤醒,这个过程就是 共享结点的唤醒过程,并无区别。
public void unlock() {
    sync.release(1);
}
public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

四、总结

本章通过ReentrantReadWriteLock的公平策略,分析了RRW的源码,非公平策略分析方法也是一样的,非公平和公平的最大区别在于写锁的获取上:

static final class NonfairSync extends Sync {
    private static final long serialVersionUID = -8159625535654395037L;
    final boolean writerShouldBlock() {
        return false; // writers can always barge
    }

非公平策略中,写锁的获取永远不需要排队,这其实时性能优化的考虑,因为大多数情况写锁涉及的操作时间耗时要远大于读锁,频次远低于读锁,这样可以防止写线程一直处于饥饿状态

关于ReentrantReadWriteLock,最后有两点规律需要注意:

  1. 当RRW的等待队列队首结点是共享结点,说明当前写锁被占用,当写锁释放时,会以传播的方式唤醒头结点之后紧邻的各个共享结点。
  2. 当RRW的等待队列队首结点是独占结点,说明当前读锁被使用,当读锁释放归零后,会唤醒队首的独占结点。

ReentrantReadWriteLock的特殊之处其实就是用一个int值表示两种不同的状态(低16位表示写锁的重入次数,高16位表示读锁的使用次数),并通过两个内部类同时实现了AQS的两套API,核心部分与共享/独占锁并无什么区别。

本文参考

https://segmentfault.com/a/1190000015807600

转载于:https://my.oschina.net/u/3995125/blog/3071231

你可能感兴趣的:(Java并发编程笔记——J.U.C之locks框架:基于AQS的读写锁(5))