您的位置:首页 > 其它

缓存的简单实现

2016-12-25 20:32 288 查看
//此文基于《Java并发编程实践》

我们都知道在应用程序中合理地使用缓存,能更快的访问我们之前的计算结果,从而提高吞吐量。例如Redis和Memcached基于内存的数据存储系统等。此篇文章介绍如何实现简单缓存。

首先定义一个Computable接口A是输入,V是输出。

package simplecache;

/**
* Created by yulinfeng on 12/25/16.
*/
public interface Computable<A, V> {
V compute(A arg) throws InterruptedException;
}


实现这个接口,也即是在ExpensiveFunction做具体的计算过程。

package simplecache;

/**
* Created by yulinfeng on 12/25/16.
*/
public class ExpensiveFunction implements Computable<String, Integer> {
@Override
public Integer compute(String arg) throws InterruptedException {
//计算
return new Integer(arg);
}
}


接着将创建一个Computable包装器,帮助记住之前的计算结果,并将缓存过程封装起来(Memoization)。

1.利用简单HashMap实现缓存

package simplecache;

import java.util.HashMap;
import java.util.Map;

/**
* Created by yulinfeng on 12/25/16.
*/
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 (null == result){
result = c.compute(arg);
cache.put(arg, result);
}
return result;
}
}


我们首先利用最简单的HashMap实现缓存,由于HashMap并不是线程安全的,所以在compute方法使用synchronized关键字,同步以实现线程安全。可见使用synchronized同步方法如此大粒度的同步必然会带来并发性的降低,因为每次只有一个线程执行compute方法,其余线程只能排队等待。

2.利用并发容器ConcurrentHashMap

第1种方法能实现缓存,且能实现线程安全的缓存,不过带来的问题就是并发性降低。我们使用并发包中的ConcurrentHashMap并发容器。

package simplecache;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
* Created by yulinfeng on 12/25/16.
*/
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;
}

@Override
public V compute(A arg) throws InterruptedException {
V result = cache.get(arg);
if (null == result){
result = c.compute(arg);
cache.put(arg, rsult);
}
return result;
}
}


毫无疑问,利用ConcurrentHashMap会比简单HashMap带来更好的并发性,同时它也是线程安全的。不过在有一种条件下,这种方式会带来一个新的问题,当这个计算过程比较复杂,计算时间比较长时,线程T1正在计算没有结束,此时线程T2并不知道此时T1已经在计算了,所以它同样会再次进行计算,这种条件下相当于一个值被计算了2次。我们应该想要达到的效果应该是T1正在计算,而此时T2能发现T1正在计算相同值,此时应该阻塞等待T1计算完毕返回计算结果,而不是T2也去做一次计算。FutureTask表示一个计算过程,这个计算过程可能已经计算完成,也有可能正在计算。如果有结果可用,那么FutureTask.get将立即返回结果,否则会一直阻塞直到计算结束返回结果。这正好我们想要达到的效果。

3.缓存的最佳实践——ConcurrentHashMap+FutureTask

package simplecache;

import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;

/**
* Created by yulinfeng on 12/25/16.
*/
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;
}

@Override
public V compute(final A arg) throws InterruptedException {
Future<V> f = cache.get(arg);
if (null == f){
Callable<V> eval = new Callable<V>() {
@Override
public V call() throws InterruptedException {
return c.compute(arg);
}
};
FutureTask<V> ft = new FutureTask<V>(eval);
cache.put(arg, ft);
ft.run();   //调用执行c.compute
}
try {
return f.get();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}


不了解FutureTask可以去补补了,但记住上面所说“FutureTask表示一个计算过程,这个计算过程可能已经计算完成,也有可能正在计算。如果有结果可用,那么FutureTask.get将立即返回结果,否则会一直阻塞直到计算结束返回结果。”,但这并不算是最完美的实现,在compute方法中出现了if的复合操作,也就是说在期间还是很有可能出现如同ConcurrentHashMap一样的重复计算,只是概率降低了而已。幸好,ConcurrentHashMap为我们提供了putIfAbsent的原子方法,从而完美的避免了这个问题。

package simplecache;

import java.util.concurrent.*;

/**
* Created by yulinfeng on 12/25/16.
*/
public class Memoizer<A, V> implements Computable<A, V> {
private final ConcurrentHashMap<A, Future<V>> cache = new ConcurrentHashMap<A, Future<V>>();
private final Computable<A, V> c;

public Memoizer(Computable<A, V> c){
this.c = c;
}

@Override
public V compute(final A arg) throws InterruptedException {
while (true) {
Future<V> f = cache.get(arg);
if (null == f) {
Callable<V> eval = new Callable<V>() {
@Override
public V call() throws Exception {
return c.compute(arg);
}
};
FutureTask<V> ft = new FutureTask<V>(eval);
f = cache.putIfAbsent(arg, ft);
if (null == f){
f = ft;
ft.run();
}
}
try {
return f.get();
} catch (CancellationException e){
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
}


这样我们利用ConcurrentHashMap的并发性已经putIfAbsent原子性,以及FutureTask的特性实现了一个简单缓存。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: