我们知道ReadWriteLock支持读锁、写锁两种锁模式。而StampedLock支持三种:写锁、悲观读锁和乐观读。
其写锁、悲观读锁和ReadWriteLock的写锁、读锁的语义类似。不同在于:StampedLock的写锁和悲观读锁加锁成功后,都会返回一个stamp;释放锁时,需要传入该stamp。
那StampedLock的性能为啥比ReadWriteLock好呢?
核心在于StampedLock支持乐观读。ReadWriteLock支持多个线程同时读,但当多线程读时,所有的写操作会被阻塞;而StampedLock提供的乐观读,是允许一个线程获取写锁的,也就是说不是所有写操作都被阻塞。
乐观读的操作是无锁的,所以相比较ReadWriteLock的读锁,乐观读性能更好。
那你工作中一般如何使用的呢?
该示例中,若执行乐观读过程中,存在写操作,会把乐观读升级为悲观读锁。这样很好,否则就要在一个循环里反复执行乐观读,直到执行乐观读操作的期间没有写操作(这样才能保证x和y的正确性和一致性),而循环读会浪费大量CPU。升级为悲观读锁,代码简练且不易出错。
谈谈你对乐观读的理解?
很多人喜欢类比StampedLock的乐观读和数据库的乐观锁。
数据库乐观锁使用场景是这样的:一个模块,会有多个人通过前端同时修改同一条订单,那如何保证订单数据是线程安全的呢?
这就可以使用乐观锁。
乐观锁实现很简单,在订单的表 product_doc 增加一个数值类型版本号字段 version,每次更新product_doc表时,将 version 字段加1。生产订单的UI在展示的时候,需要查询数据库,此时将这个 version 字段和其他业务字段一起返回给生产订单UI。
假设用户查询的生产订单的id=777,SQL如下:
select id,… ,version
from product_doc
where id=777
用户在前端执行保存操作的时候,后台利用下面的SQL语句更新生产订单,此处我们假设该条生产订单的 version=9。
update product_doc
set version=version+1,…
where id=777 and version=9
如果这条SQL语句执行成功并且返回1,说明前端执行查询操作到执行保存操作期间,没有其他人修改过这条数据。因为如果这期间其他人修改过这条数据,那么版本号字段一定会大于9。、
数据库里的乐观锁,查询的时候需要把 version 字段查出来,更新的时候要利用 version 字段做验证。这个 version 字段就类似于StampedLock里面的stamp。
StampedLock使用时踩过什么坑吗?
读多写少场景StampedLock性能很好,可替代ReadWriteLock,但StampedLock不可重入。StampedLock的悲观读锁、写锁都不支持条件变量。
如果线程阻塞在StampedLock的readLock()或writeLock(),此时调用该阻塞线程的interrupt(),会导致CPU飙升。
例如下面的代码中,线程T1获取写锁之后将自己阻塞,线程T2尝试获取悲观读锁,也会阻塞;如果此时调用线程T2的interrupt()方法来中断线程T2的话,你会发现线程T2所在CPU会飙升到100%。
final StampedLock lock
= new StampedLock();
Thread T1 = new Thread(()->{
// 获取写锁
lock.writeLock();
// 永远阻塞在此处,不释放写锁
LockSupport.park();
});
T1.start();
// 保证T1获取写锁
Thread.sleep(100);
Thread T2 = new Thread(()->
//阻塞在悲观读锁
lock.readLock()
);
T2.start();
// 保证T2阻塞在读锁
Thread.sleep(100);
//中断线程T2
//会导致线程T2所在CPU飙升
T2.interrupt();
T2.join();
所以使用StampedLock一定不要调用中断。如果需要支持中断功能,一定使用可中断的悲观读锁readLockInterruptibly()和写锁writeLockInterruptibly()。
=================================================================
StampedLock的使用看上去有点复杂,但是如果你能理解乐观锁背后的原理,使用起来还是比较流畅的。建议你认真揣摩Java的官方示例,这个示例基本上就是一个最佳实践。我们把Java官方示例精简后,形成下面的代码模板,建议你在实际工作中尽量按照这个模板来使用StampedLock。
StampedLock读模板:
final StampedLock sl =
new StampedLock();
最后
ActiveMQ消息中间件面试专题
- 什么是ActiveMQ?
- ActiveMQ服务器宕机怎么办?
- 丢消息怎么办?
- 持久化消息非常慢怎么办?
- 消息的不均匀消费怎么办?
- 死信队列怎么办?
- ActiveMQ中的消息重发时间间隔和重发次数吗?
ActiveMQ消息中间件面试专题解析拓展:
redis面试专题及答案
- 支持一致性哈希的客户端有哪些?
- Redis与其他key-value存储有什么不同?
- Redis的内存占用情况怎么样?
- 都有哪些办法可以降低Redis的内存使用情况呢?
- 查看Redis使用情况及状态信息用什么命令?
- Redis的内存用完了会发生什么?
- Redis是单线程的,如何提高多核CPU的利用率?
Spring面试专题及答案
- 谈谈你对 Spring 的理解
- Spring 有哪些优点?
- Spring 中的设计模式
- 怎样开启注解装配以及常用注解
- 简单介绍下 Spring bean 的生命周期
Spring面试答案解析拓展
高并发多线程面试专题
- 现在有线程 T1、T2 和 T3。你如何确保 T2 线程在 T1 之后执行,并且 T3 线程在 T2 之后执行?
- Java 中新的 Lock 接口相对于同步代码块(synchronized block)有什么优势?如果让你实现一个高性能缓存,支持并发读取和单一写入,你如何保证数据完整性。
- Java 中 wait 和 sleep 方法有什么区别?
- 如何在 Java 中实现一个阻塞队列?
- 如何在 Java 中编写代码解决生产者消费者问题?
- 写一段死锁代码。你在 Java 中如何解决死锁?
高并发多线程面试解析与拓展
jvm面试专题与解析
- JVM 由哪些部分组成?
- JVM 内存划分?
- Java 的内存模型?
- 引用的分类?
- GC什么时候开始?
JVM面试专题解析与拓展!
锁代码。你在 Java 中如何解决死锁?
高并发多线程面试解析与拓展
[外链图片转存中…(img-5a2yaiGp-1714824857550)]
jvm面试专题与解析
- JVM 由哪些部分组成?
- JVM 内存划分?
- Java 的内存模型?
- 引用的分类?
- GC什么时候开始?
JVM面试专题解析与拓展!
[外链图片转存中…(img-n7d0iw84-1714824857550)]