【Java 并发】【九】【AQS】【八】ReentrantReadWriteLock之ReadLock读锁原理

发布时间 2023-04-08 12:22:50作者: 酷酷-

1  前言

上节我们看了下ReentrantReadWriteLock读写锁的写锁的申请和释放过程,这节我们就来看下读锁的。

2  线程读锁记录

回顾一下之前的例子,在读写并发操作的时候,读取数据的时候加读锁:

public class ReentrantReadWriteLockTest {
    // 声明一个读写锁
    private static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
    // 声明写锁
    private static ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
    // 声明读锁
    private static ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
    // 共享变量
    private static int value = 0;
    // 读取数据操作
    public static int getValue() {
        try {
            // 读取前加读锁
            readLock.lock();
            return value;
        } finally {
            // 释放读锁
            readLock.unlock();
        }
    }

    public static void addValue() {
        try {
            writeLock.lock();
            value++;
        } finally {
            writeLock.unlock();
        }
    }
}

上面的getValue方法就是使用读锁的样例。readLock.lock、readLock.unlock分别对应着读锁的加锁和释放,之前我们讲state 的高16位表示读锁个数,那现在问题来了,每个线程怎么知道自己读锁加了多少次?由于读锁是共享的,所以state变量上表示不出每个线程加读锁的个数。应该是每个线程都会记录自己加了多少个读锁;每个线程都有自己的一份记录,所以这里就用到了ThreadLocal。ReentrantReadWriteLock就是使用ThreadLocal来记录每个线程读锁的个数的。它具体的设计如下所示:

static final class HoldCounter {
    // 当前线程的读锁个数,count的初始值是0
    int count = 0;
    // 当前线程的id
    final long tid = getThreadId(Thread.currentThread());
}

// ThreadLocalHoldCounter 读锁存储容器,这里继承了ThreadLocal
static final class ThreadLocalHoldCounter
    extends ThreadLocal<HoldCounter> {
    public HoldCounter initialValue() {
        return new HoldCounter();
    }
}
// 这里本质是一个ThreadLocal,每个线程通过它可获取自己读锁的个数
private transient ThreadLocalHoldCounter readHolds;

我们接下来就分析下读锁的底层是怎么实现的。

3  读锁加锁lock源码分析

读锁加锁的入口方法源码如下:

public void lock() {
    //调用sync的acquireShared()方法,也还是进入了AQS的acquireShared方法了
    sync.acquireShared(1);
}

这里调用的是Sync同步器的acquireShared方法,最后还是进入了AQS的acquireShared方法:

