ReentrantReadWriteLock原理
ReentrantReadWriteLock
读读并发 读写互斥 写写互斥
基本使用
读读并发
场景:创建t1,t2线程两个线程。t1,t2线程都加读锁。
代码:
/**
* 读读并发
*/
@Slf4j(topic = "hhb")
public class ReadReadTest {
//创建一把读写锁
static ReentrantReadWriteLock readWriteLock=new ReentrantReadWriteLock();
//读锁
static Lock r = readWriteLock.readLock();
public static void main(String[] args) {
//读读并发
new Thread(() -> {
r.lock();
log.debug("t1 read 获取 锁");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
r.unlock();
}
}, "t1").start();
new Thread(() -> {
r.lock();
log.debug("t2 read 获取 锁");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
r.unlock();
}
}, "t2").start();
}
}
结果打印:
结果分析:
t1,t2都获取到读锁,并执行同步代码块。
读写互斥
场景:创建t1,t2线程两个线程。t1加读锁,加锁成功后睡眠两秒。t2加写锁,加锁成功后睡眠两秒。
代码:
/**
* 读写互斥
*/
@Slf4j(topic = "hhb")
public class ReadWriteTest {
static ReentrantReadWriteLock readWriteLock=new ReentrantReadWriteLock();
//读锁
static Lock r = readWriteLock.readLock();
//写锁
static Lock w = readWriteLock.writeLock();
public static void main(String[] args) {
//读写互斥
new Thread(() -> {
r.lock();
log.debug("t1 read 获取 锁");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
r.unlock();
}
}, "t1").start();
new Thread(() -> {
w.lock();
log.debug("t2 write 获取 锁");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
w.unlock();
}
}, "t2").start();
}
}
结果打印:
结果分析:
t1,t2线程执行。t1先获取到cpu资源先执行,获取到读锁后,打印 “t1 read 获取 锁” 然后睡眠2秒。
t1在睡眠过程中,t2线程获取到cpu资源开始执行。在执行过程中t2线程开始加写锁,发现readWriteLock对象已被t1加了读锁,读写锁互斥,导致t2加写锁失败,t2线程进入等待队列等待被唤醒。
当t1线程睡眠两秒后,t1释放了读锁,readWriteLock锁状态为无锁,唤醒t2。t2获取到cpu资源,加写锁成功后,执行t2线程的同步代码块,打印“t2 write 获取 锁”。
写写互斥
原理和读写互斥差不多!注意以上场景都是两个线程分别加读写锁。如果加读写锁在一个线程中,就要考虑锁的重入等情况。
写锁的加锁流程原理
当前用的是jdk1.8
写锁加锁调用:
- lock底层调用方法
w.lock();//加写锁
查看WriteLock中实现的lock方法
进入实现的lock最终调用acquire方法
acquire方法只要实现:先尝试加锁,如果加锁失败,当前线程睡眠并加入等待队列。
我们主要看一下tryAcquire方法
- 详解ReentrantReadWriteLock写锁加锁流程
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();
//获取锁的状态 默认为0 为int类型
int c = getState();
//因为读写锁是同一个对象,所以读锁标识和写锁标识都存在AQS中state中
//int 类型占32位,前16位标识读锁状态 后16位标识写锁状态
//当前获取写锁状态(后16位)
int w = exclusiveCount(c);
//c==0标识没人上锁
//c!=0 表示有人上锁但是不确定是读锁还是写锁
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
//如果有人已经上了读锁,当前线程加写锁 就要造成锁升级 直接失败
//如果有人已经上了写锁 ,当前线程加写锁。如果当前线程不等于已上写锁线程 直接失败
//w==0 表示有人上了读锁 还未上写锁
//current != getExclusiveOwnerThread() 表示当前线程不等于已上写锁线程
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//表示当前线程等于已上写锁线程 就会造成锁的重入
//重入了把w+1 标识的长度有限 但是这个判断基本没用
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
//没用超出重入的最大限制 则把w+1 (最大65535)
setState(c + acquires);
return true;
}
//当c==0 表示第一次加锁
//writerShouldBlock()判断当前线程需不需要排队
//公平锁:如果等待队列中有人排队 writerShouldBlock返回true
//公平锁:如果等待队列中没人排队 writerShouldBlock返回false
//非公平锁:writerShouldBlock返回false
//如果writerShouldBlock()返回true, !compareAndSetState(c, c + acquires)不会执行
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
//加锁成功则把当前持有锁的线程设置自己
setExclusiveOwnerThread(current);
return true;
}
- 图解写锁上锁流程
建议结合代码一起看
问题:为什么会有锁的重入?
现在有这么一个场景:类中有两个同步方法A,B,方法内都加锁解决同步问题 。
public class ReadWtiteLock {
static ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
static ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
public static void main(String[] args) {
//创建t1线程执行A方法
new Thread(()->{
A();
},"t1").start();
}
private static void A(){
writeLock.lock();
System.out.println("A");
B();
writeLock.unlock();
}
private static void B(){
writeLock.lock();
System.out.println("B");
writeLock.unlock();
}
}
启动t1线程执行A方法,本应该A方法先加一把写锁,调用B方法时,前面的写锁还没有释放。B等待A释放锁,而A等待B执行完,最终互相等待造成死锁。但是这时B获取死锁过程中发现当前线程等于已上写锁线程(都是t1线程加的写锁),就会造成锁的重入,B方法就正常加写锁成功,AB方法最终执行成功。
有人会问:为什么不可以用下面的写法,非要加两次锁 ?B不加锁 A加锁
private static void A(){
writeLock.lock();
System.out.println("A");
B();
writeLock.unlock();
}
private static void B(){
System.out.println("B");
}
为了防止其他线程直接调用B方法从而产生线程安全问题。
写锁的解锁锁流程原理
public void unlock() {
sync.release(1);
}
可以看到写锁unlock调用release()方法,我们我看一下解锁的逻辑
public final boolean release(int arg) {
//首先尝试解锁
if (tryRelease(arg)) {
//如果解锁成功,获取等待队列中头节点
Node h = head;
//如果头节点不为空,且Node节点中waitStatus属性不为0
//waitStatus 默认为0,如果头节点的下一个节点需要被头节点唤醒,此时头节点的waitStatus值会在下一个节点入队列时修改为-1
if (h != null && h.waitStatus != 0)
//唤醒头节点的下一个节点
unparkSuccessor(h);
return true;
}
return false;
}
主要看一下尝试解锁tryRelease方法是怎么实现的?
protected final boolean tryRelease(int releases) {
//判断这把写锁是否被持有,如果没有被持有,却来解锁 抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//得到当前读写锁状态state后减一
int nextc = getState() - releases;
//调用exclusiveCount(nextc)获取到读写锁中写锁状态(就是获取state值的后16位)
//判断写锁状态是否为0,0表示当前锁状态未加写锁,1表示写锁被持有,2~65535表示锁被重入对应次数
boolean free = exclusiveCount(nextc) == 0;
//写锁状态为0,0表示当前锁状态未加写锁
if (free)
//解锁成功,把当前锁持有线程设置为空
setExclusiveOwnerThread(null);
//把当前写锁状态改为nextc的值
setState(nextc);
//这里如果写锁状态是否为0就返回true 代表解锁成功
//如果写锁状态大于0 ,说明存在重入的情况 需要多次调用unlock方法 直到写锁状态等于0
return free;
}
读锁的原理正在持续更新中 !