这次要介绍一下ReentrantReadWriteLock
,所谓的读写锁,其实也就是针对一些读多写少的情况,让读可以共享,写独占,提高效率。我们先来看个简单的例子,2个写线程,3个读线程,来观察下结果:
public class ReentrantReadWriteLockTest {
private static StringBuilder stringBuilder = new StringBuilder();
private static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
private static Lock readLock = reentrantReadWriteLock.readLock();
private static Lock writeLock = reentrantReadWriteLock.writeLock();
public static void main(String[] args) {
for (int i = 0; i < 2; i++) {
//写写互斥 写读互斥
new Thread(() -> {
while (true) {
try {
writeLock.lock();
System.out.println(Thread.currentThread().getName() + "开始写入:" + Thread.currentThread().getName());
stringBuilder.append(Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "写入完成");
} finally {
writeLock.unlock();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}, "写线程" + i).start();
}
for (int i = 0; i < 3; i++) {
//读读共享
new Thread(() -> {
while (true) {
try {
readLock.lock();
System.out.println(LocalDateTime.now() + "|" + Thread.currentThread().getName() + "开始读取:" + stringBuilder.toString());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(LocalDateTime.now() + "|" + Thread.currentThread().getName() + "开始完成");
} finally {
readLock.unlock();
}
}
}, "读线程" + i).start();
}
}
}
结果就是写线程是独占的,一个个完成,而读线程可以共享,因此也提高了读线程的效率。
写线程0开始写入:写线程0
写线程0写入完成
写线程1开始写入:写线程1
写线程1写入完成
2020-01-10T11:46:08.290991400|读线程1开始读取:写线程0写线程1
2020-01-10T11:46:08.291991400|读线程0开始读取:写线程0写线程1
2020-01-10T11:46:08.291991400|读线程2开始读取:写线程0写线程1
2020-01-10T11:46:09.323050400|读线程0开始完成
2020-01-10T11:46:09.323050400|读线程2开始完成
2020-01-10T11:46:09.323050400|读线程1开始完成
接下来我们就来分析下这个读写锁的原理,首先看下他的结构,当然从源码里也能看到,但是这个图看起来比较直观,看上去很像很多东西,我们慢慢分析:
其实可以看到,他内部有同步器,也可以有公平和非公平的锁,还有创建了读锁和写锁,其实他本身所有的功能就是考这些同步器实现的:
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
先来看看写锁,其实即是实现了Lock
接口,具体的实现都是交给sync
完成:
同步器sync
是构造的时候外部传进来的:
protected WriteLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
跟写锁也类似,只是不支持条件同步,因为是共享的嘛,还要条件干嘛:
public Condition newCondition() {
throw new UnsupportedOperationException();
}
非公平同步器,是同步器的子类,只是两个方法不一样:
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
final boolean writerShouldBlock() {
return false; // writers can always barge
}
final boolean readerShouldBlock() {
/* As a heuristic to avoid indefinite writer starvation,
* block if the thread that momentarily appears to be head
* of queue, if one exists, is a waiting writer. This is
* only a probabilistic effect since a new reader will not
* block if there is a waiting writer behind other enabled
* readers that have not yet drained from the queue.
*/
return apparentlyFirstQueuedIsExclusive();
}
}
公平嘛,当然要排队:
static final class FairSync extends Sync {
private static final long serialVersionUID = -2274990926593161451L;
final boolean writerShouldBlock() {
return hasQueuedPredecessors();//看前面有没人排队
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}
接下去就是重点来了,核心基本都在这个同步器里,他把锁的状态用位来表示,int32位分成两半,高16位表示读锁已占资源,低16位表示写锁已占资源,我这里强调的是获取到的数值是已用的资源,不是还能用的资源,因为初始是0,用了一个+1,读写的上限都是65535个,所以读到的是已经用了多少个资源。如何获取这些资源呢,就用无符号按位移动和按位与来分别获取高16和低16位数据:
static final int SHARED_SHIFT = 16;//位移的位数,为了获取高16位
static final int SHARED_UNIT = (1 << SHARED_SHIFT);//读锁1次添加的单位
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;//65535个
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;//低16位都是1的遮罩,用于获取低16位
/** Returns the number of shared holds represented in count. */
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }//获取读锁状态,无符号右边移16个,即是读锁已用资源数
/** Returns the number of exclusive holds represented in count. */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }//获取写锁状态,位运算与,直接提取低16位是写锁已用资源数
接下去就是HoldCounter
,一看就知道是持有的数量,也就是每个线程持有的资源数,为什么每个线程还能拿多个,因为可以重入,所以可以获取多个啦:
/** 资源持有器 保存线程对应的锁资源的数量,缓存在cachedHoldCounter中
* A counter for per-thread read hold counts.
* Maintained as a ThreadLocal; cached in cachedHoldCounter.
*/
static final class HoldCounter {
int count; // initially 0
// Use id, not reference, to avoid garbage retention
final long tid = LockSupport.getThreadId(Thread.currentThread());//会调用本地方法获取
}
//缓存最后一个成功获取读锁的线程的HoldCounter
private transient HoldCounter cachedHoldCounter;
针对读锁共享的问题,如何将HoldCounter
跟每个线程绑定呢,就可以用ThreadLocal
,ThreadLocalHoldCounter
是他的子类,而且限定了HoldCounter
类型,这样每个线程就可以跟一个HoldCounter
绑定:
/** 每个线程都对应单独的HoldCounter
* ThreadLocal subclass. Easiest to explicitly define for sake
* of deserialization mechanics.
*/
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();//初始化默认值
}
}
private transient ThreadLocalHoldCounter readHolds;//读资源持有器组,为每一个线程存储一个资源持有器
private transient Thread firstReader;//记录第一个读线程
private transient int firstReaderHoldCount;//记录第一个读线程的持有资源数
基本的结构讲完了,接下去我们要看主要的流程了,其他细节结合具体流程说,默认是非公平锁:
public void lock() {
sync.acquire(1);
}
里面就是AQS的流程:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
但是这个方法重写了:
@ReservedStackAccess //为了避免多线程栈溢出
protected final boolean tryAcquire(int acquires) {
/*
* Walkthrough:
* 1. If read count nonzero or write count nonzero
* and owner is a different thread, fail.
* 2. If count would saturate, fail. (This can only
* happen if count is already nonzero.)
* 3. Otherwise, this thread is eligible for lock if
* it is either a reentrant acquire or
* queue policy allows it. If so, update state
* and set owner.
*/
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())//写资源为0,或者不是独占线程就返回失败
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;
}
主要思路就是获取已用的总的锁资源和已经用的写锁资源:
如果资源有被用了,写资源还没被用,说明有读线程在用,所以失败。
如果资源有被用了,写资源有用,但是占有资源的线程不当前线程,重入不满足,所以失败。
如果资源有被用了,写资源有用,占有资源的线程是当前线程,可重入,
但是如果已用写资源+要申请的写资源总数超过限制了,返回失败
否则就设置资源,返回成功
如果资源还没被用过,写应该被阻塞的话或者写不阻塞但是资源修改失败,返回失败
否则写不阻塞,修改资源成功,就设置独占线程,返回成功
可见这个主要是针对写锁来做限制的,这样要注意writerShouldBlock
在非公平锁就直接返回false
,而公平锁需要看前面有没人在排队hasQueuedPredecessors()
,这个前面的文章有讲过就不多说了。
剩下的都是以前讲过的独占锁的东西,就不说了:
可以参考下流程图:
接下去我们看怎么释放锁。
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;
}
这个方法重写了,但是也比较简单,就是取判断减去释放的后写锁资源是不是0了,是的话才算释放成功,要去唤醒后继,否则可能有重入,还有资源没释放:
@ReservedStackAccess
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())//条件同步才可以放
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;//判断是否释放完全
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
再来看看读锁:
public void lock() {
sync.acquireShared(1);
}
就是共享锁的结构啊:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
@ReservedStackAccess
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)) {//是否能修改成功
if (r == 0) {//如果已读资源为0
firstReader = current;//设置当前线程为第一个读线程
firstReaderHoldCount = 1;//设置第一个读线程持有资源的数量1
} else if (firstReader == current) {//如果第一个读线程就是当前线程,即可重入
firstReaderHoldCount++;//持有资源数+1
} else {//不是第一个读线程
HoldCounter rh = cachedHoldCounter;//获得缓存资源持有器里
if (rh == null ||//如果这个缓存资源持有器为null,或者资源持有器的线程不是当前线程
rh.tid != LockSupport.getThreadId(current))
cachedHoldCounter = rh = readHolds.get();//初始化资源持有器并保存到缓存中
else if (rh.count == 0)//缓存资源持有器存在,且资源线程是当前线程,就是重入 如果持有资源为0
readHolds.set(rh);//将资源持有器放入读资源持有器组里
rh.count++;//持有资源数+1
}
return 1;
}
return fullTryAcquireShared(current);
}
首先会判断是否已用写资源,如果有,那其他线程就不能来,独占的,除非是写线程本身又申请了读资源才继续,这里可能就是所谓的写锁降级成读锁吧,我不太明白降级,难道写锁和读锁有级别之分么,为什么要搞的那么深奥,只就说写线程里还能获取读资源。如果没有,就继续。
然后进行是否要阻塞读线程的阻塞,公平锁会进行排队,非公平锁会对写线程有个优化,不让当前线程去抢占资源,让写线程优先获得独占资源,主要是这个方法readerShouldBlock
中的apparentlyFirstQueuedIsExclusive
:
final boolean apparentlyFirstQueuedIsExclusive() {
//如果头结点和后续结点不为空,而且后续结点还是写线程独占的且不为空,就成功返回
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
firstReader
和firstReaderHoldCount
,也就是说有个缓存,提高性能。firstReader
是当前线程,表示重入,firstReaderHoldCount++
,cachedHoldCounter
的线程:readHolds
创建一个读资源持有器,并且缓存给 cachedHoldCounter
。readHolds
里。fullTryAcquireShared
,等于说前面的是简易版的。我们来看看完整的,其实也差不多,只是一个死循环,判断内容和上面差不多,只是针对CAS修改失败还会尝试,直到成功。注意如果阻塞了,也要分两种情况,一种如果是新来的,那真要阻塞,如果是重入,那就不需要:
final int fullTryAcquireShared(Thread current) {
/*
* This code is in part redundant with that in
* tryAcquireShared but is simpler overall by not
* complicating tryAcquireShared with interactions between
* retries and lazily reading hold counts.
*/
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 != LockSupport.getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();//删除,帮助GC,不然可能存在内存泄露
}
}
if (rh.count == 0)//如果持有器资源是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 != LockSupport.getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release //存入缓存
}
return 1;
}
}
}
再来看看释放资源:
public void unlock() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
主要是重写了tryReleaseShared(int unused)
:
@ReservedStackAccess
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {//如果第一个读线程是当前线程
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)//读资源为1就直接释放了
firstReader = null;
else
firstReaderHoldCount--;//否则资源数-1
} else {//不是第一个读线程
HoldCounter rh = cachedHoldCounter;//获取缓存资源持有器
if (rh == null ||//缓存为空或者当前线程不是缓存中的线程,缓存资源持有器只存最后一次成功获取锁的线程的数据
rh.tid != LockSupport.getThreadId(current))
rh = readHolds.get();//从读资源持有器组中获取
int count = rh.count;
if (count <= 1) {
readHolds.remove();//删除资源持有器
if (count <= 0)//如果发现数量<=0,说明在释放不是当前线程的资源数
throw unmatchedUnlockException();
}
--rh.count;
}
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;//读资源-1
if (compareAndSetState(c, nextc))//如果是0表示释放全完,否则还有没释放的资源
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
return nextc == 0;
}
}
这个其实也很好理解,就是各种情况释放资源,如果放完了,才算真的释放完成,才会去唤醒后续的共享结点。
下面这段代码说明了,写的线程还可以读:
这样是可以的:
因为写是独占的,是不会有其他线程干扰,所以可以在里面继续获得读锁,而且不需要竞争,完了之后全部释放即可。
下面的代码就限制了,如果读里面获取写,就阻塞,然后就死锁了:
死锁:
https://www.cnblogs.com/rain4j/p/10135283.html
https://www.jianshu.com/p/cd485e16456e
好了,今天就到这里了,希望对学习理解有帮助,大神看见勿喷,仅为自己的学习理解,能力有限,请多包涵。