package five; /** * * K 表示输入的值,V 表示输出的值 * * @param <K> * @param <V> */ public interface Computable<K,V> { V compute(K arg); }
package five; import java.math.BigInteger; import common.Utils; public class ExpensiveFunction implements Computable<String, BigInteger> { @Override public BigInteger compute(String arg) { // 一个很长时间的计算 System.out.println("有人执行我..."); Utils.sleep(1); // sleep 一会表示执行时间很长 return new BigInteger(arg); } }
package five; /** * 缓存类 */ import java.math.BigInteger; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CountDownLatch; public class Memoizer<K,V> implements Computable<K,V>{ private final Map<K,V> cache = new ConcurrentHashMap<K,V>();// 用concurrentHashMap代替 HashMap private final Computable<K,V> c ; public Memoizer(Computable<K,V> c) { this.c = c; } public V compute(K arg) { // 去掉 synchronized V result = cache.get(arg); if(result == null) { result = c.compute(arg); cache.put(arg,result); } return result; } /** * 上面的代码存在问题:如果有两个线程同时访问相同 数值,两者都没有找到,则会两个线程都会进入计算 */ public static void main(String args[]) { final Memoizer<String,BigInteger> m = new Memoizer<String,BigInteger>(new ExpensiveFunction()); final CountDownLatch gate = new CountDownLatch(2); // 创建两个线程,同时向 catche 中取值 int n = 2; for(int i=0;i<n;i++) { Thread t = new Thread() { public void run() { m.compute("11111111111"); gate.countDown(); }; }; t.start(); } try { gate.await(); // 直到上面两个线程执行结束后再执行 }catch(InterruptedException e) { } System.out.println(m.cache.size()); } }
上面的代码存在两个线程请求同一个数可能会计算两次的可能,下面是改进的:
package five; import java.math.BigInteger; import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.FutureTask; public class Memoizers<K, V> implements Computable<K, V> { private final ConcurrentMap<K, Future<V>> cache = new ConcurrentHashMap<K, Future<V>>(); private Computable<K, V> c; public Memoizers(Computable<K, V> c) { this.c = c; } public V compute(final K key) { Future<V> future = cache.get(key); // get put 为复合操作,不能保证原子性,要用 // putIfAbsent if (future == null) { Callable<V> call = new Callable<V>() { @Override public V call() throws Exception { return c.compute(key); } }; /** * 这里不可以用 put,因为两线程都没有在缓存里找到,当两个线程都执行到这个地方时,它们请求的值可以相同,则 comput方法要执行两次, * 所以要用 putifAbsent,两者的区别是,如果存在,就不向里面添加,而且返回值为原来的值,如果不存在,则向里面添加,返回值为空,我们可以根据返回值 * 当开始两个线程都查询到 为 null 时,开始执行这里面的代码,当插入一个的时候,不光插入,我们还能起始查询的作用,如果返回值为原来的值不空,说 * 明这个值已经存在了,那么我们就可以执行这个存在的值的 compute,而不用执行两次compute了,这样就不会有两个线程请求同一个数而执行两次了 */ FutureTask<V> task = new FutureTask<V>(call); future = cache.putIfAbsent(key, task);//putifabsent如果存在则返回原来的,不存在,则返回null, if (future == null) { future = task; task.run(); } } try { return future.get(); } catch (InterruptedException e) { } catch (ExecutionException e) { e.printStackTrace(); } throw new RuntimeException(); } public static void main(String args[]) { final Memoizers<String, BigInteger> m = new Memoizers<String, BigInteger>( new ExpensiveFunction()); int n = 2; final CountDownLatch latch = new CountDownLatch(n); for (int i = 0; i < n; i++) { Thread t = new Thread() { @Override public void run() { m.compute("11111111111111111111111"); latch.countDown(); } }; t.start(); } try { latch.await(); } catch (InterruptedException e) { } System.out.println(m.cache.size()); } }