Guava工程包含了若干被Google的Java项目广泛依赖的核心库,例如:集合[collections]、缓存[caching]等,在其他项目中也广泛使用,本文讨论一下cache使用过程中与读写锁结合的过程中产生的一个坑。
背景
先简化一下场景,我们的场景中可能有多个线程对cache进行读写操作,并且在对cache进行读写操作的时候,集合业务逻辑,需要对业务部分代码加读写锁,加读写锁是为了让并发更大,如果都加写锁,则并发较低。简单而言:
- 当读取cache中的数据的时候,需要加读锁;
- 当cache中的数据被写入或是移除的时候,需要加写锁。
坑就在于,当我们加了读锁,对cache进行读取操作的时候,容易卡死,这是为什么呢?请看如下代码(具体代码都已经上传到github)
答案是因为cache.invalidate()操作会触发cache执行remove()函数,而这个函数我们设置了回调,即MyRemovalListener,而在此回调中,又去拿写锁,所以会卡死。cache.getIfPresent()操作有可能触发cache执行remove()函数。
方案
知道了这个问题的原因之后,解决这个问题就很简单了:
方法1:把移除或是获取cache中某些操作在读锁之外,这样当cache remove去拿写锁的时候,也是ok的。
方法2:设置异步RemovalListener,可以点击此处参考源码,但是这样操作也需要注意的就是,由于是异步remove cache中某些元素的,可能造成业务逻辑不准确,所以需要业务逻辑对此做一些处理。
引申
其实ReentrantReadWriteLock也有一些坑,即当有读锁也有写锁的时候,如果一些线程获取了读锁,然后一个写线程获取写锁,就会等待,这是理所当然的。但是当此之后,有其他线程想继续获取读锁的时候,也会卡死。这就是ReentrantReadWriteLock做的一些策略,为了防止写锁饿死而做的优化。详情请看
一个有趣的Java多线程”bug”: ReadWriteLock引发的”假死”。请点击此处参考具体代码
1 | public class LockHang { |