前些时候写了一个简单的cache类,实现了一些基本的功能:get和update操作。由于使用在多线程的环境中,为了提高效率,我采用了ReadWriteLock,毕竟我这里缓存更新少,取数据多。特别是一些timeout为-1的数据(持久数据),添加进去之后基本不会被更新。这种场景用读写锁是自然而然的事情(由于时间比较紧,也没去花时间了解有没有什么更适合的锁)。
缓存实现中
public V get(K key){
readLock.lock();
try{
CacheObject value = map.get(key);
if(!value.enable()){
remove(key);
return null;
} else {
return value.getObject();
}
} catch(Exception e){return null;}
finally {
readLock.unlok();
}
}
private boolean remove(K key){
writeLock.lock();
try{
map.remove(key);
return true;
} catch(Exception e){return false;}
finally{
writeLock.unlock();
}
}
大概的get逻辑就是这个了。开始我也没有在意这个同步的问题,写完了看了两眼觉得应该没什么问题了。然后今天在测试功能的时候,觉得有点奇怪。在取监控数据的时候,怎么没有提示呢?每次服务器重启完第一次是可以的,有时候可能正常一次,可能正常两次或者三次,然后就开始卡死再也不出数据。每次遇到这种奇怪的时候我的神经就莫名奇妙的兴奋。
开始我怀疑是去远程调用其他服务的时候出问题,网络有问题,因为我发现我点击没有配置不用去远程取数据的那些数据都是正常的,当然只能是猜测,线上的服务器连不上去。我只能去线上抓数据,拿下来本地调试。发现不是远程取数据的问题,却在取缓存数据的时候停住了。我看了好几遍这个get方法,看来看去也没问题啊。看自己的代码很难能看出什么bug,所以说单元测试不能自己写完代码之后再写单元测试呢。写个程序来测试这个缓存的get操作吧。
很简单的new一个cache对象,然后每隔1秒钟调用一次get方法。哎,还真的三次之后就卡住了,这下确定了错误的原因,再继续跟第三次的get,到remove的时候卡住了,我猛然就明白了,靠,死锁了。
我设置的缓存的超时时间是3秒钟,然后第三次去取的时候已经超时,需要remove旧数据,在get操作的时候当前线程已经获取了读锁,然后remove的时候又尝试去获取写锁,读锁和写锁冲突了,然后就你等我我等你,就死了。
后来看了下ReentrantReadWriteLock的javaodoc,发现Doug Lea大师已经知道我们这种小搓搓写cache的时候肯定要用到这个,还特意写了个例子:
class CachedData {
Object data;
volatile boolean cacheValid;
ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) {
// Must release read lock before acquiring write lock
rwl.readLock().unlock();
rwl.writeLock().lock();
// Recheck state because another thread might have acquired
// write lock and changed state before we did.
if (!cacheValid) {
data = ...
cacheValid = true;
}
// Downgrade by acquiring read lock before releasing write lock
rwl.readLock().lock();
rwl.writeLock().unlock(); // Unlock write, still hold read
}
use(data);
rwl.readLock().unlock();
}
}
大师就是不一样啊,提前知道我们在想什么...
我在仔细看了下Doug Lea大师怎么好像写错了,他加锁的顺序:
rwl.readLock().unlock();
rwl.writeLock().lock();
rwl.readLock().lock();
rwl.writeLock().unlock();
rwl.readLock().unlock();
先获取了写锁,还没释放就开始拿读锁,这不也是死锁吗?动手就知道了,写了个测试代码,咦,怎么没有死锁。继续看javadoc,找到了不死锁的原因,原来ReentrantReadWriteLock有锁降级机制,写锁是exclusive锁,读锁是share锁,如果一个线程拿到了写锁,那么它还可以继续拿到读锁,降级是我自己说的,可能不准确,应该能明白我的意思。
后来想想还是不对,为了要有这么个机制,这样有什么好处吗?让我开始还理解错了,跟我师兄争论了好久。晚上洗澡的时候突然想到:这个降级的机制可能保证缓存中get操作的场景的原子性。
怎么说?这个说起来有点复杂,挑最简单的被动失效缓存举例子。我想get操作的时候,发现数据已经失效,需要用新数据覆盖失效数据,并返回最新结果。这个场景用锁降级可以保证。当然用synchronized可以实现,只是那样的话就用不到读写锁的读读不互斥的优点了(转入正题)。
用大师的这个加锁顺序:
rwl.writeLock().lock();
rwl.readLock().lock();
rwl.writeLock().unlock();
rwl.readLock().unlock();
看出来了没?如果一个线程拿到了写锁,即使更新缓存的动作做完了,再拿到读锁,准备返回数据。这样可以保证返回的数据是这次更新的数据。如果没有这个降级机制的话,在释放了读锁之后,可能会被另外一个线程抢到读锁,继续更新缓存,再返回的数据就不是第一次更新的数据了。
或许这个机制是专门为了缓存设计的?其实我想可能还有别的原因,场景可能还没遇到,后面遇到可能就明白了。
看完这篇文章,写了两个问题:
(1)使用ReadWriteLock时,一种产生死锁的动作。
(2)ReentrantReadWriteLock的降级机制,加锁顺序有嚼头。这个还没理解透,大家有什么好的例子或者心得?
分享到:
相关推荐
读写锁ReentrantReadWriteLock&StampLock详解_e读写锁ReentrantReadWriteLock&StampLock详解_e读写锁ReentrantReadWriteLock&StampLock详解_e读写锁ReentrantReadWriteLock&StampLock详解_e读写锁...
主要介绍了Java多线程 ReentrantReadWriteLock原理及实例详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
针对这种场景,JAVA的并发包提供了读写锁ReentrantReadWriteLock,它表示两个锁,一个是读操作相关的锁,称为共享锁;一个是写相关的锁,称为排他锁 类图如下: 说明:如上图所示Sync为ReentrantReadWriteLock...
在学习Java过程中,自己收集了很多的Java的学习资料,分享给大家,有需要的欢迎下载,希望对大家有用,一起学习,一起进步。
下面小编就为大家带来一篇ReadWriteLock接口及其实现ReentrantReadWriteLock方法。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
6.5 深入理解 AQS之 ReentrantReadWritelock 实战副本.mp4
6.5 深入理解 AQS之 ReentrantReadWritelock 实战副本副本.mp4
ReentrantReadWriteLock 读写锁除了保证写操作对读操作可见性以及并发行提升外,简化了读写交互场景开发
6.JUC并发工具类在大厂的应用场景详解 (1).pdf ...8、读写锁ReentrantReadWriteLock&StampLock详解.pdf 9、并发容器 (Map、List、Set) 实战及其原理.pdf 10、阻塞队列BlockingQueue 实战及其原理分析.pdf
Java 多线程与并发(12_26)-JUC锁_ ReentrantReadWriteLock详解
读写锁是将被锁保护的临界资源的读操作和写操作分开,允许同时有多个线程同时对临界资源进行读操作,任意时刻只允许一个线程对资源进行写操作。简单的说,对与读操作采用的是 共享锁 ,对于写操作采用的是 排他锁...
8. Lock接口 (ReentrantLock 可重入锁) 特性 ReentantLock 继承接口 Lock 并实现了接口中定义的方法, 它是一种可重入锁, 除了能完成 synchronized 所能完成的所有工作外,还提供了诸如可响应中断锁、可轮询锁...
ReadWriteLock的使用,实际上由于ReadWriteLock是一个接口,所以实际使用的是ReentrantReadWriteLock子类。同时ReadWriteLock的使用其实也是比较简单的,就是读写的锁的使用以及注意事项而已。
争用分析 ReentrantLock 和 ReentrantReadWriteLock 上的配置文件争用
多个线程操作共享对象导致的状态不一致问题 原因 共享资源的竞态条件问题 问题 竞态条件 指令重排序 工作内存中主内存同步延迟 解决 要有安全策略文档或注释 不共享 线程封闭 仅在单线程内访问数据 栈...
本篇文章主要介绍了Java concurrency之共享锁和ReentrantReadWriteLock,非常具有实用价值,需要的朋友可以参考下
从线程的优先级看饥饿问题.mp4 从Java字节码的角度看线程安全性问题.mp4 synchronized保证线程安全的原理(理论层面).mp4 synchronized保证线程安全的原理(jvm层面).mp4 单例问题与线程安全性深入解析.mp4 理解...
11.深入理解读写锁ReentrantReadWriteLock 12.详解Condition的await和signal等待通知机制 13.LockSupport工具 14.并发容器之ConcurrentHashMap(JDK 1.8版本) 15.并发容器之ConcurrentLinkedQueue 16.并发容器之...
ReentrantLock//互斥锁 class CachedData { Object data; volatile boolean cacheValid; ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
第3节解析多线程与多进程的联系以及上下文切换所导致资源浪费问题 [免费观看] 00:13:03分钟 | 第4节学习并发的四个阶段并推荐学习并发的资料 [免费观看] 00:09:13分钟 | 第5节线程的状态以及各状态之间的转换详解...