源码分析:ReentrantReadWriteLock之读写锁
简介
ReentrantReadWriteLock 从字面意思可以看出,是和重入、读写有关系的锁,实际上 ReentrantReadWriteLock 确实也是支持可重入的读写锁,并且支持公平和非公平获取锁两种模式。
为什么会出现读写锁?
普通锁可以保证共享数据在同一时刻只被一个线程访问,就算有多个线程都只是读取的操作,也还是要排队等待获取锁,我们知道数据如果只涉及到读操作,是不会出现线程安全方面的问题的,那这部分加锁是不是可以去掉?或者是加锁不互斥?如果在读多写少的情况下,使用普通的锁,在所有读的情况加锁互斥等待会是一个及其影响系统并发量的问题,如果所有的读操作不互斥,只有涉及到写的时候才互斥,这样会不会大大的提高并发量呢?答案是肯定的,ReentrantReadWriteLock 就是这样干的,读读不互斥,读写、写读、写写都是互斥的,可以大大提高系统并发量。
源码分析
类结构
ReentrantReadWriteLock 仅实现了ReadWriteLock接口
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {...}
ReadWriteLock 接口仅有两个方法,分别是 readLock()
和 writeLock()
;
主要属性
ReentrantReadWriteLock 有3个重要的属性,分别是读锁readerLock,写锁writerLock和同步器sync,源码如下:
private final ReentrantReadWriteLock.ReadLock readerLock;
private final ReentrantReadWriteLock.WriteLock writerLock;
final Sync sync;
主要内部类
Sync:同步器,继承至AbstractQueuedSynchronizer,定义了两个抽象方法,用于两种模式下自定义实现判断是否要阻塞
abstract static class Sync extends AbstractQueuedSynchronizer{
...
abstract boolean readerShouldBlock();
abstract boolean writerShouldBlock();
...
}
NonfairSync:非公平同步器,用于实现非公平锁,继承Sync
static final class NonfairSync extends Sync {...}
FairSync:公平同步器,用于实现公平锁,继承Sync
static final class FairSync extends Sync {...}
ReadLock:读锁,实现了Lock接口,持有同步器Sync的具体实例
public static class ReadLock implements Lock, java.io.Serializable {
...
private final Sync sync;
...
}
WriteLock:写锁,实现了Lock接口,持有同步器Sync的具体实例
public static class WriteLock implements Lock, java.io.Serializable {
...
private final Sync sync;
...
}
构造方法
有两个默认的构造方法,无参默认采用非公平锁,有参传入true使用公平锁
public ReentrantReadWriteLock() {
this(false);
}
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; }
获取读锁:readLock.lock()
读锁主要是按照共享模式来获取锁的,在前面讲AQS的例子中——基于AQS实现自己的共享锁,也是差不多的流程,只不过不同的锁的实现方法tryAcquireShared有一定的区别。ReentrantReadWriteLock 读锁获取过程源码如下:
public void lock() {
// 共享模式获取锁
sync.acquireShared(1);
}
// acquireShared 是AQS框架里面的代码
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
// tryAcquireShared 是RRWLock.Sync 里面的自己实现,所以这里没有公平和非公平所谓之称
protected final int tryAcquireShared(int unused) {
// 当前想要获得锁的线程
Thread current = Thread.currentThread();
// 获取state值
int c = getState();
// 独占锁被占用了,并且不是当前线程占有的,返回-1,出去要排队
if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
return -1;
// 读锁共享锁的次数
int r = sharedCount(c);
// 判断读是否要阻塞,读共享锁的次数是否超过最大值,CAS 更新锁state值
// readerShouldBlock 的返回要根据同步器是否公平的具体实现来决定
if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
// r==0, 设置第一次获得读锁的读者
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
// 持有第一个读者读锁的线程重入计数
firstReaderHoldCount++;
} else {
// 除第一个线程之后的其他线程获得读锁
// 每个线程每次获得读锁重入计数+1
// readHolds 就是一个ThreadLocal,里面放的HoldCounter,用来统计每个线程的重入次数
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
// 获得读锁,返回1
return 1;
}
// 上面if分支没进去时,走这里尝试获取读锁
return fullTryAcquireShared(current);
}
上面代码中的readerShouldBlock()
方法有两种情况下会返回true:
公平模式下,调用的
AQS.hasQueuedPredecessors()
方法static final class FairSync extends Sync {
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
} public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
// head 头结点是当前持有锁的节点,它的下一个节点不是当前线程,返回true,表示应该要阻塞当前线程
return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}
上面代码的主要思想就是:看一下队头排队等待获取锁的线程是不是当前线程,不是的话就应该要阻塞当前线程;
非公平模式下,最终调用的
AQS.apparentlyFirstQueuedIsExclusive()
方法static final class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
final boolean writerShouldBlock() {
return false; // writers can always barge
}
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
}
// apparentlyFirstQueuedIsExclusive 方法是AQS里面的方法
final boolean apparentlyFirstQueuedIsExclusive() {
// h 是同步队列的头结点,当前持有锁的节点
// s 是下一个应该获得锁的节点
Node h, s;
// s 节点如果不是共享模式(在RRWLock 里面就是读锁的意思),s节点是排他模式(想要写锁)返回true,
return (h = head) != null && (s = h.next) != null && !s.isShared() && s.thread != null;
}
上面代码的主要思想就是:看一下队头排队等待获取锁的第一个线程是不是要获取写锁,如果是就返回true,表示要阻塞当前线程,当前线程前面还有个要获得写锁的线程在排队呢!如果存在这种情况,其他获取读锁的线程都要给这种情况让路(写锁优先级更高)。那如果队列中第一个线程不是要获取写锁,那既然都是获取读锁,那就无所谓了,允许你插队。
上面的if分支进入失败时,会进入到fullTryAcquireShared()
方法再次尝试获得读锁有3种情况会进入到这个方法:
readerShouldBlock()
方法返回true,上面已经分析了,这个方法什么时候会返回true- 共享计数达到了最大值 MAX_COUNT(65535),可能性较小
- CAS 修改state 值失败,也就是获取锁失败
下面是 fullTryAcquireShared() 方法的分析:
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
// 自旋
for (;;) {
int c = getState();
// != 0 已经有其他线程获得了写锁
if (exclusiveCount(c) != 0) {
// 如果不是当前线程获得的写锁,返回-1,出去阻塞排队
if (getExclusiveOwnerThread() != current)
return -1;
// else we hold the exclusive lock; blocking here
// would cause deadlock.
} else if (readerShouldBlock()) {
// 要进入到这个分支,说明exclusiveCount(c) == 0 , 也就是写锁没被占用
// readerShouldBlock() == true , 公平模式下,同步队列中有其他线程在排队,非公平模式下,有即将要获得写锁的线程
// readerShouldBlock() 返回true ,也就是要阻塞当前线程的意思
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} else {
// 进入到这里,说明第一个读锁不是当前线程获得的
// rh 可以理解为当前线程的重入计数
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
// 返回-1,阻塞当前线程,出去排队
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 {
// 非第一个获得读锁的线程
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++;
// 缓存成功获取readLock的最后一个线程的计数
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
如果上面fullTryAcquireShared()方法还是没有获得锁,返回-1,就会进入下面的doAcquireShared(int arg)
方法:
// doAcquireShared 方法是AQS里面的代码,非RRWLock 实现
private void doAcquireShared(int arg) {
// 添加一个共享模式的节点到同步队列,并返回当前节点
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
// 中断标识
boolean interrupted = false;
// for循环自旋操作
for (;;) {
// 在同步队列中,当前节点的前驱结点
final Node p = node.predecessor();
if (p == head) {
// 如果前驱结点是头结点,说明排队轮到当前节点获得锁
// tryAcquireShared 再次尝试获取锁,上面的逻辑一模一样
int r = tryAcquireShared(arg);
if (r >= 0) {
// >=0 说明成功获得了锁
// 设置新的头结点,并检查后面是否是在获得读锁,如果是就唤醒它
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
// 阻塞期间线程被中断了
selfInterrupt();
failed = false;
return;
}
}
// 阻塞中断线程
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private void setHeadAndPropagate(Node node, int propagate) {
// 旧的头结点
Node h = head; // Record old head for check below
// 获得锁的线程节点设置为新的头结点
setHead(node);
if (propagate > 0 || h == null || h.waitStatus < 0 || (h = head) == null || h.waitStatus < 0) {
Node s = node.next;
// 检查获得锁的下一个节点s是否是共享模式的节点(读)
if (s == null || s.isShared())
doReleaseShared();
}
}
private void doReleaseShared() {
// 自旋
for (;;) {
Node h = head;
// 同步队列不为空
if (h != null && h != tail) {
int ws = h.waitStatus;
// -1 :表示当前节点的后继节点包含的线程需要运行,也就是unpark
if (ws == Node.SIGNAL) {
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
}
// 只会唤醒一个节点,在调用上面代码过程中,如果head节点变了,就会一直自旋,直到成功
if (h == head) // loop if head changed
break;
}
}
获取读锁过程总结
- 尝试去获取锁
tryAcquireShared()
- 在
tryAcquireShared()
中成功获得锁,就直接退出,执行lock() 之后的代码逻辑- 如果有其他线程已经占用了写锁,退出方法,返回-1,获取锁失败
- 检查是否要阻塞当前的线程
readerShouldBlock()
,有两种情况下(也就是公平锁和非公平锁获取读锁的区别)会阻塞当前现在:- 如果是公平锁,会看一下队头排队等待获取锁的线程是不是当前线程,不是的话就应该要阻塞当前线程;公平模式下是不允许插队的!
- 如果是非公平锁,看一下队头排队等待获取锁的第一个线程是不是要获取写锁,如果是表示要阻塞当前线程,写锁优先级更高!
- 检查读锁计数是否已经到了最大值(65535)
- 上面检查通过,才尝试CAS 修改同步状态,修改成功,代表成功获取读锁,退出方法返回1
- 成功获取读锁,如果是第一个获得读锁的线程,会缓存该线程
firstReader
,如果是重入,会进行重入计数,如果是新的线程获得读锁,会用一个ThreadLocal来保存重入计数
- 成功获取读锁,如果是第一个获得读锁的线程,会缓存该线程
- 如果到上面还没获取到锁(可能是CAS修改同步状态失败),会进行自旋继续尝试获取锁,对应方法
fullTryAcquireShared()
,该方法要么获取锁成功,要么获取锁失败,直到退出整个tryAcquireShared()
方法
- 如果
tryAcquireShared()
中没有获得锁,进入到AQS的doAcquireShared
方法,排队、阻塞线程doAcquireShared
方法也是一个自旋的操作,没有获取到锁,就会阻塞线程,等待被唤醒后继续获取锁,知道获取锁成功为止
释放读锁:readLock.lock()
读锁释放锁的逻辑如下:
public void unlock() {
// 开始释放读锁
sync.releaseShared(1);
}
//AQS框架中 的方法
public final boolean releaseShared(int arg) {
// tryReleaseShared 在RRWLock 中的Sync里面
if (tryReleaseShared(arg)) {
// 唤醒后面的读锁节点
doReleaseShared();
return true;
}
return false;
}
// RRWLock.Sync 的实现方法
protected final boolean tryReleaseShared(int unused) {
// 当前线程
Thread current = Thread.currentThread();
if (firstReader == current) {
// 第一个读锁线程
if (firstReaderHoldCount == 1)
// 如果它只获得了一次锁,直接置为null
firstReader = null;
else
// 第一个线程获得读锁,并且重入获取锁很多次,慢慢减,直到为1,置为null
firstReaderHoldCount--;
} else {
// 不是第一个线程
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;
}
// 上面只是减重入的计数
// 下面是自旋,重置同步状态state值
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// CAS 修改成功,并且要state为0才是真正释放了读锁
// 如果有重入,只有释放最后一次才会返回true, 之后才会去尝试唤醒之后的节点
return nextc == 0;
}
}
private void doReleaseShared() {
// 自旋
for (;;) {
Node h = head;
// 同步等待的队列不为空
if (h != null && h != tail) {
int ws = h.waitStatus;
// 检查状态是否要唤醒下一个节点的线程
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
// 加入h节点是持有锁的节点,会唤醒它的下一个节点线程
unparkSuccessor(h);
} else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
// 理论上唤醒一个就会退出
if (h == head) // loop if head changed
break;
}
}
释放读锁过程总结:
- 减计数,包含线程重入获取锁的计数
- 从这里可以看出一个线程存在多次释放锁,会抛出异常
- 自旋,CAS 修改同步状态,重入获取锁的线程只有在state等于0时才是真正的释放锁成功
- 释放锁成功后,会唤醒队列中的下一个节点,下一个节点会继续获取锁
获取写锁:writeLock.lock()
public void lock() {
sync.acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
// 当前线程
Thread current = Thread.currentThread();
int c = getState();
// 写锁计数,>0的话说明写锁已经被占用了
int w = exclusiveCount(c);
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
// c != 0 and w == 0 可能共享锁已经被占用了,这时候写锁获取失败
// 同一个线程先获取读锁,再获取写锁,也会在这里返回false,获取写锁出去之后会阻塞自己,
// 然后自己的读锁也不会释放,其他线程也获取不了读锁,就出现了死锁
if (w == 0 || current != getExclusiveOwnerThread())
// c != 0 and w == 0 锁的持有者不是当前线程,返回false
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
// 超限了 65535
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
// 重入获取锁,计数+1
setState(c + acquires);
return true;
}
// writerShouldBlock的实现代码,以看上面读锁获取readerShouldBlock的分析
// 公平锁时,writerShouldBlock 调用的hasQueuedPredecessors()
// 非公平锁时,只返回false
if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
// CAS 修改失败,返回false
return false;
// 成功获取写锁,设置锁的拥有者线程
setExclusiveOwnerThread(current);
return true;
}
如果上面方法没有获取到写锁,会执行acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
,这块的代码分析,可以查看之前的文章,关于AQS的分析或者ReentrantLock的分析。
释放写锁:writeLock.unlock()
释放写锁的逻辑比较简单,一般加锁和解锁都是成对出现的,所以这里解锁并不需要同步互斥的手段来进行,源代码如下:
public void unlock() {
sync.release(1);
}
// AQS 框架的代码
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) {
// 校验是否是当前线程持有写锁
if (!isHeldExclusively())
// 释放别人的写锁,抛出异常
throw new IllegalMonitorStateException();
// 计算下一个同步状态值
int nextc = getState() - releases;
// 重入的情况,是否已经完全释放了
boolean free = exclusiveCount(nextc) == 0;
if (free)
// 完全释放了,设置锁的持有者线程
setExclusiveOwnerThread(null);
//
setState(nextc);
return free;
}
完全释放锁成功后,唤醒下一个节点的逻辑在AQS的unparkSuccessor代码中,不需要RRWLock来实现。
死锁问题
在上面获取写锁的过程中,分析了同一个线程先获取读锁,再获取写锁,写锁的逻辑会阻塞自己的线程,但是写锁和读锁又是同一个线程,相当于前面的写锁也被阻塞了,这时候写锁没地方释放,读锁也没有地方释放,其他线程读锁和写锁也都获取不了了,因为前面有个写锁在排队获取。
public static void main(String[] args) throws InterruptedException{
ReentrantReadWriteLock lock = new ReentrantReadWriteLock(false);
Lock writeLock = lock.writeLock();
Lock readLock = lock.readLock();
new Thread(new Runnable(){
@SneakyThrows
@Override
public void run(){
TimeUnit.SECONDS.sleep(1);
// 模拟1秒后其他线程来获得读锁
System.out.println(Thread.currentThread().getName()+":准备获得读锁");
readLock.lock();
System.out.println(Thread.currentThread().getName()+":线程获得读锁");
readLock.unlock();
System.out.println(Thread.currentThread().getName()+":释放了读锁");
}
},"T0").start();
readLock.lock();
System.out.println(Thread.currentThread().getName()+":获得了读锁");
writeLock.lock();
System.out.println(Thread.currentThread().getName()+":获得了写锁");
readLock.unlock();
System.out.println(Thread.currentThread().getName()+":解读锁");
writeLock.unlock();
System.out.println(Thread.currentThread().getName()+":解写锁");
}
输出结果:
main:获得了读锁
T0:准备获得读锁
从上面输出结果可以看出,只有main线程获得了读锁,自己获取写锁被阻塞,其他线程也获取不了读锁,最后产生了死锁。
写线程饥饿问题
ReentrantReadWriteLock 的读写是互斥的,意思就是读锁在获取锁后,在还没有释放锁的期间,获取写锁的进程来了也要阻塞自己排队,如果有大量的线程获取了读锁,之后有一个线程获取写锁,写锁就可能一直获取不到写锁,引起写锁线程“饥饿”,这就是RRWLock的写线程饥饿问题。
我们用代码来验证一下上面的结论:
private static void testWriteLockHunger() throws InterruptedException{
ReentrantReadWriteLock lock = new ReentrantReadWriteLock(false);
Lock writeLock = lock.writeLock();
Lock readLock = lock.readLock();
// T0 线程先获得读锁,并持有一段时间
new Thread(new Runnable(){
@SneakyThrows
@Override
public void run(){
readLock.lock();
System.out.println(Thread.currentThread().getName()+":最开始线程获得读锁");
// 睡眠15秒,一直持有读锁
TimeUnit.SECONDS.sleep(15);
readLock.unlock();
System.out.println(Thread.currentThread().getName()+":释放了读锁");
}
},"T0").start();
// 1秒后其他线程再来获取锁,保证前面那个T0线程最先获得读锁
TimeUnit.SECONDS.sleep(1);
// TW-1 来排队获取写锁,是为了让后面的读锁,写锁都入队排队
new Thread(new Runnable(){
@SneakyThrows
@Override
public void run(){
System.out.println(Thread.currentThread().getName()+":准备获得写锁");
writeLock.lock();
System.out.println(Thread.currentThread().getName()+":获得写锁");
TimeUnit.SECONDS.sleep(5);
writeLock.unlock();
System.out.println(Thread.currentThread().getName()+":释放了写锁");
}
},"TW-1").start();
TimeUnit.SECONDS.sleep(1);
// 这里睡眠1秒是为了写锁排队在读锁获取的前面
IntStream.range(1,5).forEach(i->{
new Thread(new Runnable(){
@SneakyThrows
@Override
public void run(){
System.out.println(Thread.currentThread().getName()+":准备获取读锁");
readLock.lock();
System.out.println(Thread.currentThread().getName()+":获取了读锁");
// 持有部分时间的读锁
TimeUnit.SECONDS.sleep(i*2);
readLock.unlock();
System.out.println(Thread.currentThread().getName()+":释放了读锁");
}
},"T-"+i).start();
});
// 最后再来个获取写锁的线程,肯定会在所有读锁的后面获取到写锁
new Thread(new Runnable(){
@SneakyThrows
@Override
public void run(){
System.out.println(Thread.currentThread().getName()+":准备获取写锁");
writeLock.lock();
System.out.println(Thread.currentThread().getName()+":获取了写锁");
// 持有部分时间的读锁
TimeUnit.SECONDS.sleep(2);
writeLock.unlock();
System.out.println(Thread.currentThread().getName()+":释放了写锁");
}
},"TW").start();
}
上面代码输出示例:
T0:最开始线程获得读锁
TW-1:准备获得写锁
T-1:准备获取读锁
T-2:准备获取读锁
T-4:准备获取读锁
T-3:准备获取读锁
TW:准备获取写锁
T0:释放了读锁
TW-1:获得写锁
TW-1:释放了写锁
T-1:获取了读锁
T-2:获取了读锁
T-4:获取了读锁
T-3:获取了读锁
T-1:释放了读锁
T-2:释放了读锁
T-3:释放了读锁
T-4:释放了读锁
TW:获取了写锁
TW:释放了写锁
从上面输出结果可以看出,TW写锁是最后才获取到写锁的,如果前面有大量的读锁在排队的话,写锁肯定就会造成饥饿的。
如果不想让获取写锁的线程“饥饿”怎么办呢?
可以把最后获取写锁的线程TW获取锁方式改造下,代码如下:
new Thread(new Runnable(){
@SneakyThrows
@Override
public void run(){
System.out.println(Thread.currentThread().getName()+":准备获取写锁");
while(!writeLock.tryLock()){
// 一直尝试获得写锁,直到成功
}
System.out.println(Thread.currentThread().getName()+":获取了写锁");
// 持有部分时间的读锁
TimeUnit.SECONDS.sleep(2);
writeLock.unlock();
System.out.println(Thread.currentThread().getName()+":释放了写锁");
}
},"TW").start();
测试输出结果:
T0:最开始线程获得读锁
TW-1:准备获得写锁
T-1:准备获取读锁
T-2:准备获取读锁
T-3:准备获取读锁
T-4:准备获取读锁
TW:准备获取写锁
T0:释放了读锁
TW-1:获得写锁
TW-1:释放了写锁
TW:获取了写锁
TW:释放了写锁
T-4:获取了读锁
T-2:获取了读锁
T-3:获取了读锁
T-1:获取了读锁
T-1:释放了读锁
T-2:释放了读锁
T-3:释放了读锁
T-4:释放了读锁
从上面输出结果可以看出,TW线程成功的在读锁前面获取到了写锁;那为什么会这样呢?因为采用lock()来获取锁,如果第一次tryAcquire没有获取到锁,就会被加入到队列等待,只要进入了队列,就只能按照队列中的顺序来获得锁了,而tryLock在获取锁失败后是不会加入到同步等待队列中去的,从而实现“插队”的功能。
总结
- 读写锁除了读读不互斥,读写、写读、写写都是互斥的。
- 读写互斥的意思是A线程先获取读锁不释放,B来获取写锁,这时候B线程一样的要阻塞自己
- 同一个线程先获取读锁,再获取写锁,会导致死锁
- 允许同一个线程先获取写锁,再获取读锁;但是不允许同一个线程先获取读锁,再获取写锁;可以理解为允许锁降级,不允许锁升级。
- 公平锁模式下,获取写锁会去检查队列中是否有排队更久的线程。
- 非公平锁模式下,获取写锁不会去检查同步队列中是否有排队更久的线程。
- 公平锁模式下,获取读锁会去检查队列中是否有排队更久的线程。
- 非公平锁模式下,获取读锁会去检查队列中第一个等待获取的是不是写锁,如果存在就要阻塞当前获取读锁的线程(写锁优先级更高)。
源码分析:ReentrantReadWriteLock之读写锁的更多相关文章
- java读写锁源码分析(ReentrantReadWriteLock)
读锁的调用,最终委派给其内部类 Sync extends AbstractQueuedSynchronizer /** * 获取读锁,如果写锁不是由其他线程持有,则获取并立即返回: * 如果写锁被其他 ...
- ReentrantReadWriterLock源码(state设计、读写锁、共享锁、独占锁及锁降级)
ReentrantReadWriterLock 读写锁类图(截图来源https://blog.csdn.net/wangbo199308/article/details/108688148) stat ...
- [源码分析]读写锁ReentrantReadWriteLock
一.简介 读写锁. 读锁之间是共享的. 写锁是独占的. 首先声明一点: 我在分析源码的时候, 把jdk源码复制出来进行中文的注释, 有时还进行编译调试什么的, 为了避免和jdk原生的类混淆, 我在类前 ...
- 锁对象-Lock: 同步问题更完美的处理方式 (ReentrantReadWriteLock读写锁的使用/源码分析)
Lock是java.util.concurrent.locks包下的接口,Lock 实现提供了比使用synchronized 方法和语句可获得的更广泛的锁定操作,它能以更优雅的方式处理线程同步问题,我 ...
- Java并发指南10:Java 读写锁 ReentrantReadWriteLock 源码分析
Java 读写锁 ReentrantReadWriteLock 源码分析 转自:https://www.javadoop.com/post/reentrant-read-write-lock#toc5 ...
- java并发锁ReentrantReadWriteLock读写锁源码分析
1.ReentrantReadWriterLock 基础 所谓读写锁,是对访问资源共享锁和排斥锁,一般的重入性语义为如果对资源加了写锁,其他线程无法再获得写锁与读锁,但是持有写锁的线程,可以对资源加读 ...
- 深入理解读写锁—ReadWriteLock源码分析
转载:https://blog.csdn.net/qq_19431333/article/details/70568478 ReadWriteLock管理一组锁,一个是只读的锁,一个是写锁.读锁可以在 ...
- 源码分析:升级版的读写锁 StampedLock
简介 StampedLock 是JDK1.8 开始提供的一种锁, 是对之前介绍的读写锁 ReentrantReadWriteLock 的功能增强.StampedLock 有三种模式:Writing(读 ...
- 【JUC】JDK1.8源码分析之ReentrantReadWriteLock(七)
一.前言 在分析了锁框架的其他类之后,下面进入锁框架中最后一个类ReentrantReadWriteLock的分析,它表示可重入读写锁,ReentrantReadWriteLock中包含了两种锁,读锁 ...
随机推荐
- Js电子时钟
简单版电子时钟,需要以下几个步骤 1. 封装一个函数 返回当前的时分秒 2. 使用定时器使当前以获取到的系统时间走动,每间隔一面调用 3. 把获取到的时间放到span盒子里,添加样式 效果展示 实现 ...
- 用算法去扫雷(go语言)
最初的准备 首先得完成数据的录入,及从扫雷的程序读取界面数据成为我的算法可识别的数据 其次是设计扫雷的算法,及如何才能判断格子是雷或者可以点击鼠标左键和中键. 然后将步骤2的到的结果通过我的程序实现鼠 ...
- C语言和单片机C语言为什么会有差异?虽不同但理同!
许多小伙伴在学完C语言后想入门单片机,但学着学着发现明明都是C语言,为什么单片机C语言和我当初学的C语言有差异呢? 今天小编就来梳理我们平时所学的C语言与单片机C语言的有什么样的不同. 单片机c语言比 ...
- Docker下部署springboot项目
1.背景 如何在docker容器环境下部署一个springboot项目? 2.具体步骤 第一步:准备一个springboot项目的xxxx.jar包 jar包中用于测试的一个接口如下 第二步:编写Do ...
- 【Flutter 混合开发】与原生通信-BasicMessageChannel
Flutter 混合开发系列 包含如下: 嵌入原生View-Android 嵌入原生View-iOS 与原生通信-MethodChannel 与原生通信-BasicMessageChannel 与原生 ...
- Linux入门到放弃之二《目录处理常用命令的使用方法》
一,目录操作命令 1.用pwd命令查看当前所在的目录: 2.用ls命令列出此目录下的文件和目录: 3.列出此目录下包括隐藏文件在内的所有文件和目录并且长格式显示: ( -l表示长格式,-a表示隐藏文 ...
- .Net Core实现基于Quart.Net的任务管理
前段时间给公司项目升级.net框架,把原先的任务管理平台用.net core实现,现做如下整理: 一.实现思路 之前的实现也是参考了博客园中其他文章实现的思路: 一个任务定义一个实现IJob接口的类, ...
- 020_Typora使用
目录 Typora使用 下载 安装 设置 使用 Typora使用 Typora插入本地图片为本地路径,网络上无法查看,所以建议只插入网络图片. 下载 百度搜索官网 下载 安装 设置 视图->显示 ...
- C#连接Access
连接数据库 string oleCon = @"Provider=Microsoft.ACE.OLEDB.12.0;Data Source= " + Application.Sta ...
- 微信小程序UI自动化:实践之后的记录01-选择工具/框架
目录 1. 前言 2. 工具/框架/库选择 2.1 miniprogram-automator官方介绍(摘自官方哈) 小程序自动化 特性 2.2 minium官方介绍 特性 3. 如何选择 4. 对应 ...