}
}
输出结果,从这里可以看到 Thread-0 锁定期间,Thread-1 的读操作不受影响:
测试 读锁-写锁
相互阻塞:
@Slf4j
public class TestReadWriteLock {
public static void main(String[] args) throws InterruptedException {
Datacontainer datacontainer = new Datacontainer();
new Thread(() -> {
datacontainer.read();
}, “t1”).start();
Thread.sleep(100);
new Thread(() -> {
datacontainer.write();
}, “t2”).start();
}
}
结果:
注意:
- 读锁不支持条件变量
- 重入时升级不支持:即持有读锁的情况下去获取写锁,会导致获取写锁永久等待
- 重入时降级支持:即持有写锁的情况下去获取读锁
看一个重入降级获得锁栗子:
class CachedData {
Object data;
// 是否有效,如果失效,需要重新计算 data
volatile boolean cacheValid;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) {
// 获取写锁前必须释放读锁
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
// 判断是否有其它线程已经获取了写锁、更新了缓存, 避免重复更新
if (!cacheValid) {
data = …
cacheValid = true;
}
// 降级为读锁, 释放写锁, 这样能够让其它线程读取缓存
rwl.readLock().lock();
} finally {
rwl.writeLock().unlock();
}
}
// 自己用完数据, 释放读锁
try {
use(data);
} finally {
rwl.readLock().unlock();
}
}
}
1.2 原理演示
读写锁用的是同一个 Sycn 同步器,因此等待队列、state 等也是同一个。
t1 w.lock,t2 r.lock
1) t1 成功上锁,流程与 ReentrantLock 加锁相比没有特殊之处,不同是写锁状态占了 state 的低 16 位,而读锁使用的是 state 的高 16 位
2)t2 执行 r.lock
,这时进入读锁的 sync.acquireShared
(1) 流程,首先会进入 tryAcquireShared
流程。如果有写锁占据,那么 tryAcquireShared 返回 -1 表示失败
tryAcquireShared 返回值表示:
- -1 表示失败
- 0 表示成功
- 正数表示成功(而且数值是还有几个后继节点需要唤醒,读写锁返回1)
3)这时会进入 sync.doAcquireShared
(1) 流程,首先也是调用 addWaiter
添加节点,不同之处在于节点被设置为 Node.SHARED
模式而非 Node.EXCLUSIVE 模式,注意此时 t2 仍处于活跃状态
4)t2 会看看自己的节点是不是老二,如果是,还会再次调用 tryAcquireShared
(1) 来尝试获取锁
5)如果没有成功,在 doAcquireShared 内 for (;;)
循环一次,把前驱节点的 waitStatus 改为 -1,再 for (;😉 循环一 次尝试 tryAcquireShared(1) 如果还不成功,那么在 parkAndCheckInterrupt
() 处 park
t3 r.lock,t4 w.lock
这种状态下,假设又有 t3 加读锁和 t4 加写锁,这期间 t1 仍然持有锁,就变成了下面的样子
t1 w.unlock
这时会走到写锁的 sync.release
(1) 流程,调用 sync.tryRelease
(1) 成功,变成下面的样子
接下来执行唤醒流程 sync.unparkSuccessor
,即让老二恢复运行,这时 t2 在 doAcquireShared
内 parkAndCheckInterrupt
处恢复运行 这回再来一次 for (;;)
执行 tryAcquireShared
成功则让读锁计数加一
这时 t2 已经恢复运行,接下来 t2 调用 setHeadAndPropagate
(node, 1),它原本所在节点被置为头节点
事情还没完,在 setHeadAndPropagate
方法内还会检查下一个节点是否是 shared
,如果是则调用 doReleaseShared
() 将 head 的状态从 -1 改为 0 并唤醒老二,这时 t3 在 doAcquireShared
内 parkAndCheckInterrupt
处恢复运行
这回再来一次 for (;;)
执行 tryAcquireShared
成功则让读锁计数加一
这时 t3 已经恢复运行,接下来 t3 调用 setHeadAndPropagate
(node, 1),它原本所在节点被置为头节点
下一个节点不是 shared
了,因此不会继续唤醒 t4 所在节点
t2 r.unlock,t3 r.unlock
t2 进入 sync.releaseShared
(1) 中,调用 tryReleaseShared
(1) 让计数减一,但由于计数还不为零
t3 进入 sync.releaseShared
(1) 中,调用 tryReleaseShared
(1) 让计数减一,这回计数为零了,进入 doReleaseShared
() 将头节点从 -1 改为 0 并唤醒老二,即
之后 t4 在 acquireQueued
中 parkAndCheckInterrupt
处恢复运行,再次 for (;;)
这次自己是老二,并且没有其他竞争,tryAcquire
(1) 成功,修改头结点,流程结束
2.1 概述
该类自 JDK 8 加入,是为了进一步优化读性能,它的特点是在使用读锁、写锁时都必须配合【戳】使用
加解读锁:
long stamp = lock.readLock();
lock.unlockRead(stamp);
加解写锁:
long stamp = lock.writeLock();
lock.unlockWrite(stamp);
乐观读:StampedLock
支持 tryOptimisticRead()
方法(乐观读),读取完毕后需要做一次戳校验如果校验通过,表示这期间确实没有写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据安全。
long stamp = lock.tryOptimisticRead();
// 验戳
if(!lock.validate(stamp)){
// 锁升级
}
2.2 例子
提供一个 数据容器类 内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法:
@Slf4j
class DataContainerStamper {
private int data;
private final StampedLock lock = new StampedLock();
public DataContainerStamper(int data) {
this.data = data;
}
public int read(int readTime) {
//乐观读
long stamp = lock.tryOptimisticRead();
log.debug(“optimistic read lockuing … {}”, stamp);
//模拟读取时间
Sleeper.sleep(readTime);
//校验戳,通过直接返回
if (lock.validate(stamp)) {
log.debug(“read finish… {}”, stamp);
return data;
}
//锁的升级->读锁
log.debug(“updating to read lock… {}”, stamp);
try {
//获取读锁
stamp = lock.readLock();
log.debug(“read lock {}”, stamp);
Sleeper.sleep(readTime);
log.debug(“read finish… {}”, stamp);
return data;
} finally {
log.debug(“read unlock {}”, stamp);
//释放读锁
lock.unlockRead(stamp);
}
}
public void write(int newData) {
//加写锁
long stamp = lock.writeLock();
log.debug(“write lock {}”, stamp);
try {
Sleeper.sleep(2);
this.data = newData;
} finally {
log.debug(“write unlock {}”, stamp);
//释放写锁,需要传入戳
lock.unlockWrite(stamp);
}
}
}
测试 读-读
可以优化:
public class TestStampedLock {
public static void main(String[] args) {
DataContainerStamper dataContainer = new DataContainerStamper(1);
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
线程、数据库、算法、JVM、分布式、微服务、框架、Spring相关知识
一线互联网P7面试集锦+各种大厂面试集锦
学习笔记以及面试真题解析
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
、实战项目、讲解视频,并且会持续更新!**
如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)
线程、数据库、算法、JVM、分布式、微服务、框架、Spring相关知识
[外链图片转存中…(img-xS5rXK6v-1713412632493)]
一线互联网P7面试集锦+各种大厂面试集锦
[外链图片转存中…(img-bKa3BdkV-1713412632493)]
学习笔记以及面试真题解析
[外链图片转存中…(img-cMpH7wBt-1713412632494)]
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!