假设服务器有一个computer复杂运算,耗时较久,如果服务器要实现缓存,一种思路就是通过HashMap或者redis来做缓存
下面是使用HashMap的简单示例,源码取自《并发编程实战》
1 package net.jcip.examples; 2 3 import java.math.BigInteger; 4 import java.util.*; 5 6 import net.jcip.annotations.*; 7 8 /** 9 * Memoizer1 10 * 11 * Initial cache attempt using HashMap and synchronization 12 * 13 * @author Brian Goetz and Tim Peierls 14 */ 15 public class Memoizer1 <A, V> implements Computable<A, V> { 16 @GuardedBy("this") private final Map<A, V> cache = new HashMap<A, V>(); 17 private final Computable<A, V> c; 18 19 public Memoizer1(Computable<A, V> c) { 20 this.c = c; 21 } 22 23 public synchronized V compute(A arg) throws InterruptedException { 24 V result = cache.get(arg); 25 if (result == null) { 26 result = c.compute(arg); 27 cache.put(arg, result); 28 } 29 return result; 30 } 31 } 32 33 34 interface Computable <A, V> { 35 V compute(A arg) throws InterruptedException; 36 } 37 38 class ExpensiveFunction 39 implements Computable<String, BigInteger> { 40 public BigInteger compute(String arg) { 41 // after deep thought... 42 return new BigInteger(arg); 43 } 44 }
我们看compute方法的逻辑,先从hashMap中取,取不到再计算后存入hashMap。这是最容易想到的逻辑,但由于hashMap不是线程安全的,所以做一下优化,用ConcurrentHashMap
1 package net.jcip.examples; 2 3 import java.util.*; 4 import java.util.concurrent.*; 5 6 /** 7 * Memoizer2 8 * <p/> 9 * Replacing HashMap with ConcurrentHashMap 10 * 11 * @author Brian Goetz and Tim Peierls 12 */ 13 public class Memoizer2 <A, V> implements Computable<A, V> { 14 private final Map<A, V> cache = new ConcurrentHashMap<A, V>(); 15 private final Computable<A, V> c; 16 17 public Memoizer2(Computable<A, V> c) { 18 this.c = c; 19 } 20 21 public V compute(A arg) throws InterruptedException { 22 V result = cache.get(arg); 23 if (result == null) { 24 result = c.compute(arg); 25 cache.put(arg, result); 26 } 27 return result; 28 } 29 }
使用ConcurrentHashMap仍然存在一个问题,前面说过,compute的计算耗时较久,可能存在线程A已经快计算完了,但还没来得及放到ConcurrentHashMap中,线程B获取不到就自己开始计算了
我们理想的情况是,线程B发现ConcurrentHashMap中没有的时候,先去瞅一眼有没有其他线程已经开始计算了,有的话就等,没有的话再自己算,算完放到缓存中。
这时候可以想到用FutureTask, 将Future作为ConcurrentHashMap的Value,如果有结果可用,Future.get将立即返回结果,否则线程就会阻塞直到计算完成,下面用ConcurrentHashMap+FutureTask进行优化。
1 package net.jcip.examples; 2 3 import java.util.*; 4 import java.util.concurrent.*; 5 6 /** 7 * Memoizer3 8 * <p/> 9 * Memoizing wrapper using FutureTask 10 * 11 * @author Brian Goetz and Tim Peierls 12 */ 13 public class Memoizer3 <A, V> implements Computable<A, V> { 14 private final Map<A, Future<V>> cache 15 = new ConcurrentHashMap<A, Future<V>>(); 16 private final Computable<A, V> c; 17 18 public Memoizer3(Computable<A, V> c) { 19 this.c = c; 20 } 21 22 public V compute(final A arg) throws InterruptedException { 23 Future<V> f = cache.get(arg); 24 if (f == null) { 25 Callable<V> eval = new Callable<V>() { 26 public V call() throws InterruptedException { 27 return c.compute(arg); 28 } 29 }; 30 FutureTask<V> ft = new FutureTask<V>(eval); 31 f = ft; 32 cache.put(arg, ft); 33 ft.run(); // call to c.compute happens here 34 } 35 try { 36 return f.get(); 37 } catch (ExecutionException e) { 38 throw LaunderThrowable.launderThrowable(e.getCause()); 39 } 40 } 41 }
上面的代码看似完美了,但还是存在一个明显的问题,就是if条件的“”先检查再执行”,两个线程仍有可能在同一时间进行了if判断,然后分别计算,分别放到缓存。所以并不能解决上面的问题
继续优化。使用ConcurrentHashMap.putIfAbsent来避免这个漏洞
1 package net.jcip.examples; 2 3 import java.util.concurrent.*; 4 5 /** 6 * Memoizer 7 * <p/> 8 * Final implementation of Memoizer 9 * 10 * @author Brian Goetz and Tim Peierls 11 */ 12 public class Memoizer <A, V> implements Computable<A, V> { 13 private final ConcurrentMap<A, Future<V>> cache 14 = new ConcurrentHashMap<A, Future<V>>(); 15 private final Computable<A, V> c; 16 17 public Memoizer(Computable<A, V> c) { 18 this.c = c; 19 } 20 21 public V compute(final A arg) throws InterruptedException { 22 while (true) { 23 Future<V> f = cache.get(arg); 24 if (f == null) { 25 Callable<V> eval = new Callable<V>() { 26 public V call() throws InterruptedException { 27 return c.compute(arg); 28 } 29 }; 30 FutureTask<V> ft = new FutureTask<V>(eval); 31 f = cache.putIfAbsent(arg, ft); 32 if (f == null) { 33 f = ft; 34 ft.run(); 35 } 36 } 37 try { 38 return f.get(); 39 } catch (CancellationException e) { 40 cache.remove(arg, f); 41 } catch (ExecutionException e) { 42 throw LaunderThrowable.launderThrowable(e.getCause()); 43 } 44 } 45 } 46 }
完美实现。
但这里需要注意如果f.get()抛出异常时,需要将Future从缓存中移除,即
cache.remove(arg, f);
不然可能存在缓存污染问题。