Java并发编程之读写锁ReentyrantReadWriteLock

309 阅读5分钟

这是我参与11月更文挑战的第25天,活动详情查看:2021最后一次更文挑战

ReentrantWriteLock概述

在实际开发中,往往会有写少读多的场景,显然ReentrantLock满足不了这个需求,所以ReentrantWriteLock应运而生。其采用读写分离的策略,允许多个线程可以同时获取到锁。

image.png

读写锁内部维护了一个ReadLLock和一个WriteLock,它们依赖Sync实现具体功能。而Sync继承自AQS,并且也提供了公平和非公平的实现。我们下面介绍一下非公平锁的读写锁实现。AQS中维护了一个state状态,而读写锁需要维护读写两个状态,于是可以利用state的高16表示读状态,低16位表示写状态。

static final int SHARED_SHIFT   = 16;
//共享锁状态单位值
static final int SHARED_UNIT    = (1 << SHARED_SHIFT);
/共享锁线程最大个数65536
static final int MAX_COUNT      = (1 << SHARED_SHIFT) - 1;
//排它锁(写锁)掩码,二进制15个1
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

//返回读锁线程数
static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }
//返回写锁线程数
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

读写锁中的firstReader用来记录第一个获取到读锁的线程,firstReaderHoldCount则记录第一个获取到读锁的线程获取的可重入的次数。cachedHoldCounter用来记录最后一个获取读锁的线程获取读锁的可重入次数。

static final class HoldCounter {
    int count = 0;
    // Use id, not reference, to avoid garbage retention
    final long tid = getThreadId(Thread.currentThread());
}

readHolder是ThreadLocal变量,用来存放除去第一个获取读锁线程外,其他线程获取读锁的可重入次数。ThreadLocalHoldCounter对象继承了ThreadLocal,因而initialValue方法返回一个HoldCounter对象。

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

ReentrantReadWriteLock支持以下功能:

  • 支持公平和非公平的获取锁的方式;

  • 支持可重入。读线程在获取了读锁后还可以获取读锁;写线程在获取了写锁之后既可以再次获取写锁又可以获取读锁;

  • 还允许从写入锁降级为读取锁,其实现方式是:先获取写入锁,然后获取读取锁,最后释放写入锁。但是,从读取锁升级到写入锁是不允许的;

  • 读取锁和写入锁都支持锁获取期间的中断;

  • Condition支持。仅写入锁提供了一个 Conditon 实现;读取锁不支持 Conditon ,readLock().newCondition() 会抛出 UnsupportedOperationException。

ReentrantWriteLock写锁的获取与释放(WriteLock)

  1. void lock()

写锁是个独占锁,只有一个线程可以同时持有该锁。如果当前线程没有线程获取到读锁和写锁,则当前线程可以获取到写锁然后返回。如果当前已经有线程获取到读锁和写锁,则请求写锁的线程会被阻塞挂起。如下代码所示,lock()内部调用了AQS的acquire方法,其中tryAcquire()方法是内部的sync类重写的。

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

如下代码,如果当前AQS状态值不为0(c!=0)则说明当前已经有线程获取到了读锁或者写锁。接下来继续判断,如果w==0说明state的低16位为0,则表示已经有线程获取到了读锁,直接返回false。如果w!=0,且当前线程不是写锁拥有者,返回false。如果前面的情况都不是,则说明当前线程获取了写锁,修改写锁次数,返回true。

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))
        return false;
    setExclusiveOwnerThread(current);
    return true;
}

如果当前AQS状态值为0(c=0),表示当前没有线程获取到读锁和写锁。 其中writerShouldBlock()分为公平和非公平实现,非公平直接返回false,然后执行CAS抢占获取锁。公平锁是采用hasQueuedPredecessors来判断当前节点是否有前驱节点,如果有则当前线程则放弃获取写锁的权限,返回false。

//公平
final boolean writerShouldBlock() {
    return hasQueuedPredecessors();
}

