建立高速缓存机制-java版
前言
一台计算机的核心是CPU,它是计算机系统的运算和控制核心。由于它处理运算速度快,所以基本都会给CPU配置一级缓存,当CPU要读取一个数据时,首先从缓存中查询,如果没有在从内存或者磁盘块中找。
同样的,作为一个服务器应用程序,为了让应用程序运行更快速,响应更给力,我们会给它设置一些数据缓存,这样可以提高应用程序的吞吐量、缩短客户端的响应时间。
建立缓存过程分析
我们从java最常用的方案开始——一个简单的HashMap。
public interface Computable<A, V> {
V compute(A arg) throws InterruptedException;
}
public class ExpensiveFunction implements Computable<String, BigInteger> {
@Override
public BigInteger compute(String arg) throws InterruptedException {
// after deep thought...
return new BigInteger(arg);
}
}
Computable<A, V>接口描述了一个功能,输入类型是A,输出结果的类型是V。ExpensiveFunction实现了Computable。需要花比较长的时间来计算结果。所以我们计划把计算过的值都放进一个HashMap中,这样下一次有同一个A值进来时,直接获取A的计算结果。
2.1 Synchronized版
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;
}
@Override
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;
}
}
Memoizer1实现了第一个版本,HashMap不是线程安全的,所以使用synchronzied关键字来保证线程安全,如果cache变量中有计算结果,直接从cache取,不需要再次计算,省下许多时间。但使用synchronzied使得一次只有一个线程能够执行compute。如果一个线程正在计算结果,那其他调用compute的线程可能被阻塞很长时间,造成性能下降,这不是我们希望通过缓存得到的性能优化结果。
2.2 ConcurrentHashMap版
public class Memoizer2<A, V> implements Computable<A, V> {
private final Map<A, V> cache = new ConcurrentHashMap<>();
private final Computable<A, V> c;
public Memoizer2(Computable<A, V> c) {
this.c = c;
}
@Override
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;
}
}
Memoizer2用ConcurrentHashMap取代HashMap,改进了Memoizer1中那种糟糕的并发行为。因为ConcurrentHashMap是线程安全的,所以不需要使用Synchronized关键字,而是使用内部hash桶的分段锁机制。
Memoizer2与Memoizer1相比,毫无疑问具有了更好的并发性:多线程可以真正并发访问了。但是作为高速缓存仍然存在缺陷:当两个线程同时调用compute时,如果是计算同一个值,此时compute是需要很大的开销的,在一个线程还在计算中时,其它线程不知道,所以可能会重复计算。而我们希望的是:如果A线程正在计算arg,那B就不要重复计算arg,等A计算好,直接取arg对应的V就好了。
2.3 ConcurrentHashMap + FutureTask版
public class Memoizer3<A, V> implements Computable<A, V> {
private final Map<A, Future<V>> cache = new ConcurrentHashMap<>();
private final Computable<A, V> c;
public Memoizer3(Computable<A, V> c) {
this.c = c;
}
@Override
public V compute(A arg) throws InterruptedException {
Future<V> f = cache.get(arg);
if (f == null) {
Callable<V> eval = () -> {
return c.compute(arg);
};
FutureTask<V> ft = new FutureTask<>(eval);
f = ft;
cache.put(arg, ft);
ft.run(); // 调用 c.compute发生在这里
}
try {
return f.get();
} catch (ExecutionException e) {
e.printStackTrace();
throw new RuntimeException(e.getMessage());
}
}
}
Memoizer3为缓存的值重新定义可存储Map,用ConcurrentHashMap<A, Future>取代ConcurrentHashMap<A,V>。Memoizer3首先检查一个相应的计算是否开始,如果不是,就创建一个FutureTash,并把它注册到map中,并开始计算,如果是,那么它就会等待正在计算的结果。
Memoizer3的实现近乎是完美的:它展示了非常好的并发性,能很快返回已经计算过的结果,如果新到的线程请求的是其它线程正在计算的结果,它也会耐心的等待。
Memoizer3只有一个问题,就是仍然存在这种可能性:2个线程同时计算arg,此时由于compute中的if代码块是非原子性的复合操作,2个线程会同时进入到if代码块中,依旧会同时计算同一个arg。但ConcurrentHashMap中提供了一个原子化的putIfAbsent方法,可以消除Memoizer3的隐患。
2.4 最终版
public class Memoizer<A, V> implements Computable<A, V> {
private final Map<A, Future<V>> cache = new ConcurrentHashMap<>();
private final Computable<A, V> c;
public Memoizer(Computable<A, V> c) {
this.c = c;
}
@Override
public V compute(A arg) throws InterruptedException {
Future<V> f = cache.get(arg);
if (f == null) {
Callable<V> eval = () -> {
return c.compute(arg);
};
FutureTask<V> ft = new FutureTask<>(eval);
f = ft;
cache.putIfAbsent(arg, ft);
ft.run(); // 调用 c.compute发生在这里
}
try {
return f.get();
} catch (ExecutionException e) {
e.printStackTrace();
throw new RuntimeException(e.getMessage());
}
}
}
Memoizer可以说是缓存的完美实现了:支持高并发,同一个参数计算也不会重复执行(多亏于ConcurrentHashMap的putIfAbsent原子化操作)。最终调用者通过调用Future.get(arg)方法获取计算结果。