这个例子来自《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. 洛谷 P1407 稳定婚姻

    问题描述 我国的离婚率连续7年上升,今年的头两季,平均每天有近5000对夫妇离婚,大城市的离婚率上升最快,有研究婚姻问题的专家认为,是与简化离婚手续有关.25岁的姗姗和男友谈恋爱半年就结婚,结婚不到两 ...

  2. dict以及defaultdict的简单使用

    先看一个需求 from collections import defaultdict """ 需求: 统计user_list中字母出现的次数 ""&q ...

  3. c#代码规则,C#程序中元素的命名规范

    俩种命名方法 1.Pascal 命名法,第一个字母大写其它字母小写Userid 2.Camel命名法,所有单第一方写大写,其它小写,骆峰命名法,userId C#程序中元素的命名规范项目名:公司名.项 ...

  4. Newtonsoft.Json源码的solution打开之后,无法加载project

    无法加载项目 https://github.com/JamesNK/Newtonsoft.Json C:\repository\GitHub\Other\Newtonsoft.Json\Src\New ...

  5. Windows建立目录软连接

    创建:mklink /j "I:\dst" "I:\src" 删除: rmdir "I:\dst"

  6. php红包功能

    最近公司要开发 广告红包功能,这是写好的代码先放到这 https://files.cnblogs.com/files/jxkshu/PHP%E5%B9%BF%E5%91%8A%E7%BA%A2%E5% ...

  7. node中console自定义样式

    最近公司的项目一直使用gulpfile打包,项目会有三种项目打包(生产环境)和监听(开发环境)两种过程,同时需要清除文件夹,希望打包时增加提示以便区分,暂时分为上述三种提示打包.监听.清除. 先上co ...

  8. 理解JavaScript中的回调函数

    理解回调函数,首先要知道在JavaScript中,函数也是对象,它可以赋值给变量,也可以作为参数传递给另一个函数.比如: var add=function(a,b){ console.log(a+b) ...

  9. Jquery.extend()和jQuery.fn.extend(object);

    摘自: jquery $.fn $.fx是什么意思有什么用_jquery_脚本之家 jQuery.extend(object); 为扩展jQuery类本身.为类添加新的方法. jQuery.fn.ex ...

  10. Convolutional Neural Networks(4):Feature map size,Padding and Stride

    在CNN(1)中,我们用到下图来说明卷积之后feature maps尺寸和深度的变化.这一节中,我们讨论feature map size, padding and stride. 首先,在Layer1 ...