一種習以為常的緩存寫法:
IF value in cached THEN return value from cache ELSE compute value save value in cache return value END IF
看上去邏輯無比正確,但實際上會造成2種問題:
1、這種方法是不線程安全的。
2、產生數值寫入重復,造成錯誤的數據。
如下圖,在線程1執行計算數值的過程中,線程2也進入數據檢查,將多次寫入數據,程序非常危險。
演示錯誤代碼:
//最容易產生的錯誤寫法,先讀取緩存,讀不到就寫緩存 public Long getNumber(final long index) { if (cache.containsKey(index)) { return cache.get(index); } final long value = getNumber(index - 1) + getNumber(index - 2); cache.put(index, value); return value; }
1、傳統的解決辦法,使用重入鎖 (getNumberByLock 方法)或者同步鎖(getNumberBySynchroniz 方法)。
代碼
import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public class NaiveCacheExample { private final Map<Long, Long> cache = new HashMap<>(); private Object o=new Object(); Lock lock =new ReentrantLock(); public NaiveCacheExample() { cache.put(0L, 1L); cache.put(1L, 1L); } //最容易產生的錯誤寫法,先讀取緩存,讀不到就寫緩存 public Long getNumber(final long index) { if (cache.containsKey(index)) { return cache.get(index); } final long value = getNumber(index - 1) + getNumber(index - 2); cache.put(index, value); return value; } //使用折返鎖,使讀寫同步 public Long getNumberByLock(final long index) { long value =0; try { lock.lock(); if (cache.containsKey(index)) { return cache.get(index); } value = getNumberByLock(index - 1) + getNumberByLock(index - 2); cache.put(index, value); return value; } catch (Exception e) {} finally { lock.unlock(); } return 0l; } //使用同步,使讀寫同步 public Long getNumberBySynchroniz(final long index) { synchronized (o) { long value =0; try { if (cache.containsKey(index)) { return cache.get(index); } value = getNumberBySynchroniz(index - 1) + getNumberBySynchroniz(index - 2); cache.put(index, value); return value; } catch (Exception e) {} finally { } } return 0l; } public static void main(final String[] args) { NaiveCacheExample naiveCacheExample =new NaiveCacheExample(); Thread threadA =new Thread(new Runnable() { @Override public void run() { System.out.println(naiveCacheExample.getNumberBySynchroniz(1000)); } } ,"Thread-A"); threadA.start(); final Thread threadB = new Thread(new Runnable() { public void run() { System.out.println(naiveCacheExample.getNumberBySynchroniz(1000)); } }, "Thread-B"); threadB.start(); } }
2、一個更好的緩存算法可以用 Callable
和 Future
。 緩存的值將存儲在一個實例 ConcurrentMap 中 ,ConcurrentMap 是線程安全的。
代碼:
import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.FutureTask; public class GenericCacheExample<K, V> { private final ConcurrentMap<K, Future<V>> cache = new ConcurrentHashMap<>(); private Future<V> createFutureIfAbsent(final K key, final Callable<V> callable) { Future<V> future = cache.get(key); if (future == null) { final FutureTask<V> futureTask = new FutureTask<V>(callable); future = cache.putIfAbsent(key, futureTask); if (future == null) { future = futureTask; futureTask.run(); } } return future; } public V getValue(final K key, final Callable<V> callable) throws InterruptedException, ExecutionException { try { final Future<V> future = createFutureIfAbsent(key, callable); return future.get(); } catch (final InterruptedException e) { cache.remove(key); throw e; } catch (final ExecutionException e) { cache.remove(key); throw e; } catch (final RuntimeException e) { cache.remove(key); throw e; } } public void setValueIfAbsent(final K key, final V value) { createFutureIfAbsent(key, new Callable<V>() { @Override public V call() throws Exception { return value; } }); } }
參考博客:
http://www.javacreed.com/how-to-cache-results-to-boost-performance/