Guava cache同步Remove与读写锁集合的坑

Guava工程包含了若干被Google的Java项目广泛依赖的核心库,例如:集合[collections]、缓存[caching]等,在其他项目中也广泛使用,本文讨论一下cache使用过程中与读写锁结合的过程中产生的一个坑。

背景

先简化一下场景,我们的场景中可能有多个线程对cache进行读写操作,并且在对cache进行读写操作的时候,集合业务逻辑,需要对业务部分代码加读写锁,加读写锁是为了让并发更大,如果都加写锁,则并发较低。简单而言:

  1. 当读取cache中的数据的时候,需要加读锁;
  2. 当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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
public class LockHang {

private static ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private static int cacheMaxSize = 10;

/**
* 创建一个带有RemovalListener监听的缓存
*/
private static Cache<Integer, Integer> cache;

public static void main(String[] args) {
cacheInit();
putCache();
readWork();
System.exit(0);
}

private static void cacheInit() {
cache = CacheBuilder.newBuilder()
.removalListener(new MyRemovalListener()).maximumSize(cacheMaxSize).build();
}

private static void putCache() {
for (int i = 0; i < cacheMaxSize; i++) {
cache.put(i, i);
}
}

private static void readWork() {
// get read lock
lock.readLock().lock();
System.out.println("Thread={}" + Thread.currentThread() + " get read lock");
try {
// do something
cache.invalidate(1);
} finally {
lock.readLock().unlock();
System.out.println("Thread={}" + Thread.currentThread() + " unlock the read lock");
}
}


/**
* 创建一个监听器,当cache被remove的时候,需要加写锁做是一些事情。
*/
private static class MyRemovalListener implements RemovalListener<Integer, Integer> {
@Override
public void onRemoval(RemovalNotification<Integer, Integer> notification) {
System.out.println("Thread={}" + Thread.currentThread() + " try to get write lock");
lock.writeLock().lock();
System.out.println("Thread={}" + Thread.currentThread() + " get write lock");
try {
String tips = String
.format("key=%s,value=%s,reason=%s was removed", notification.getKey(),
notification.getValue(), notification.getCause());
System.out.println(tips);
} finally {
lock.writeLock().unlock();
System.out.println("Thread={}" + Thread.currentThread() + " unlock the write lock");
}

}
}

}