深入理解ReentrantReadWriteLock
一、为什么要出现读写锁
synchronized和ReentrantLock都是互斥锁
如果说有一个操作是读多写少的,还要保证线程安全的话,如果采用上述的两种互斥锁,效率方面肯定是很低的
在这种情况下,咱们就可以用ReentrantReadWirteLock读写锁去实现
读读之间是不互斥的,可以读和读操作并发执行操作
但是如果涉及到写操作,那么还得是互斥得操作
public class IReentrantReadWriteLock {
private final static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private final static ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
private final static ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
public static void main (String[] args) throws InterruptedException {
System.out.println("=====USE ReentrantReadWriteLock-> readLock=====");
new Thread(() -> {
readLock.lock();
System.out.println(Thread.currentThread().getName() + " 获取到读锁资源,并睡眠:" + (60 * 60 * 1000) + " ");
try {
Thread.sleep(60 * 60 * 1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "lock-1").start();
Thread.sleep(1000);
readLock.lock();
System.out.println(Thread.currentThread().getName() + " 获取到读锁资源");
readLock.unlock();
System.out.println("=====USE ReentrantReadWriteLock-> readLock=====");
System.out.println("=====USE ReentrantReadWriteLock-> writeLock=====");
new Thread(() -> {
writeLock.lock();
System.out.println(Thread.currentThread().getName() + " 获取到写锁资源,并睡眠:" + (60 * 60 * 1000) + " ");
try {
Thread.sleep(60 * 60 * 1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}, "lock-2").start();
Thread.sleep(1000);
// 主线程是拿不到写锁,写锁互斥
writeLock.lock();
System.out.println(Thread.currentThread().getName() + " 获取到读锁资源");
writeLock.unlock();
System.out.println("=====USE ReentrantReadWriteLock-> writeLock=====");
}
}
二、读写锁得实现原理
ReentrantReadWriteLock还是基于AQS实现的,还是对state进行操作,拿到锁资源就去干活,如果没有拿到,依然去AQS队列中排队
读锁操作:基于state的高16位进行操作
写锁操作:基于state的低16位进行操作
ReentrantReadWirteLock依然是可重入锁
写锁重入: 读写锁中的写锁的重入方式,基本和ReentrantLock一致,没有什么区别,依然是对state进行+操作即可,只要确认持有锁资源的线程,是当前写锁线程即可,只不过之前ReentrantLock的重入次数是state的正数取值范围,但是读写锁中的写锁范围就变小了,基于state的低16位进行操作
读锁重入: 因为读锁是共享锁,读锁在获取资源操作时,只要对state的高16进行+1操作,因为读锁是共享锁,所以同一时间会有多个线程持有读锁资源,这样一来,多个读操作在持有读锁时,无法确认每个线程读锁重入的次数,为了去记录读锁重入的次数,每个读锁的线程,都会有个ThreadLock记录锁重入的次数
写锁的饥饿问题: 读锁是共享的,当有线程持有读锁资源时,再来一个线程想要获取读锁,直接对state修改即可,在读锁资源先被占用后,来了一个写锁资源,此时,大量的需要获取读锁的线程来请求锁资源,如果可以绕过写锁,直接拿锁资源,会造成写锁长时间无法获取到写锁资源
读锁在拿到锁资源后,如果再有读线程需要获取读锁资源,需要去AQS队列排队,如果队列的前面有写锁资源的线程,那么后续读锁线程是无法拿到锁资源的,持有读锁的线程,只会让写锁线程之前的读线程拿到锁资源
三、写锁分析
3.1 写锁的加锁流程
写锁的加锁流程:
- 写线程来竞争写锁资源
- 写线程会直接通过tryAcquire获取写锁资源(公平锁&非公平锁)
- 获取state值,并且拿到低16位的值
- 如果state值不为0,判断是否是锁重入操作,判断当前持有锁线程是否是当前线程
- 如果state值为0
- 判断是公平锁:查看队列是否有排队,有就去排队,没有抢一下
- 判断是非公平锁:直接抢锁资源
- 如果拿到锁资源,直接告辞,如果没有拿到去排队,而排队的逻辑和ReentrantLock一样
3.1 写锁的加锁源码分析
- lock()
// 写锁加锁入口
public void lock() {
sync.acquire(1);
}
- acquire(int arg)
public final void acquire(int arg) {
// tryAcquire(arg) 尝试获取锁
if (!tryAcquire(arg) &&
// acquireQueued 和 ReetrantLock一样
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
- boolean tryAcquire(int acquires)
protected final boolean tryAcquire(int acquires) {
// 获取到当前线程
Thread current = Thread.currentThread();
// 拿到state的值
int c = getState();
// 得到state低16位的值,为了判断是否有读锁,读写互斥
int w = exclusiveCount(c);
// c不等于说明有线程持有锁
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
// w == 0 说明读锁占用了,读写互斥,写锁去排队
// w!=0 判断当前线程是否是持有锁线程,如果不是,就去排队
if (w == 0 || current != getExclusiveOwnerThread())
return false;
// 判断锁重入次数,最大重入次数65535
// static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
// 锁重入
setState(c + acquires);
// 锁重入成功
return true;
}
// 尝试获取锁
if (writerShouldBlock() ||
// CAS拿锁,拿锁失败返回false去排队
!compareAndSetState(c, c + acquires))
return false;
// 拿锁成功,设置占有互斥锁的线程
setExclusiveOwnerThread(current);
return true;
}
- exclusiveCount(int c)
// 这个方法就是将state的低16位拿到
// static final int SHARED_SHIFT = 16;
// static final int SHARED_UNIT = (1 << SHARED_SHIFT);
// static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
// 00000000 00000000 00000000 00000001 == 1
// 00000000 00000001 00000000 00000000 == 1 << 16 (1往左移16)
// 00000000 00000000 11111111 11111111 == EXCLUSIVE_MASK (1 << 16) - 1 (1往左移16) -1
// &运算,一个为0,必然是0,都为1才会使1
// 通过计算如果state值是高16位,state & EXCLUSIVE_MASK,肯定是0,说明读锁占用了
// 读锁每次对高16位进行加 c + SHARED_UNIT
- writerShouldBlock() 公平锁
// 公平锁和ReentrantLock一样
final boolean writerShouldBlock() {
// 查看是否有线程在AQS中排队
return hasQueuedPredecessors();
}
- hasQueuedPredecessors()
public final boolean hasQueuedPredecessors() {
// 尾节点
Node t = tail;
// 头节点
Node h = head;
Node s;
// 条件1 h != t
// 当h == t,则 h!=t 返回false
// 条件2 ((s = h.next) == null || s.thread != Thread.currentThread())
// s 为头节点下一个节点
// s 节点不为null,并且是节点的线程为当前线程(排在第一名的是不是我)
// 当 头节点下一个节点的线程 == 当前线程,则 s.thread != Thread.currentThread() 返回false
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
- writerShouldBlock() 非公平锁
// 非公平锁
final boolean writerShouldBlock() {
return false; // writers can always barge
}
3.3 写锁释放流程&源码分析
释放的流程和ReentrantLock一致,只是判断释放释放干净时,判断低16位的值
- unlock()
public void unlock() {
// 释放锁资源不分为公平锁和非公平锁,都是一个sync对象
sync.release(1);
}
- release(int arg)
// 释放锁的核心流程
public final boolean release(int arg) {
// 核心释放锁资源,对state-1操作
if (tryRelease(arg)) {
// 如果锁释放干净了,走这个逻辑
// 获取到头节点
Node h = head;
// 头节点不为null
// 如果头节点状态不为0(为-1),说明后面有点排队的Node,并且线程已经挂起了
if (h != null && h.waitStatus != 0)
// 唤醒排队的线程
unparkSuccessor(h);
return true;
}
return false;
}
- tryRelease(int releases)
protected final boolean tryRelease(int releases) {
// 判断当前线程释放是否是持有锁资源线程
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 获取state值-1
int nextc = getState() - releases;
// 判断低16位结果是否为0,是0代表锁是否干净
boolean free = exclusiveCount(nextc) == 0;
if (free)
// 将持有锁线程设置为null
setExclusiveOwnerThread(null);
// 设置给state
setState(nextc);
// 释放干净,返回true,没释放干净返回false
return free;
}
四、读锁分析
4.1 读锁加锁流程概述
4.1.1 基本读锁流程
读锁加锁流程
- 读操作线程,竞争读锁资源
- 会竞争共享锁的资源
- 拿到state
- 判断state中的低16位是否为0
- 如果不为0,代表写锁占用着资源如果有资源占用着写锁,但是不是当前线程,结束(写锁 - 读锁的降级)
- 拿到state高16位的值
- 公平锁:如果有人排队,直接去排队
- 非公平锁:查看AQS的队列中,是否写线程在排队,如果有,就去排队
- CAS对state的高16位+1,成功,拿到读锁资源
- 读锁记录锁重入的次数,需要让每个读线程用ThreadLock存储重入次数,ReentrantReadWriteLock对读锁重入做了一些优化
记录重入次数的核心
ReentrantReadWriteLock在内部对ThreadLock做了封装,基于HoldCount对象存储重入次数,在内部有个count属性记录,而且每个线程都是自己的ThreadLocalHoldCounter,所以可以直接对内部的count进行++操作
- 第一个获取读锁资源的重入次数记录方式
第一个拿到读锁资源的线程,不需要通过ThreadLock存储,内部提供了两个属性来记录第一个拿到读锁资源线程信息
内部提供了firstReader记录第一个拿到读锁资源的线程,fistReaderHoldCount记录fistReader的锁重入次数 - 最后一个获取读锁资源的重入次数方式
最后一个拿到读锁资源的线程,也会缓存他的重入次数,这样++起来方便,基于cacheHoldCounter缓存最后一个拿到锁资源线程的重入次数
锁重入流程
- 判断当前线程是否是第一个拿到读锁资源的:如果是,直接将firstReader以及firstReaderHoldCount设置为当前线程的信息
- 判断当前线程是否是firstReader:如果是,直接对firstReaderHoldCount++即可
- 和firstReader没有关系,先获取cachedHoldCounter判断是否是当前线程
- 如果不是,获取当前线程的重入次数,将cachedHoldCounter设置为当前线程
- 如果是,判断当前重入次数是否为0,重新设置当前线程的锁重入信息到readHolds(ThreadLocal)中,算是初始化操作,重 入次数是0
- 前面两者最后做了count++
读线程在AQS队列获取锁资源的后续操作
- 正常如果都是读线程来获取读锁资源,不需要使用到AQS队列的,直接CAS操作即可
- 如果写线程持有写锁,这时候读线程需要进入到AQS队列排队,可能会有多个读线程在AQS中
当写锁资源是否后,会唤醒head后面的读线程,当head后面的读线程拿到锁资源后,还需要查看next节点是否也是读线程阻塞,如果是,直接唤醒
4.1.2 源码分析
- lock()
public void lock() {
// 读锁加锁方法入口
sync.acquireShared(1);
}
- acquireShared(int arg)
public final void acquireShared(int arg) {
// 竞争锁资源,返回-1说明没有拿到锁资源
if (tryAcquireShared(arg) < 0)
// 没有拿到锁资源,去排队
doAcquireShared(arg);
}
- tryAcquireShared(int unused)
// 读锁竞争锁资源核心流程
protected final int tryAcquireShared(int unused) {
// 拿到当前线程
Thread current = Thread.currentThread();
// 拿到state
int c = getState();
// 拿到state的低16位,判断!=0,说明有写锁占用锁资源
// 并且如果持有锁线程不是当前线程,无法获取锁资源,返回-1,去排队
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// 没有线程持有写锁
// 获取读锁的信心,state的高16位
int r = sharedCount(c);
// 公平锁:就查看队列是否有排队的,有排队的,就直接结束
// 非公平锁:没有排队的,直接抢,有排队的,但是读锁其实不用排队的,如果出现这种情况,大部分是写锁资源刚刚释放
if (!readerShouldBlock() &&
// 判断持有读锁的临界值释放达到
r < MAX_COUNT &&
// CAS修改state,对高16位进行+1
compareAndSetState(c, c + SHARED_UNIT)) {
// r==0,当前是第一个线程拿到读锁资源的线程
if (r == 0) {
// 将firstReader设置位当前线程
firstReader = current;
// 将count设置为1
firstReaderHoldCount = 1;
} else if (firstReader == current) {
// firstReader == current 判断当前线程是否是第一个获取读锁资源的线程
// 是直接++
firstReaderHoldCount++;
} else {
// 到这说明不是第一个线程获取读锁的线程
// 获取到最后一个拿到读锁资源的线程
HoldCounter rh = cachedHoldCounter;
// 判断当前线程是否是最后一个拿到读锁资源的线程
if (rh == null || rh.tid != getThreadId(current))
// 如果不是,设置cachedHoldCounter为当前线程
cachedHoldCounter = rh = readHolds.get();
// 当前线程是之前的最后一个线程cachedHoldCounter
else if (rh.count == 0)
// 将当前的重入信息设置到ThreadLocal中
readHolds.set(rh);
// 重入次数++
rh.count++;
}
// 获取锁成功返回1
return 1;
}
// 如果上面没有拿到锁资源,尝试在获取一次
return fullTryAcquireShared(current);
}
- sharedCount(int c)
// state无符号右移动16位
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
- readerShouldBlock()
// 公平锁,查看队列是否有人排队
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
- hasQueuedPredecessors()
public final boolean hasQueuedPredecessors() {
// 尾节点
Node t = tail;
// 头节点
Node h = head;
Node s;
// 条件1 h != t
// 当h == t,则 h!=t 返回false
// 条件2 ((s = h.next) == null || s.thread != Thread.currentThread())
// s 为头节点下一个节点
// s 节点不为null,并且是节点的线程为当前线程(排在第一名的是不是我)
// 当 头节点下一个节点的线程 == 当前线程,则 s.thread != Thread.currentThread() 返回false
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
- readerShouldBlock()
// 非公平锁
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
- apparentlyFirstQueuedIsExclusive()
// 非公平锁的判断
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
// && 只要🈶一个条件为 false就可以抢锁 !readerShouldBlock(
// head 是 null 可以直接抢占锁资源
return (h = head) != null &&
// head的后继节点为null,可以直接抢占锁资源
(s = h.next) != null &&
// head后面的是Node,是共享锁,可以直接抢占锁资源
!s.isShared() &&
// head后面排队的thread为null,可以直接抢占锁资源
s.thread != null;
}
- isShared()
// 判断是否是共享锁
final boolean isShared() {
return nextWaiter == SHARED;
}
- fullTryAcquireShared(Thread current)
// 如果上面没有拿到锁资源,尝试在获取一次
final int fullTryAcquireShared(Thread current) {
// 声明当前线程重入次数
HoldCounter rh = null;
for (;;) {
// 拿到state的值
int c = getState();
// 如果有写锁占用锁资源,并且不是当前线程,返回-1,走排队策略
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
}
// 查看当前释放可以尝试竞争锁资(公平锁和非公平锁的逻辑)
else if (readerShouldBlock()) {
// 无论公平还是非公平,只要进来,就代表着放到AQS队列中了,先做一波准备
// 在处理ThreadLocal的内存泄漏问题
if (firstReader == current) {
// 如果当前线程是之前的firstReader,什么都不需要做
// assert firstReaderHoldCount > 0;
} else {
// 第一次进来的是NULL
if (rh == null) {
// 拿到最后一个获取读锁的线程
rh = cachedHoldCounter;
// 当前线程并不是cachedHoldCounter,没有拿到
if (rh == null || rh.tid != getThreadId(current)) {
// 从自己的ThreadLocal中拿到重入次数计数器
rh = readHolds.get();
// 如果计数器为0,说明之前没有拿到过读锁资源
if (rh.count == 0)
// remove,避免内存泄漏
readHolds.remove();
}
}
// 前面处理完事之后,直接返回-1
if (rh.count == 0)
return -1;
}
}
// 判断重入次数,是否超出预知
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// CAS尝试获取锁
if (compareAndSetState(c, c + SHARED_UNIT)) {
// state高16位==0,说明第一次抢到读锁
if (sharedCount(c) == 0) {
// 设置第一个拿到读锁的线程
firstReader = current;
// 第一个读锁线程重入次数设置为1
firstReaderHoldCount = 1;
} else if (firstReader == current) {
// 当前线程是第一次拿到读锁的线程,重入次数++
firstReaderHoldCount++;
} else {
// rh==null,获取到最后一个拿到读锁线程
if (rh == null)
rh = cachedHoldCounter;
// 前线程并不是cachedHoldCounter
if (rh == null || rh.tid != getThreadId(current))
// 从ThreadLocal中获取
rh = readHolds.get();
// 设置当前线程为最后一个拿到读锁的线程并经++
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
- doAcquireShared(int arg) 进AQS队列排队
// 读锁需要排队的操作
private void doAcquireShared(int arg) {
// 声明Node,类型是共享锁,并且扔到AQS中排队
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 拿到上一个节点
final Node p = node.predecessor();
// 如果前继节点是头节点,直接执行tryAcquireShared
if (p == head) {
// -1 抢锁是不,1拿锁成功设置头信息
int r = tryAcquireShared(arg);
if (r >= 0) {
// 拿到读锁信息后,需要做后续处理
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// 找到prev有效节点,将状态设置-1,挂起线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
- setHeadAndPropagate(Node node, int propagate)
private void setHeadAndPropagate(Node node, int propagate) {
// 拿到head节点
Node h = head; // Record old head for check below
// 将当前节点设置为head节点
setHead(node);
// propagate=1,第一个判断更多的是在信号量有处理JDK1.5bug
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
// 拿到当前节点
Node s = node.next;
// 如果next节点是共享锁,直接唤醒next节点
if (s == null || s.isShared())
doReleaseShared();
}
}
- doReleaseShared()
// 唤醒AQS中排队的读线程
private void doReleaseShared() {
for (;;) {
// 拿到头节点
Node h = head;
// 头节点不为null,说明有排队的
if (h != null && h != tail) {
// 拿到head的状态
int ws = h.waitStatus;
// 判断是否为-1
if (ws == Node.SIGNAL) {
// 到这,说明后面有挂起的线程,先基于CAS将head的状态-1,修改为0
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 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;
}
}
4.2 读锁的释放流程
4.2.1 释放锁流程
1、处理重入以及state的值
2、唤醒后续排队的Node
4.2.2 源码分析
- unlock()
public void unlock() {
// 读锁释放入口
sync.releaseShared(1);
}
- releaseShared(int arg)
public final boolean releaseShared(int arg) {
// tryReleaseShared 释放锁的核心流程
if (tryReleaseShared(arg)) {
// 唤醒在AQS排队中的读线程
doReleaseShared();
return true;
}
return false;
}
- tryReleaseShared(int unused)
protected final boolean tryReleaseShared(int unused) {
// 拿到当前线程
Thread current = Thread.currentThread();
// 如果是第一个拿到的读锁的线程,直接进行--,不需要ThreadLocal
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
// 重入次数等于1,将firstReader设置为空
if (firstReaderHoldCount == 1)
firstReader = null;
else
// --操作
firstReaderHoldCount--;
} else {
// 不是第一个拿到读锁的线程,从cachedHoldCounter一集ThreadLocal处理
HoldCounter rh = cachedHoldCounter;
// 如果不是最后一个拿到读锁的线程,就从ThreadLocal中拿
if (rh == null || rh.tid != getThreadId(current))
// 从ThreadLocal中拿
rh = readHolds.get();
// 获取到count值
int count = rh.count;
// 如果重入次数小于等于1
if (count <= 1) {
// 从ThreadLocal中异常
readHolds.remove();
// 如果已经是0,没必要在unlock,抛出一昂
if (count <= 0)
throw unmatchedUnlockException();
}
// 重入次数--
--rh.count;
}
for (;;) {
// 拿到state,高16位,-1,成功返回state是否为0
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// 等于0说明锁释放干净了
return nextc == 0;
}
}
- doReleaseShared()
// 唤醒AQS中排队的读线程
private void doReleaseShared() {
for (;;) {
// 拿到头节点
Node h = head;
// 头节点不为null,说明有排队的
if (h != null && h != tail) {
// 拿到head的状态
int ws = h.waitStatus;
// 判断是否为-1
if (ws == Node.SIGNAL) {
// 到这,说明后面有挂起的线程,先基于CAS将head的状态-1,修改为0
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 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;
}
}
end~