缓存可以降低系统延迟,负载,提高吞吐量。我们一般用到的第三方缓存框架OSCache,ehcache,memcache,redis等。当然一 个缓存框架是很复杂的,要考虑key的生成,索引生成,文件结构的生成,序列化还有反序列化,还有淘汰算法,过期检查,还有持久化媒介等。
当然我们不会去实现这么一个复杂的缓存系统。我们下面讲一个用java简单高效可伸缩的内存缓存系统。
背景:
我们现在有一个比较耗时的方法,我们希望把这个耗时的方法计算的结果缓存起来,同样的参数请求过来就不需要进行昂贵的计算过程了。
public interface Computable<A, V> { V compute(A arg); }
方案一:
public class HMCache<A, V> implements Computable<A, V> { private final Map<A, V> cache = new HashMap<A, V>(); private final Computable<A, V> c; public HMCache(Computable<A, V> c) { this.c = c; } public synchronized V compute(A arg) { V result = cache.get(arg); if (result == null) { result = c.compute(arg); cache.put(arg, result); } return result; } } 上面我们用HashMap实现了一个简单的缓存,它把我们计算的结果缓存了起来,并且通过sychronized保证了多个客户端访问情况下的线程安全。但是这个缓存效果可能达不到我们预期。这种同步策略,只能保证每次 只有一个线程访问(通过this来保证线程同步)。假如有一个线程进行了一个十分耗时的计算,那么其他线程都会被阻塞。这恐怕是不能接受的。
方案二:
public class CHMCache<A, V> implements Computable<A, V> { private final Map<A, V> cache = new ConcurrentHashMap<A, V>(); private final Computable<A, V> c; public CHMCache(Computable<A, V> c) { this.c = c; } public V compute(A arg) { V result = cache.get(arg); if (result == null) { result = c.compute(arg); cache.put(arg, result); } return result; } } 为了避免上诉HashMap结合sychronized导致的线程长时间独占锁阻塞引起的伸缩性性能问题。我们采用了ConcurrentHashMap来代替HashMap。这里我们为什么不用HashTable呢,因为HashTable是通过锁定整个 hash表来保证线程安全性的,get,put,remove等操作都会独占锁。但是ConcurrentHashMap采用了段锁,它把hash表分成了16段,每个操作只锁其操作的那一段hash表,这样就提高了它的性能。 用ConcurrentHashMap的方法,我们避免了方法一引起的性能问题。它拥有更好的性能,更好的伸缩性。但是上诉方案可能会引入了另一个问题:会计算相同的值。当一个线程正在进行一次长时间计算,但是其他线程又 不知道,就可能会引起上边的问题。 多次计算相同的值,会对性能带来影响。
方案三:
扫描二维码关注公众号,回复:
496447 查看本文章
public class CHMWithFutureCache<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 CHMWithFutureCache(Computable<A, V> c) { this.c = c; } public V compute(final A arg) { Future<V> f = cache.get(arg); if (f == null) { Callable<V> eval = new Callable<V>() { public V call() throws Exception { return c.compute(arg); } }; FutureTask<V> ft = new FutureTask<V>(eval); f = ft; cache.put(arg, ft); ft.run(); } try { return f.get(); } catch (Exception e) { //此处处理异常方式不好 return null; } } } 方案二的问题是,可能会导致重复计算。我们要达到的目的是什么呢:如果请求过来发现缓存里边没值的化,不是马上进行计算,而是去检查一下是否已经有计算在进行中了。如果有,那么阻塞等待计算返回。有没有可以 实现提交一个任务,然后等待它计算完成之后再取结果的组件呢?有,FutureTask!我们往缓存里边放的不是最终的结果,而是一个FutureTask。这样我们就能避免因为上诉问题产生的重复计算的问题。
方案四:
上面方案三还有没有什么问题呢?看到if判断语句没?先检查后执行,这个操作不是原子性的,还是会导致重复提交任务。
if (f == null)
public class CHMWithFutureCache<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 CHMWithFutureCache(Computable<A, V> c) { this.c = c; } public V compute(final A arg) { Future<V> f = cache.get(arg); if (f == null) { Callable<V> eval = new Callable<V>() { public V call() throws Exception { 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 (Exception e) { return null; } } } 我们采用了putIfabsent来避免上诉情况引起的重复计算的问题。如果已经提交过任务了,那么返回的是null,就不再进行提交计算了。
方案五:解决缓存污染问题。
public class CHMWithFutureCache3<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 CHMWithFutureCache3(Computable<A, V> c) { this.c = c; } public V compute(final A arg) { Future<V> f = cache.get(arg); while (true) { if (f == null) { Callable<V> eval = new Callable<V>() { public V call() throws Exception { 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 (Exception e) { cache.remove(arg); } } } } 假如提交的任务因为某些原因执行失败了,那么如果不做任何处理的话就会造成缓存污染,后续相同参数近来的请求都将会失败,所以需要在任务执行失败的时候将相关的FutureTask remove掉。
上诉示例摘自Java并发编程实战。