Java并发(具体实例)—— 构建高效且可伸缩的结果缓存
这个例子来自《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并发(具体实例)—— 构建高效且可伸缩的结果缓存的更多相关文章
- 【JAVA并发编程实战】5、构建高效且可伸缩的结果缓存
首先创建一个借口,用来表示耗费资源的计算 package cn.xf.cp.ch05; public interface Computable<A, V> { V compute(A ar ...
- Java并发(三):实例引出并发应用场景
前两篇介绍了一些Java并发的基础知识,博主正巧遇到一种需求:查询数据库,根据查询结果集修改数据库记录,但整个流程是做成了一个schedule的,并且查询比较耗时,并且需要每两分钟执行一次,cpu经常 ...
- Java并发编程实例(synchronized)
此处用一个小程序来说明一下,逻辑是一个计数器(int i):主要的逻辑功能是,如果同步监视了资源i,则不输出i的值,但如果没有添加关键字synchronized,因为是两个线程并发执行,所以会输出i的 ...
- java并发编程实战学习(3)--基础构建模块
转自:java并发编程实战 5.3阻塞队列和生产者-消费者模式 BlockingQueue阻塞队列提供可阻塞的put和take方法,以及支持定时的offer和poll方法.如果队列已经满了,那么put ...
- 【Java并发.5】基础构建模块
本章会介绍一些最有用的并发构建模块,有丶东西(最后一小节,纯干货). 5.1 同步容器类 同步容器类包括 Vector 和 Hashtable ,这些类实现线程安全的方式是:将它们的状态封装起来,并对 ...
- java并发编程实战笔记---(第五章)基础构建模块
. 5.1同步容器类 1.同步容器类的问题 复合操作,加容器内置锁 2.迭代器与concurrentModificationException 迭代容器用iterator, 迭代过程中,如果有其他线程 ...
- 《java并发编程实战》读书笔记4--基础构建模块,java中的同步容器类&并发容器类&同步工具类,消费者模式
上一章说道委托是创建线程安全类的一个最有效策略,只需让现有的线程安全的类管理所有的状态即可.那么这章便说的是怎么利用java平台类库的并发基础构建模块呢? 5.1 同步容器类 包括Vector和Has ...
- Java并发编程实战 第5章 构建基础模块
同步容器类 Vector和HashTable和Collections.synchronizedXXX 都是使用监视器模式实现的. 暂且不考虑性能问题,使用同步容器类要注意: 只能保证单个操作的同步. ...
- 《Java并发编程实战》读书笔记-第5章 基础构建模块
同步容器类 同步容器类实现线程安全的方式:将所有状态封装起来,对每个公有方法使用同步,使得每一次只有一个线程可以访问.同步容器类包含:Vector.Hashtable.Collections.sync ...
随机推荐
- win cmd执行Python脚本提示找不到模块问题
Windows关于命令行执行Python脚本,提示找不到模块的问题,我 本人也是在pycharm上运行没毛病的,后来在本地搞了个Jenkins做定时任务,谁知道就提示找不到模块 也百度了很多,都是说什 ...
- js arguments参数
在调用函数时,浏览器每次都会传递进两个隐含的参数: 1.函数的上下文对象 this 2.封装实参的对象 arguments - arguments是一个类数组对象, ...
- Android之SAX解析笔记
books.xml: <?xml version="1.0" encoding="utf-8"?> <books> <book i ...
- Anaconda概念和使用方法
Anaconda概述 Anaconda是一个用于科学计算的Python发行版,支持 Linux, Mac, Windows系统,提供了包管理与环境管理的功能,可以很方便地解决多版本python并存.切 ...
- 在成为测试大牛的路上,我推荐BestTest
BestTest-Python自动化测试9月份班开始招生啦! 网络+现场同步进行,课程新升级,web自动化+接口自动化双管齐下,一线互联网测试开发工程师带你在自动化的世界里自由翱翔! 推荐优惠多多,欢 ...
- Java 设计模式之 策略模式
思维导图: 我们先来看 STRATEGY 设计模式的通俗解释: 跟不同类型的MM约会,要用不同的策略,有的请电影比较好,有的则去吃小吃效果不错,有的去海边浪漫最合适,但目的都是为了得到 MM 的芳心, ...
- 我的Podfile如下
# Uncomment this line to define a global platform for your projectuse_frameworks!(http://www.my516.c ...
- (转)pycharm autopep8配置
转:https://blog.csdn.net/BobYuan888/article/details/81943808 1.pip下载安装: 在命令行下输入以下命令安装autopep8 pip ins ...
- How-To-Ask-Questions-The-Smart-Way提问的技巧 提问的智慧
How-To-Ask-Questions-The-Smart-Way https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/b ...
- 微信小程序的开发框架
wxss:是一套样式语言,用来描述wxml的组件样式:小程序在css基础上做的修改和扩充的版本 css:是一套样式语言,样式表,用来描述xml和html文件样式的呈现: 设备像素:是图片在设备上显示的 ...