通过多线程的所有组件学习之后,我们要学习到如何把一个类包装成一个多线程安全类,下面通过构造一个——计算缓存类,在构造的过程中一步一步的优化,最终来得到我们想要的计算缓存类。
类提供的功能:类提供一个计算的功能,然后把计算传入的值与结果缓存在一个Map中,当第二次计算时先从缓存里面查看看曾经有没有计算过,有的话就直接返回结果,没有的话就进行计算,存到缓存再返回结果。
菜鸟的做法
如果是我的话,首先我会这么做
定义一个计算的接口,里面提供一个计算的方法,等待实现
public interface Computable<A,V> {//V代表返回结果
V computer(A arg);//A表示参数
}
定义一个具体的计算类,用来实现计算的功能
public class ExpensiveFunction implements Computable<String,BigInteger> {
/* (非 Javadoc)
* @see com.jjt.cache.Computable#computer(java.lang.Object)
*/
@Override
public BigInteger computer(String arg) {
//模拟经过长时间的计算
return new BigInteger(arg);
}
}
最后定义一个主类来实现我们想要的逻辑,这里使用的是组合的方式来进行计算
public class Memosizer<A,V> implements Computable<A, V>{
private final Map<A,V> cache = new HashMap<A,V>();//缓存数据结构
private final Computable<A,V> c;//具体计算类
/**
*
*/
public Memosizer(Computable<A, V> c) {
this.c=c;//构造函数传入一个具体的计算类
}
/* (非 Javadoc)
* @see com.jjt.cache.Computable#computer(java.lang.Object)
*/
@Override
public synchronized V computer(A arg) {//实现我们想要的逻辑
V result = cache.get(arg);
if(result==null){
result = c.computer(arg);
cache.put(arg, result);
}
return result;
}
}
解析上面的代码,我们可以看到,由于hashMap是非线程安全的,所以我们只能通过synchronized 加一个隐形锁来对我们的逻辑进行锁定,但是这样导致我们发生并发时,我们会因为计算时间的关系导致线程间进行长时间的等待,降低了吞吐率。那我们接下来看看如何改进。
把线程安全性交给线程安全Map类管理
// private final Map<A,V> cache = new HashMap<A,V>();
private final Map<A,V> cache = new ConcurrentHashMap<A,V>();
ConcurrentHashMap内部实现机构是分段锁,去除隐式锁换成并发Map有助于我们增加吞吐量,不过这里又存在另外的一个问题。
延时任务交给FutureTask
我们已经知道有一个类能基本实现这个功能:FutureTask。FutureTask表示一个计算的过程。这个过程可能是已经计算完成,也可能是正在进行。如果结果可用,那么 FutureTask.get将立即返回,否则会一直阻塞,直到结果计算出来再将其返回。
所以我们现在把原来的
ConcurrenHashMap<A,V>,更改为ConcurrentHashMap<A,Future<V>>
我们直接把计算的步骤放到FutureTask里面,让他帮我们进行处理,我们直接把FutureTask放到缓存里面,需要的时候再get出来,这样就可以解决了我们第二个点提到的那个技术难题。改进后的Memosize如下:
public class Memosizer<A,V> implements Computable<A, V>{
// private final Map<A,V> cache = new HashMap<A,V>();
private final Map<A,Future<V>> cache = new ConcurrentHashMap<A,Future<V>>();
private final Computable<A,V> c;//具体计算类
/**
*
*/
public Memosizer(Computable<A, V> c) {
this.c=c;
}
/* (非 Javadoc)
* @see com.jjt.cache.Computable#computer(java.lang.Object)
*/
@Override
public V computer(A arg) throws InterruptedException, ExecutionException {//去除分段锁之后
Future<V> result = cache.get(arg);
if(result==null){
Callable<V> callable = new Callable<V>() {
@Override
public V call() throws Exception {
return c.computer(arg);
}
};
FutureTask<V> f = new FutureTask<V>(callable);
result=f;
cache.put(arg, result);
f.run();//这里将调用c.compute方法,在这里线程被阻塞等没关系,因为futureTask已经进了缓存了
}
return result.get();
}
}
虽然上面的代码看起来好像几乎是完美的,但是不要忘了还存在一个非原子操作