这个例子来自《Java并发编程实战》第五章。本文将开发一个高效且可伸缩的缓存,文章首先从最简单的HashMap开始构建,然后分析它的并发缺陷,并一步一步修复。

hashMap版本                                                                                              

首先我们定义一个Computable接口,该接口包含一个compute()方法,该方法是一个耗时很久的数值计算方法。Memoizer1是第一个版本的缓存,该版本使用hashMap来保存之前计算的结果,compute方法将首先检查需要的结果是否已经在缓存中,如果存在则返回之前计算的值,否则重新计算并把结果缓存在HashMap中,然后再返回。

interface Computable<A, V> {
V compute(A arg) throws InterruptedException;//耗时计算
} public class Memoizer1<A, V> implements Computable<A, V> {
private final Map<A, V> cache = new HashMap<A, V>();
private final Computable<A, V> c; public Memoizer1(Computable<A, V> c) {
this.c = c;
} public synchronized V compute(A arg) throws InterruptedException {
V result = cache.get(arg);
if (result == null) {
result = c.compute(arg);
cache.put(arg, result);
}
return result;
}
}

  HashMap不是线程安全的,因此要确保两个线程不会同时访问HashMap,Memoizer1采用了一种保守的方法,即对整个方法进行同步。这种方法能确保线程安全性,但会带来一个明显的可伸缩问题:每次只有一个线程可以执行compute。

ConcurrentHashMap版本                                                                        

我们可以用ConcurrentHashMap代替HashMap来改进Memoizer1中糟糕的并发行为,由于ConcurrentHashMap是线程安全的,因此在访问底层Map时就不需要进行同步,因此避免了对compute()方法进行同步带来的串行性:

public class Memoizer2 <A, V> implements Computable<A, V> {
private final Map<A, V> cache = new ConcurrentHashMap<A, V>();
private final Computable<A, V> c; public Memoizer2(Computable<A, V> c) {
this.c = c;
} public V compute(A arg) throws InterruptedException {
V result = cache.get(arg);
if (result == null) {
result = c.compute(arg);
cache.put(arg, result);
}
return result;
}
}

  但是这个版本的缓存还是有问题的,如果线程A启动了一个开销很大的计算,而其他线程并不知道这个线程正在进行,那么很可能会重复这个计算。

FutureTask版本1                                                                                      

我们可以在map中存放Future对象而不是最终计算结果,Future对象相当于一个占位符,它告诉用户,结果正在计算中,如果想得到最终结果,请调用get()方法。Future的get()方法是一个阻塞方法,如果结果正在计算中,那么它会一直阻塞到结果计算完毕,然后返回;如果结果已经计算完毕,那么就直接返回。

public class Memoizer3<A, V> implements Computable<A, V> {
private final Map<A, Future<V>> cache = new ConcurrentHashMap<A, Future<V>>();
private final Computable<A, V> c; public Memoizer3(Computable<A, V> c) {
this.c = c;
} public V compute(final A arg) throws InterruptedException {
Future<V> f = cache.get(arg);
if (f == null) {
Callable<V> eval = new Callable<V>() {
public V call() throws InterruptedException {
return c.compute(arg);
}
};
FutureTask<V> ft = new FutureTask<V>(eval);
f = ft;
cache.put(arg, ft);
ft.run(); // call to c.compute happens here
}
try {
return f.get();
} catch (ExecutionException e) {
cache.remove(arg);
}
return null;
}
}

  Memoizer3解决了上一个版本的问题,如果有其他线程在计算结果,那么新到的线程会一直等待这个结果被计算出来,但是他又一个缺陷,那就是仍然存在两个线程计算出相同值的漏洞。这是一个典型的"先检查再执行"引起的竞态条件错误,我们先检查map中是否存在结果,如果不存在,那就计算新值,这并不是一个原子操作,所以两个线程仍有可能在同一时间内调用compute来计算相同的值。

FutureTask版本2                                                                                      

Memoizer3存在这个问题的原因是,复合操作"若没有则添加"不具有原子性,我们可以改用ConcurrentMap中的原子方法putIfAbsent,避免了Memoizer3的漏洞。

public class Memoizer <A, V> implements Computable<A, V> {
private final ConcurrentMap<A, Future<V>> cache
= new ConcurrentHashMap<A, Future<V>>();
private final Computable<A, V> c; public Memoizer(Computable<A, V> c) {
this.c = c;
} public V compute(final A arg) throws InterruptedException {
while (true) {
Future<V> f = cache.get(arg);
if (f == null) {
Callable<V> eval = new Callable<V>() {
public V call() throws InterruptedException {
return c.compute(arg);
}
};
FutureTask<V> ft = new FutureTask<V>(eval);
f = cache.putIfAbsent(arg, ft);
if (f == null) {
f = ft;
ft.run();
}
}
try {
return f.get();
} catch (CancellationException e) {
cache.remove(arg, f);
} catch (ExecutionException e) {
throw LaunderThrowable.launderThrowable(e.getCause());
}
}
}
}

  当缓存对象是Future而不是值的时候,将导致缓存污染问题,因为某个计算可能被取消或失败,当出现这种情况时,我们应该把对象从map中移除,然后再重新计算一遍。这个缓存系统的使用十分简单,只需传入一个Computable对象即可构造缓存,要得到计算结果,调用compute()方法即可,该方法会把计算过的结果缓存起来。

总结                                                                                                           

相较于第一个版本,最终版本在性能上有了很大提升,ConcurrentHashMap的使用避免了整个方法加锁;FutureTask的使用使计算异步化,同时通过一个Future对象告诉当前线程计算正在进行中,避免了大量重复计算。

Java并发(具体实例)—— 构建高效且可伸缩的结果缓存的更多相关文章

  1. 【JAVA并发编程实战】5、构建高效且可伸缩的结果缓存

    首先创建一个借口,用来表示耗费资源的计算 package cn.xf.cp.ch05; public interface Computable<A, V> { V compute(A ar ...

  2. Java并发(三):实例引出并发应用场景

    前两篇介绍了一些Java并发的基础知识,博主正巧遇到一种需求:查询数据库,根据查询结果集修改数据库记录,但整个流程是做成了一个schedule的,并且查询比较耗时,并且需要每两分钟执行一次,cpu经常 ...

  3. Java并发编程实例(synchronized)

    此处用一个小程序来说明一下,逻辑是一个计数器(int i):主要的逻辑功能是,如果同步监视了资源i,则不输出i的值,但如果没有添加关键字synchronized,因为是两个线程并发执行,所以会输出i的 ...

  4. java并发编程实战学习(3)--基础构建模块

    转自:java并发编程实战 5.3阻塞队列和生产者-消费者模式 BlockingQueue阻塞队列提供可阻塞的put和take方法,以及支持定时的offer和poll方法.如果队列已经满了,那么put ...

  5. 【Java并发.5】基础构建模块

    本章会介绍一些最有用的并发构建模块,有丶东西(最后一小节,纯干货). 5.1 同步容器类 同步容器类包括 Vector 和 Hashtable ,这些类实现线程安全的方式是:将它们的状态封装起来,并对 ...

  6. java并发编程实战笔记---(第五章)基础构建模块

    . 5.1同步容器类 1.同步容器类的问题 复合操作,加容器内置锁 2.迭代器与concurrentModificationException 迭代容器用iterator, 迭代过程中,如果有其他线程 ...

  7. 《java并发编程实战》读书笔记4--基础构建模块,java中的同步容器类&并发容器类&同步工具类,消费者模式

    上一章说道委托是创建线程安全类的一个最有效策略,只需让现有的线程安全的类管理所有的状态即可.那么这章便说的是怎么利用java平台类库的并发基础构建模块呢? 5.1 同步容器类 包括Vector和Has ...

  8. Java并发编程实战 第5章 构建基础模块

    同步容器类 Vector和HashTable和Collections.synchronizedXXX 都是使用监视器模式实现的. 暂且不考虑性能问题,使用同步容器类要注意: 只能保证单个操作的同步. ...

  9. 《Java并发编程实战》读书笔记-第5章 基础构建模块

    同步容器类 同步容器类实现线程安全的方式:将所有状态封装起来,对每个公有方法使用同步,使得每一次只有一个线程可以访问.同步容器类包含:Vector.Hashtable.Collections.sync ...

随机推荐

  1. IndexError: list index out of range的错误原因

    第1种可能情况list[index]index超出范围 第2种可能情况list是一个空的 没有一个元素进行list[0]就会出现该错误 ————————————————版权声明:本文为CSDN博主「m ...

  2. 微信公众号开发(二)获取access_token

    参考:https://www.cnblogs.com/liuhongfeng/p/4848851.html 一:介绍. 接口调用请求说明 http请求方式: GET https://api.weixi ...

  3. Windows无法启动MapGIS DataStorage Service服务

    但是启动又启动不了,查看属性 发现计算机服务器确实少了该文件目录.. 可能是不小心删除了? 之前确实有删过一些文件 下次直接把.net禁止就可以了,不用删除,不然不小心删除了其它服务.. 参考文献:h ...

  4. CTF_知识点网址收藏

    杂项 图片 python lsb.py extract big.png-stego.png3.txt详解 : https://www.4hou.com/technology/2515.html  :h ...

  5. 配置自己的CocoaPods库

    序 默认安装的cocoapods确实很好用,可是毕竟自己会写一些库和修改一些第三方库来用.所幸cocoapods确实是一个神器.他可以定义自己的库来用. 如何安装Cocoapods,请参考这篇 从头来 ...

  6. Linux C编程

    Linux C网络编程 1.Linux套接字 1.1 套接字介绍 套接字(Sockets),即为网络进程ID,是由运行这个进程的计算机的IP地址和这个进程使用的端口(Port)组成. 可以只用'net ...

  7. http://research.google.com/archive/mapreduce.html

    http://research.google.com/archive/mapreduce.html

  8. 利用 Skywalking 搭建 APM(应用性能管理)— 安装与配置

    1.什么是 Skywalking Skywalking 是一个APM系统,即应用性能监控系统,为微服务架构和云原生架构系统设计.它通过探针自动收集所需的指标,并进行分布式追踪.通过这些调用链路以及指标 ...

  9. AUC

    https://www.cnblogs.com/earendil/p/9400275.html

  10. (转载)如何在 Github 上发现优秀的开源项目?

    转载自:传送门 之前发过一系列有关 GitHub 的文章,有同学问了,GitHub 我大概了解了,Git 也差不多会使用了,但是还是搞不清 GitHub 如何帮助我的工作,怎么提升我的工作效率? 问到 ...