一个线程在 sleep,另外两个线程执行到了 FutureTask 的 get 方法。
sleep 的好理解,为什么另外两个线程阻塞在 get 方法上呢?
很简单,因为另外两个线程返回的 future 不是 null,这是由 putIfAbsent 方法的特性决定的:
好了,书中给出的最终方案的代码也解释完了。
但是书里面还留下了两个“坑”:
一个是不支持缓存过期机制。
一个是不支持缓存淘汰机制。
等下再说,先说说我的另一个方案。
还有一个方案
其实我也还有一个方案,拿出来给大家看看:
public class ScoreQueryService2 { public static final Map<String, Future<Integer>> SCORE_CACHE = new ConcurrentHashMap<>(); public Integer query(String userName) throws Exception { while (true) { Future<Integer> future = SCORE_CACHE.get(userName); if (future == null) { Callable<Integer> callable = () -> loadFormDB(userName); FutureTask futureTask = new FutureTask<>(callable); FutureTask<Integer> integerFuture = (FutureTask) SCORE_CACHE.computeIfAbsent(userName, key -> futureTask); future = integerFuture; integerFuture.run(); } try { return future.get(); } catch (CancellationException e) { SCORE_CACHE.remove(userName, future); } catch (Exception e) { throw e; } } } private Integer loadFormDB(String userName) throws InterruptedException { System.out.println("开始查询userName=" + userName + "的分数"); //模拟耗时 TimeUnit.SECONDS.sleep(1); return ThreadLocalRandom.current().nextInt(380, 420); } }
和书中给出的方案差异点在于用 computeIfAbsent 代替了 putIfAbsent:
V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
computeIfAbsent,首先它也是一个线程安全的方法,这个方法会检查 Map 中的 Key,如果发现 Key 不存在或者对应的值是 null,则调用 Function 来产生一个值,然后将其放入 Map,最后返回这个值;否则的话返回 Map 已经存在的值。
putIfAbsent,如果 Key 不存在或者对应的值是 null,则将 Value 设置进去,然后返回 null;否则只返回 Map 当中对应的值,而不做其他操作。
所以这二者的区别之一在于返回值上。
用了 computeIfAbsent 之后,每次返回的都是同一个 FutureTask,但是由于 FutureTask 的生命周期,或者说是状态扭转的存在,即使三个线程都调用了它的 run 方法,这个 FutureTask 也只会执行成功一次。
可以看一下,这个 run 方法的源码,一进来就是状态和当前操作线程的判断:
但是从程序实现的优雅角度来说,还是 putIfAbsent 方法更好。
坑怎么办?
前面不是说最终的方案有两个坑嘛:
- 一个是不支持缓存过期机制。
- 一个是不支持缓存淘汰机制。
在使用 ConcurrentHashMap 的前提下,这两个特性如果要支持的话,需要进行对应的开发,比如引入定时任务来解决,想想就觉得麻烦。
同时也我想到了 spring-cache,我知道这里面有 ConcurrentHashMap 作为缓存的实现方案。
我想看看这个组件里面是怎么解决这两个问题的。
二话不说,我先把代码拉下来看看:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency>
由于 spring-cache 也不是本文重点,所以我就直接说关键地方的源码了。
至于是怎么找到这里来的,就不详细介绍了,以后安排文章详细解释。
另外我不得不说一句:spring-cache 这玩意真的是优雅的一比,不论是源码还是设计模式的应用,都非常的好。
首先,我们可以看到 @Cacheable 注解里面有一个参数叫做 sycn,默认值是 false:
关于这个参数,官网上的解释是这样的:
就是针对我们前面提到的缓存如何维护的情况的一个处理方案。使用方法也很简单。
该功能对应的核心部分的源码在这个位置:
org.springframework.cache.interceptor.CacheAspectSupport#execute(org.springframework.cache.interceptor.CacheOperationInvoker, java.lang.reflect.Method, org.springframework.cache.interceptor.CacheAspectSupport.CacheOperationContexts)
在上面这个方法中会判断是不是 sync=true 的方法,如果是则进入到 if 分支里面。
接着会执行到下面这个重要的方法:
org.springframework.cache.interceptor.CacheAspectSupport#handleSynchronizedGet
而我关心的是 ConcurrentMapCache 实现,点进去一看,好家伙,这方法我熟啊:
org.springframework.cache.concurrent.ConcurrentMapCache#get
computeIfAbsent 方法,我们不是刚刚才说了嘛。但是我左翻右翻就是找不到设置过期时间和淘汰策略的地方。
于是,我又去翻官网了,发现答案就直接写在官网上的:
这里说了,官方提供的是一个缓存的抽象,而不是具体的实现。而缓存过期和淘汰机制不属于抽象的范围内。
为什么呢?
比如拿 ConcurrentHashMap 来说,假设我提供了缓存过期和淘汰机制的抽象,那你说 ConcurrentHashMap 怎么去实现这个抽象方法?
实现不了,因为它本来就不支持这个机制。
所以官方认为这样的功能应该由具体的缓存实现类去实现而不是提供抽象方法。
这里也就回复了前面的最终方案引申出的这两个问题:
- 一个是不支持缓存过期机制。
- 一个是不支持缓存淘汰机制。
別问,问就是原生的方法里面是支持不了的。如果要实现自己去写代码,或者换一个缓存方案。
再说两个点
最后,再补充两个点。
第一个点是之前的《当Synchronized遇到这玩意儿,有个大坑,要注意!》这篇文章里面,有一个地方写错了。
框起来的地方是我后面加上来的。
上周的文章发出去后,大概有十来个读者给我反馈这个问题。
我真的特别的开心,因为真的有人把我的示例代码拿去跑了,且认真思考了,然后来和我讨论,帮我指正我写的不对的地方。
再给大家分享一下我的这篇文章《当我看技术文章的时候,我在想什么?》
里面表达了我对于看技术博客的态度:
看技术文章的时候多想一步,有时候会有更加深刻的理解。
带着怀疑的眼光去看博客,带着求证的想法去证伪。
多想想 why,总是会有收获的。
第二个点是这样的。
关于 ConcurrentHashMap 的 computeIfAbsent 我其实也专门写过文章的:《震惊!ConcurrentHashMap里面也有死循环,作者留下的“彩蛋”了解一下?》
老读者应该是读到过这篇文章的。
之前在 seata 官网上闲逛的时候,看到了这篇博客:
名字叫做《ConcurrentHashMap导致的Seata死锁问题》,我就随便这么点进去一看:
这里提到的这篇文章,就是我写的。
在 seata 官网上偶遇自己的文章是一种很神奇的体验。
四舍五入,我也算是给 seata 有过贡献的男人。
而且你看这篇文章其实也提到了我之前写过的很多文章,这些知识都通过一个小小的点串起来了,由点到线,由线到面,这也是我坚持写作的原因。
共勉之。
最后,呼应一下文章的开头部分,考研马上要查分了,我知道我的读者里面还是有不少是今年考研的。
如果你看到了这里,那么下面这个图送给你:
本文已收录至个人博客,更多原创好文,欢迎大家来玩: