根据书里面的实现高效本次缓存的代码,下面的代码的前提是,假设有一个很耗时的计算,并且计算结果可以重用,我想将这个计算结果缓存在map里,并且保证计算过程(代码中的Callable代码)只会被执行一次。
private final ConcurrentHashMap> cache = new ConcurrentHashMap>(); private final Computable c; public Memoizer(Computable c) { this.c = c; } /* (non-Javadoc) * @see com.demo.buildingblocks.Computable#compute(java.lang.Object) */ @Override public V compute(final A arg) throws InterruptedException { while (true) { Futuref = cache.get(arg); if (f == null) { // Callable eval = new Callable () { @Override public V call() throws Exception { return c.compute(arg); } }; FutureTask ft = new FutureTask (eval); // **这里** f = cache.putIfAbsent(arg, ft); if (f == null) { f = ft; ft.run(); } } try { return f.get(); } catch (CancellationException e) { cache.remove(arg, f); } catch (ExecutionException e) { launderThrowable(e); } } } public static RuntimeException launderThrowable(Throwable t) { if (t instanceof RuntimeException) return (RuntimeException) t; else if (t instanceof Error) throw (Error) t; else throw new IllegalStateException("Not unchecked", t); }
我的分析是,putIfAbsent既然只能保证原子性,如果两个线程同时执行这个方法,那么会同时返回null,继而同时进入下面的if代码块,最后还是会导致compute执行了两次。
如果分析错误,那么正确的理解应该是怎样的呢?
看ConcurrentHashMap
的源码找到的原因:
public V putIfAbsent(K key, V value) { if (value == null) throw new NullPointerException(); int hash = hash(key.hashCode()); return segmentFor(hash).put(key, hash, value, true); }
而SegmentFor
的put方法有加锁操作:
V put(K key, int hash, V value, boolean onlyIfAbsent) { lock(); try { // ... } finally { unlock(); } }
这样就保证了不会有两个线程同时返回null的情况。