ReentrantReadWriteLock(读写锁)
什么是读写锁
读写锁是一种特殊的锁,他把对资源的访问分为读访问和写访问,多个线程可以同时对共享资源进行读访问,但是同一时间只能有一个线程对共享资源进行写访问,使用读写锁可以极大地提高并发量。
读写锁特性
是否互斥 | 读 | 写 |
---|---|---|
读 | 否 | 是 |
写 | 是 | 是 |
类结构
从类图我们可以看到
1、ReentrantReadWriteLock
本身实现了ReadWriteLock
接口,这个接口只提供了两个方法readLock()
和writeLock()
。
2、主要由AQS(同步器)实现相关功能,包含一个继承了AQS的Sync内部类,以及其两个子类FairSync
和NonfairSync
。
3、ReadLock
和WriteLock
两个内部类实现了Lock
接口,它们具有锁的一些特性。
源码分析
主要属性
以下就是读写锁中的几个核心属性,很简单,但是它内部却是十分复杂。
// 读锁
private final ReentrantReadWriteLock.ReadLock readerLock;
// 写锁
private final ReentrantReadWriteLock.WriteLock writerLock;
// 同步器
final Sync sync;
主要构造方法
它提供了两个构造方法,默认构造方法使用的是非公平锁模式,在构造方法中初始化了读锁和写锁。
public ReentrantReadWriteLock() {
this(false);
}
// 参数为ture创建公平锁,false创建非公平锁
// 并初始化读锁和写锁
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
获取读锁和写锁的方法
上面我们说过ReentrantReadWriteLock
实现了ReadWriteLock
接口,这里通过实现readLock()
和writeLock()
两个方法将读锁和写锁暴露给外部。
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
读锁和写锁
读锁和写锁的代码其实都很简单,主要是实现了Lock的接口,这里就不再详细讲解,因为读锁和写锁的实现的核心代码主要都在Sync和它的两个子类FairSync
和NonfairSync
中,所以我们要重点分析这三个类。
// 读锁
public static class ReadLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = -5992448646407690164L;
// 注意此处持有Sync的引用,核心代码都在这个类里面
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;
}
}
Sync核心类及其子类
Sync核心属性
abstract static class Sync extends AbstractQueuedSynchronizer {
// 高16位为读锁,低16位为写锁
static final int SHARED_SHIFT = 16;
// 读锁单位,2^16
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
// 读锁最大持有数量或读锁最大可重入数
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
// 和state执行&操作,使高16抹0
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
// 本地线程计数器
private transient ThreadLocalHoldCounter readHolds;
// 缓存计数器
private transient HoldCounter cachedHoldCounter;
// 第一个读线程
private transient Thread firstReader = null;
// 第一个读线程的计数
private transient int firstReaderHoldCount;
// 读锁的数量,持有读锁的线程数
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
// 写锁的数量,即写锁的重入次数
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
}
控制的读锁和写锁状态的核心
我们知道在AQS中都是通过state变量来表示锁的状态,但是state是一个整形,只能表示两种状态,但是在读写锁中有读写锁是分离的,需要四种表示状态,为了解决这个问题就将state变量分为了高16位和低16位,高16位表示读锁状态,低16位表示写锁状态。我们知道int是32位的,将其无符号右移16位就得到了高16位(读锁)的值;将其与2^16 - 1做与运算,高16位全部和0相与全为零,低16位全部和1相与全部不变,这样就相当于将高16位的值全部抹零了,得到的就是低16位(写锁)的值。
Sync内部类
Sync中有两个内部类,分别为HoldCounter和ThreadLocalHoldCounter
,其中HoldCounter
主要与读锁配套使用。
// 记录线程持有的锁的重入次数
static final class HoldCounter {
// 计数
int count = 0;
// 线程id
final long tid = getThreadId(Thread.currentThread());
}
// 记录了每个线程对应的HoldCounter
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
// 重写了ThreadLocal的initialValue方法
public HoldCounter initialValue() {
return new HoldCounter();
}
}
读锁和写锁的实现
我们知道读写锁是基于AQS实现的,关于AQS我们在上一篇博客中有详细的介绍,这里就不做重点分析,感兴趣的朋友可以参考这篇博客。我们这里主要分析Sync如何重写AQS的四个钩子方法tryAcquire
、tryRelease
、tryAcquireShared
、tryReleaseShared
。
我们从读写锁的读写、写写互斥的特性我们可以看出读锁其实是共享锁,因此主要是通过重写tryAcquireShared
、tryReleaseShared
两个方法实现,写锁其实是互斥锁,主要通过tryAcquire
、tryRelease
这两个方法实现。下面我们重点分析这几个方法。
读锁的获取
// ReentrantReadWriteLock.ReadLock.lock()
public void lock() {
sync.acquireShared(1);
}
// AbstractQueuedSynchronizer.acquireShared()
public final void acquireShared(int arg) {
// 尝试获取共享锁(返回1表示成功,返回-1表示失败)
if (tryAcquireShared(arg) < 0)
// 失败了就可能要排队
doAcquireShared(arg);
}
// ReentrantReadWriteLock.Sync.tryAcquireShared()
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
// 当前状态
int c = getState();
// 如果写锁的状态不为0并且持有锁的线程和不等于当前线程,说明此时其它的线程获得了写锁,返回-1
// exclusiveCount(c)互斥锁(写锁)的次数
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// 读锁被获取的次数
int r = sharedCount(c);
// readerShouldBlock:读锁是否阻塞,是返回true,不是false
// 读锁不阻塞,持有读锁的数量小于最大值,并且CAS修改锁的状态成功,此时表示获取读锁成功。
// SHARED_UNIT = 2^16,高16位表示读锁,相当于读锁数量加1
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
// 获取读锁成功,如果持有读锁的数量为0,将当前线程设置为第一个持有读锁线程,并设置持有读锁的数量为1
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
// 如果第一持有读锁的线程等于当前线程,那么持有读锁的数量加1
firstReaderHoldCount++;
} else {
// 缓存计数器,我们上面讲过HoldCounter主要用来记录线程持有的锁的重入次数
HoldCounter rh = cachedHoldCounter;
// 如果rh等于null或者当前线程id和缓存的计数器中的线程不是同一个线程,那么去本地计数器中获取线程对应的HoldCounter
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
// 缓存的HoldCounter是这个线程的HoldCounter,并且count等于0,将HoldCounter保存在本地计数器中
else if (rh.count == 0)
readHolds.set(rh);
// 该线程的锁的计数加1
rh.count++;
}
return 1;
}
// 获取锁失败重新尝试获取锁
return fullTryAcquireShared(current);
}
上面我们说了获取锁失败则在fullTryAcquireShared
重新尝试获取,其实fullTryAcquireShared
的代码和tryAcquireShared
差不多。
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
// 死循环
for (;;) {
int c = getState();
// 此处里面分为好几种情况,十分复杂
// 如果写锁的状态不为0,说明此时有线程获得了写锁
if (exclusiveCount(c) != 0) {
// 1、写锁被获取并且持有锁的线程和不等于当前线程,返回-1,获取锁失败
if (getExclusiveOwnerThread() != current)
return -1;
// 2、写锁被获取并且持有锁的线程等于当前线程
} else if (readerShouldBlock()) {
// 3、写锁空闲,且该读线程应该被阻塞
// readerShouldBlock:读锁是否阻塞,是返回true,不是false。
// 如果当前线程是持有读锁的第一个线程,确保我们不会在获取到读锁
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} else {
// 当前线程不是持有读锁的第一个线程
// 如果rh为null,则从缓存计数中获取
// 如果缓存计数也为空则从本地计数器中获取
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
// 如果rh的count为0则从本地计数器中移除该线程对应的HoldCounter
// 4、当前读线程需要阻塞且是非重入(当前线程还未获取读锁的),获取失败。
if (rh.count == 0)
readHolds.remove();
}
}
// 如果rh.count等于0,返回-1,表示获取锁失败
// 4、这里表示的是该读线程需要阻塞,且是未重入的(即该线程之前未获取到读锁),获取失败
if (rh.count == 0)
return -1;
}
}
// 共享锁(读锁)的数量大于最大值,抛异常
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 修改state的值,尝试去获取读锁
if (compareAndSetState(c, c + SHARED_UNIT)) {
// 获取读锁成功,且持有读锁的数量为0,初始化
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
// 如果第一持有读锁的线程等于当前线程,那么持有读锁的数量加1
firstReaderHoldCount++;
} else {
// rh为null,从缓存计数器中获取
if (rh == null)
rh = cachedHoldCounter;
// 仍然为null,从本地计数器中获取
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
// 如果count等于0,将其保存到本地计数器
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
// 获取成功
return 1;
}
}
}
读锁的释放
// ReentrantReadWriteLock.ReadLock.unlock()
public void unlock() {
sync.releaseShared(1);
}
// AbstractQueuedSynchronizer.releaseShared()
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
// ReentrantReadWriteLock.Sync.tryReleaseShared()
// 此处unused为1
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
// 如果当前线程是第一个持有读锁的线程
if (firstReader == current) {
// 如果第一个持有读锁的线程的重入次数为1,则将firstReader设置为null
if (firstReaderHoldCount == 1)
firstReader = null;
// 如果第一个持有读锁的线程的重入次数不为1,将重入次数减1
else
firstReaderHoldCount--;
// 如果当前线程不是第一个持有读锁的线程
} else {
HoldCounter rh = cachedHoldCounter;
// 如果rh为空从本地计数器中获取
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
// 如果count小于等于1,则将其该线程对应的HoldCounter从本地计数器中移除,说明该线程完全释放了锁
if (count <= 1) {
readHolds.remove();
// 如果count小于等于0抛异常
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
for (;;) {
// 死循环,读锁的线程持有数量减1
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// 释放读锁对其他读线程没有任何影响,
// 但可以允许等待的写线程继续,如果读锁、写锁都空闲1253254570
return nextc == 0;
}
}
注意:在读锁的获取或释放时,锁的重入次数(holdcount)和锁的获取次数(state)都要同步的加1或减1。
写锁的获取
// acquires为1
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
// 写锁的持有数量
int w = exclusiveCount(c);
// 如果c不等于0,表示有线程正在持有锁
if (c != 0) {
// 如果c != 0和w == 0并且持有锁的线程不等于当前线程,获取锁失败
// c !=0和w=0,这句表示写锁的数量为0,读锁的数量不为0
if (w == 0 || current != getExclusiveOwnerThread())
return false;
// 如果c != 0,w!=0,持有锁线程等于当前线程,并且写锁的重入次数大于写锁最大可重入次数,抛出异常
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 写锁的重入次数加1
// 这里为什么说是写锁的冲入次数而不是数量呢?
// 因为写锁同一时刻只能被同一线程持有,而写锁是可重入的因此state的低16位表示的写锁的重入次数
setState(c + acquires);
// 获取锁成功
return true;
}
// c=0,表示没有线程持有锁
// writerShouldBlock:写线程是否阻塞,true是,false否
// compareAndSetState尝试加锁失败,返回false
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
// 加锁成功设置持有锁线程,注意只有写锁才会设置持有锁线程,读锁不会设置
setExclusiveOwnerThread(current);
return true;
}
写锁的释放
protected final boolean tryRelease(int releases) {
// 不是排它锁抛异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 写锁的重入次数减1
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
// 如果写锁的重入数等于0,将持有锁的线程设置为null
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
// free为true释放成功,false失败
return free;
}
Sync的钩子方法
在Sync中暴露了两个钩子方法给子类去重写,分别是readerShouldBlock
、writerShouldBlock
,这两个方法在我们上面分析锁的获取释放时有提到,但没有详细深入,其实这两个方法十分重要。我们知道在Sync有公平(FairSync
)和非公平(NonfairSync
)两个子类,这两个子类中这两个方法的实现都不相同,公平策略和非公平策略。
公平类(FairSync
)
我们看到在公平类中这两个方法都是通过调用hasQueuedPredecessors
方法实现的,下面我们来进行详细的分析。
static final class FairSync extends Sync {
// 当AQS队列未初始化或第一个等待节点和当前线程相同时返回false,即不阻塞,不需要排队获取锁
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
// 当AQS队列未初始化或第一个等待节点和当前线程相同时返回false,即不阻塞,不需要排队获取锁
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
// 判断队列中是否有等待节点,此处分为三种情况
// 1、队列未初始化,头节点和尾节点都为null,所以返回fasle,队列中没有等待的节点,当然不需要排队
// 2、队列以初始化,头节点的后续节点为空,此时s.thread肯定为null,所以s.thread != Thread.currentThread()条件不成立,即表示队列中只有一个节点,此时我们要注意的是我们的头节点是持有锁的节点,如果队列中只有一个头节点,那么表示队列中没有等待的节点,需要排队,返回true
// 3、队列以初始化,头节点后续节点不为空,即队列中不止一个节点,若后续节点的线程不等于当前线程,即比较排队的第一个节点和当前节点是否相同,是则不需要排队尝试获取锁(返回false),不是需要排队(返回true)
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
非公平(NonfairSync
)
static final class NonfairSync extends Sync {
// 返回false,表示写锁永远不阻塞,为什么这么写?因为写锁只有在锁空闲的时候或者是重入的时候才能够加锁成功,没必要阻塞,影响性能
final boolean writerShouldBlock() {
return false;
}
// 1、队列未初始化,2、队列中只有一个节点,3、共享锁,4、后继节点线程为空,线程取消,这四种情况不阻塞,不需要排队
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
}
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
// 1、队列未初始化,false
// 2、队列初始化,后继节点为空,即队列中只有一个节点,false
// 3、队列初始化,后继节点不为空,是共享锁,false
// 4、队列初始化,后继节点不为空,不是共享锁,后继节点的线程为空,线程取消,false
// 5、队列初始化,后继节点不为空,不是共享锁,后继节点的线程不为空,true
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
参考博客:
https://segmentfault.com/a/1190000015768003
https://blog.51cto.com/14267003/2408707
e
// 3、队列初始化,后继节点不为空,是共享锁,false
// 4、队列初始化,后继节点不为空,不是共享锁,后继节点的线程为空,线程取消,false
// 5、队列初始化,后继节点不为空,不是共享锁,后继节点的线程不为空,true
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
**参考博客:**
[https://segmentfault.com/a/1190000015768003](https://segmentfault.com/a/1190000015768003)
[https://blog.51cto.com/14267003/2408707](https://blog.51cto.com/14267003/2408707)
[https://www.cnblogs.com/xiaoxi/p/9140541.html](https://www.cnblogs.com/xiaoxi/p/9140541.html)