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 ...
随机推荐
- kafka docker-composer.yml
使用Docker快速搭建Kafka开发环境 表现力 关注 0.5 2018.05.04 03:00* 字数 740 阅读 25240评论 1喜欢 11 Docker在很多时候都可以帮助我们快速搭建想 ...
- hdu 6044 : Limited Permutation (2017 多校第一场 1012) 【输入挂 组合数学】
题目链接 参考博客: http://blog.csdn.net/jinglinxiao/article/details/76165353 http://blog.csdn.net/qq_3175920 ...
- springboot+thymeleaf 访问静态资源解决(static)
---------------------------------------2.1.3-------------------------------------------------------- ...
- win8安装maven
1.下载并解压maven F:\maven\apache-maven-3.5.2 2. 设置环境变量 3. Path路径中添加maven的可执行文件目录(bin目录) 4.验证maven是否安装成功: ...
- sql2000如何完美压缩.mdf文件
--收缩数据库 dbcc shrinkdatabase('test_db') , ) --数据文件mdf , ) --日志文件ldf ) --更新
- 用ASP实现文件下载
<% '**************************************************************'**使用方法: ...
- 123、TensorFlow的Job
# 如果你在分布式环境中部署TensorFlow # 你或许需要指定job name和task ID # 来将变量放置在参数服务器上 # 将操作放在worker job import tensorfl ...
- JavaScript浅析
目录 JacaScript概述 ECMAScript和JavaScript的关系 ECMAScript的历史: JavaScript的引入方式: 引入额外的JS文件: JavaScript的语言规范: ...
- Jquery append() 添加多次同个元素时,只有一次作用,如何解决?
这是一个简单的table <table id="mytable"> <!-- 这里将要动态加载tr数据 --> </table> 这是一个模版t ...
- centos6.5下apollo1.7.1的搭建
前言:apollo MQ作为消息队列中间件,在需要消息列表的应用程序环境中,需要使用该服务器中间件 1.准备工作 2.搭建 3.测试 1.准备工作 第一步:linux系统中配置好java环境 A.卸载 ...