//非公平
final boolean writerShouldBlock() {
    return false();
}

  1. boolean tryLock()

尝试获取写锁,如果当前没有其他线程持有任何锁,则当前线程获取写锁会成功,然后返回true。如果当前已经有其他线程持有写锁或者读锁则该方法直接返回false,不会阻塞。如果当前线程已经持有了该写锁则简单增加AQS的state值后返回true。

public boolean tryLock( ) {
    return sync.tryWriteLock();
}
final boolean tryWriteLock() {
    Thread current = Thread.currentThread();
    int c = getState();
    if (c != 0) {
        int w = exclusiveCount(c);
        if (w == 0 || current != getExclusiveOwnerThread())
            return false;
        if (w == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
    }
    if (!compareAndSetState(c, c + 1))
        return false;
    setExclusiveOwnerThread(current);
    return true;
}
  1. void unlock()

尝试释放锁,如果当前线程持有该锁,调用该方法会让该线程对该线程持有的AQS状态值减1,如果减1后状态值为0则当前线程会释放掉该锁。

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;
}
protected final boolean tryRelease(int releases) {
//判断是否是写锁拥有者调用的unlock
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
        //获取可state值
    int nextc = getState() - releases;
    boolean free = exclusiveCount(nextc) == 0;
    //如果当前state=0则释放锁
    if (free)
        setExclusiveOwnerThread(null);
    setState(nextc);
    return free;
}

ReentrantWriteLock读锁的获取与释放(ReadLock)

  1. void lock()

获取读锁,如果当前没有其他线程持有写锁,则当前线程可以获取读锁,AQS的状态值state的高16位的值会增加1,然后返回。否则其他的线程持有写锁,则当前线程会被阻塞。

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

在上面代码中,读锁的lock方法调用了AQS的acquireShared方法,在其内部调用了ReentrantReadWriteLock中的sync重写的tryAcquire

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);
    //尝试获取锁,多个线程抢只有一个会成功,不成功会执行fullTryAcquireShared(自旋)
    // 读线程是否应该被阻塞、并且小于最大值、并且CAS设置成功
    //如果全都满足,则开始抢锁
    if (!readerShouldBlock() &&
        r < MAX_COUNT &&
        compareAndSetState(c, c + SHARED_UNIT)) {
        //如果是该线程第一个线程获取到读锁
        if (r == 0) {
        //设置第一个读线程,和占用次数
            firstReader = current;
            firstReaderHoldCount = 1;
            //如果和第一个获取到读锁的线程相同
        } else if (firstReader == current) {
        //第一个线程占用次数+1
            firstReaderHoldCount++;
        } else {//读锁数量不为0,并且不为当前线程
            //获取计数器
            //cachedHoldCounter记录最后一个获取读锁的线程
            HoldCounter rh = cachedHoldCounter;
            //计数器为空或者计数器的tid不为当前正在运行的线程的tid
            //则设置最后一个读线程为当前线程
            //readHolds是ThreadLocal变量,存放当前线程的读锁次数
            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);
}

上面的代码中,首先获取AQS的状态值,然后查看是否有其他线程获取了写锁,如果是则返回-1,调用AQS的doAcquireShared方法把当前线程放入AQS阻塞队列。如果当前线程持有了写锁也可以持有读锁(释放锁时,读写锁都需要释放)。

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

我们主要介绍一下tryReleaseShared方法。

protected final boolean tryReleaseShared(int unused) {
    //获取当前线程,如果当前线程为第一个读线程,
    //则如果读线程占用的资源数为,firstReader置为空 
    Thread current = Thread.currentThread();
    if (firstReader == current) {
        // assert firstReaderHoldCount > 0;
        if (firstReaderHoldCount == 1)
            firstReader = null;
        else
            firstReaderHoldCount--;
    } else {//如果不是当前线程,则通过HoldCounter获取本线程的读锁次数(本地变量)
        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;
    }
}

最后我们用一个图来总结读写锁:

image.png