上次的推送,我们讲到了一种提供并发协作的Map缓存,代码如下:
我们说,以上的实现是几乎完美的:它展现了非常好的并发性,能很快地返回已经缓存的结果,如果新到的线程请求的是其他线程正在查询的结果,它会耐心等待。
那么,“几乎”指的是什么问题呢?
锵锵
“查询”方法中的if代码块是非原子的,因此就存在着:两个线程几乎在同一时间调用本方法时,双方都没在缓存中取得期望的值,两者又同时开始了数据库的访问工作。如下图所示:
上图中,偶发的时序导致该方法进行了两次的查询操作。那我们要怎么解决这个问题呢?
锵锵
ConcurrentHashMap类的
putIfAbsent方法!
putIfAbsent(K, V)是一个原子方法,如果Map中不含有K则将K-V加入Map并返回null,否则返回Map中已存在的K对应的V。
新的代码如下:
一切看起来如此美妙!
突然有一天,DBMS这个重量级软件突然崩溃了,所有不在缓存中的新题目都要去这个不可能访问到的地方取数据。
运维团队很给力,只用了10分钟就解决了数据库的问题,可是随之而来的却是奇怪的Java异常……
原来,在数据库崩溃的10分钟内,评判系统需要找到试卷的第2题。可是,ProblemDaoImpl费了九牛二虎之力也没能成功取到数据库的数据,评判时,每传入“2”并调用“查询”方法时,该方法就会抛出一个异常,哪怕是修复了数据库以后……
也就是说,缓存Future对象带来了
缓存污染(cache pollution)
的新问题!
这里的缓存污染指的是,如果一个操作被取消或者它本身失败了,未来尝试对它调用get()时,其永远表现为取消或者失败。
为了解决避免这个问题,我们应该实现这样的功能:
- 每当操作被发现取消时,方法应该负责把Future从缓存中移除;
- 同时,产生RuntimeException时也应如此,这样新请求中的操作才有可能成功。
修改后的代码如下:
经过以上的修改,
我们实现了一个
足够健壮的ProblemDaoImpl。
本文中心思想来自于
《Java并发编程实践(Java Concurrency in Practice)》
附:本文的最终目的是引入备忘录技术(memoization)。
备忘录是一种用于缓存计算结果的常见技术,它常常被用于提升递归效率等场景。
下面是在Java中实现的Memoizer。