public final void acquireShared(int arg) {
    // 1.调用子类Sync的tryAcquireShared方法
    // 2. 如果获取读锁失败,则调用doAcquireShared进入等待队列等待
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

获取读锁的入口acquireShared的模板流程:
(1)首先调用子类的tryAcquireShard方法去尝试获取读锁,也就是调用子类Sync的tryAcquireShared方法尝试获取读锁
(2)如果获取读锁成功,直接返回;否则获取失败,调用doAcquireShared方法进入等待队列等待
我们画个图理解一下:

doAcquireShared方法之前讲解AQS的时候已经分析过了,我们来看一下Sync的tryAcquireShared方法。

3.1  Sync的tryAcquireShred源码实现

protected final int tryAcquireShared(int unused) {
    // 获取当前线程
    Thread current = Thread.currentThread();
    // 获取state变量的值
    int c = getState();
    // 计算写锁的次数,如果写锁次数非0,且加写锁的不是自己
    // 说明别人加了写锁,自己这时候获取读锁失败,返回-1
    if (exclusiveCount(c) != 0 &&
        getExclusiveOwnerThread() != current)
        return -1;
    // 计算一下读锁的个数
    int r = sharedCount(c);
    // 调用readerShouldBolck,判断是否是公平锁,是否允许加锁
    if (!readerShouldBlock() &&
        // 读锁个数r < 65535,说明读锁个数还剩余
        r < MAX_COUNT &&
        // 执行cas尝试加读锁
        compareAndSetState(c, c + SHARED_UNIT)) {
        // 如果r == 0,说明自己是第一个加读锁的线程
        if (r == 0) {
            // 记录一些第一个加读锁线程
            firstReader = current;
            // 第一个加锁线程加锁次数为1
            firstReaderHoldCount = 1;
        }
        // 如果自己是第一个加读锁的线程,说明之前加锁过了
        // 直接修改一下次数即可
        else if (firstReader == current) {
            firstReaderHoldCount++;
        } else {
            HoldCounter rh = cachedHoldCounter;
            if (rh == null || rh.tid != getThreadId(current))
                // 从ThreadLocal中获取下当前线程加锁的次数
                cachedHoldCounter = rh = readHolds.get();
            // 如果当前线程第一次加锁,设置一下ThreadLocal
            else if (rh.count == 0)
                readHolds.set(rh);
            // 当前线程加锁次数加1即可
            rh.count++;
        }
        return 1;
    }
    // 如果上面CAS操作加锁失败了,进入这个兜底方法
    return fullTryAcquireShared(current);
}

我们画个图来理解一下:

上面的流程图,我们再一步步分析一下:
(1)首先线程进来,先获取锁的记录变量state
(2)计算一下写锁个数,如果写锁个数非零,并且加写锁的线程不是自己,那么由于读写互斥,此时加读锁失败,返回-1
(3)如果写锁个数为零,或者是自己加了写锁,则继续
(4)readShouldBlock根据当前的公平锁、非公平锁、等待队列情况返回是否允许加锁,如果不允许则暂时获取锁失败,进入兜底加锁机制,即执行fullTryAcquireShared方法。
(5)判断读锁的加锁次数,r < MAX_COUNT即65535是否达到上限,如果达到上限则进入兜底加锁机制fullTryAcquireShared。
         未达到上限则执行CAS操作尝试去加读锁,如果CAS加锁失败,也会进入兜底加锁机制fullTryAcquireShared方法
(6)如果CAS加锁成功了,则从ThreadLocal获取当前加读锁的次数,将加读锁的次数+1,就可以了

3.2  fullTryAcquireShared加读锁源码

那我们来看下兜底机制,里面到底是个什么逻辑。

final int fullTryAcquireShared(Thread current) {
    
    HoldCounter rh = null;
    // 在for循环中,不断重试,知道有结果
    for (;;) {
        // 获取读写锁的变量state
        int c = getState();
        // 如果有写锁,并且加锁不是自己
        // 说明别人加了写锁,读写互斥,直接返回失败
        if (exclusiveCount(c) != 0) {
            if (getExclusiveOwnerThread() != current)
                return -1;
        // 根据公平、非公平模式、等待队列等判断是否应该被阻塞
        } else if (readerShouldBlock()) {
            // 如果被阻塞
            // 第一个加读锁的线程是自己,啥也不干
            if (firstReader == current) {
                // assert firstReaderHoldCount > 0;
            } else {
                if (rh == null) {
                    // 自己不是第一个加读锁的线程
                    // 则获取一下自己加读锁的次数
                    rh = cachedHoldCounter;
                    if (rh == null || rh.tid != getThreadId(current)) {
                        rh = readHolds.get();
                        // 如果加读锁的次数是0,从ThreadLocal从移除
                        if (rh.count == 0)
                            readHolds.remove();
                    }
                }
                // 加读锁次数是0此,此时有应该阻塞,直接返回加锁失败
                if (rh.count == 0)
                    return -1;
            }
        }
        // 如果读锁次数已达上限,抛出异常
        if (sharedCount(c) == MAX_COUNT)
            throw new Error("Maximum lock count exceeded");
        // cas操作尝试加锁,如果cas加锁成功,进入下面的逻辑
        if (compareAndSetState(c, c + SHARED_UNIT)) {
            // 如果自己是第一个加锁的线程,设置一下第一个加锁的人是当前线程
            if (sharedCount(c) == 0) {
                firstReader = current;
                firstReaderHoldCount = 1;
            } else if (firstReader == current) {
                // 第一个加锁的线程是自己,将自己加锁次数+1即可
                firstReaderHoldCount++;
            } else {
                // 这里的操作不外乎就是从ThreadLocal从取出自己加锁的次数,然后将次数+1即可
                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
            }
            // 加锁成功,返回1
            return 1;
        }
    }
}

其实大体上跟tryAcquireShred差不多的,只是在一个for循环里面不断重试而已,提高加锁成功的概率,下面我们也是再画个图来看一下:

上面的流程图,我们再捋一下:
(1)首先获取读写锁状态state,判断如果有别人加了写锁,由于读写互斥,则直接加读锁失败,返回-1
(2)如果别人没有加写锁,判断一下自己是否应该被阻塞(结合公平锁、非公平所、等待队列)。
  如果应该被阻塞,且自己加读锁的次数count == 0,则返回-1,加锁失败。
  如果自己是第一个加读锁的线程,可以网开一面,继续尝试获取读锁,进入for循环重试
(3)如果不应该被阻塞,判断加锁次数是否达到上限,如果达到上限,直接抛出异常
(4)如果读锁次数还有剩余,直接CAS操作尝试加锁,加锁失败则进入for循环重试。
如果加锁成功,则从ThreadLocal中取出之前加锁次数,然后将加锁次数+1,最后返回1,表示本次操作加锁成功。

4  读锁释放锁unlock源码分析

我们接下来继续,讲解读锁ReadLock的释放锁流程:

public void unlock() {
    // 调用的还是Sync同步器的releaseShared方法,也就是AQS的releaseShared方法
    sync.releaseShared(1);
}

这里就是对Sync的releaseShared方法的一个封装底层还是会进入的AQS的releaseShared方法,继续来看AQS的releaseShared模板方法:

public final boolean releaseShared(int arg) {
    // 1. 直接调用到子类的tryReleaseShared方法释放共享锁
    if (tryReleaseShared(arg)) {
        // 2. 如果共享锁释放成功,将共享资源传播,唤醒等待队列的后续节点线程
        doReleaseShared();
        return true;
    }
    return false;
}

这里又回到了之前讲解过的AQS的释放共享资源releaseShared的模板方法里面了:
(1)调用子类Sync的tryReleaseShared方法,去实际释放共享锁
(2)如果释放成功,则调用AQS的doReleaseShared方法唤醒等待队列中的线程,进行共享锁资源的传播,这里之前讲解AQS的时候讲解过了
我们画个图理解下:

核心的释放逻辑还是在类Sync的tryReleaseShared方法里面,我们继续分析。

4.1  Sync的tryReleaseShared源码实现

protected final boolean tryReleaseShared(int unused) {
    // 获取当前线程
    Thread current = Thread.currentThread();
    // 将当前线程加锁次数-1
    if (firstReader == current) {
        // 如果之前只是加了一次锁,那么就释放锁了
        if (firstReaderHoldCount == 1)
            firstReader = null;
        else
        // 如果加了多次锁,锁的次数减少1
            firstReaderHoldCount--;
    } else {
        // 这里的逻辑,就是将ThreadLocal中自己存储的加锁次数减少1而已
        // 没啥特殊的地方
        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;
    }
    // 然后这里就是执行CAS减少加锁的次数,直到成功为止
    for (;;) {
        int c = getState();
        int nextc = c - SHARED_UNIT;
        // cas修改读写锁变量state,将读锁次数-1
        // 注意由于使用高16位表示读锁,所以单位值SHARED_UNIT
        if (compareAndSetState(c, nextc))
            // 判断读锁的个数是否为0,如果为0说明读锁完全释放了
            return nextc == 0;
    }
}

我们画个图来理解一下:

这里就是读锁ReadLock释放锁的流程了,我们再来总结一下:
(1)释放锁首先判断一下,当前线程是否是第一个加锁线程,也就是current == firstReaderHoldCount (因为第一个加读锁的线程,加锁次数存储在firstReaderHoldCount中!!!,后续的加读锁的线程加锁次数存储在ThreadLocal中!!!)
(2)如果自己是第一个加锁线程,扣减firstReaderHolderCount次数,如果扣为零了,则将firstReaderHolderCount 置为null
(3)如果不是第一个加锁线程,从ThreadLocal中取出加锁次数,然后次数扣减1;如果加锁次数为零,从ThreadLocal中移除,因为不需要记录这个线程的加锁次数了,直接释放ThreadLocal的空间。否则还继续保存在ThreadLocal中
(4)执行CAS操作修改state读写锁的状态变量,注意这里由于是高16位表示读锁,所以读锁每减少1,则state 减少 65536
(5)不断重复CAS操作,直到成功为止,判断如果当前读锁个数为0,则读锁完全释放了返回1,否则返回-1

5  小结

到这里ReentrantReadWriteLock中读锁ReadLock加锁、释放锁的底层原理和源码我们就看的差不多了,有理解不对的地方欢迎指正哈。