1. 程式人生 > >【轉】基於Map的簡易記憶化緩存

【轉】基於Map的簡易記憶化緩存

還在 自己 == map cti extends inter end 參考資料

看到文章後,自己也想寫一些關於這個方面的,但是覺得寫的估計沒有那位博主好,而且又會用到裏面的許多東西,所以幹脆轉載。但是會在文章末尾寫上自己的學習的的東西。

原文出處如下:

http://www.cnblogs.com/micrari/p/6921661.html

背景

在應用程序中,時常會碰到需要維護一個map,從中讀取一些數據避免重復計算,如果還沒有值則計算一下塞到map裏的的小需求(沒錯,其實就是簡易的緩存或者說實現記憶化)。在公司項目裏看到過有些代碼中寫了這樣簡易的緩存,但又忽視了線程安全、重復計算等問題。本文主要就是談談這個小需求的實現。

實現

HashMap的實現

在公司項目裏看到過有類似如下的代碼。


public class SimpleCacheDemo {

    private Map<Integer, Integer> cache = new HashMap<>();

    public synchronized Integer retrieve(Integer key) {
        Integer result = cache.get(key);
        if (result == null) {
            result = compute(key);
            cache.put(value,result);
        }
        return result;
    }

    private Integer compute(Integer key) {
        // 模擬代價很高的計算
        return key;
    }
}

只是那位同事寫的代碼比這段代碼更糟,連synchronized關鍵字都沒加。

這段代碼的問題還在於由於在compute方法上進行了同步,所以大大降低了並發性,在具體場景中,如果compute代價很高,那麽其他線程會長時間阻塞。

基於ConcurrentHashMap的改進

一種改進的策略是將上述map的實現類替換為ConcurrentHashMap並去除compute上的synchronized。這樣可以規避在compute上同步帶來的伸縮性問題。

但與上面的方法一樣還有一個問題在於,由於compute的耗時可能不少,在另一個線程讀到map中還沒有值時可能同樣會開始進行計算,這樣就出現了重復高代價計算的問題。

基於Future的改進

為了規避重復計算的問題,可以將map中的值類型用Future封起來。代碼如下:


public class SimpleCacheDemo {

    private Map<Integer, Future<Integer>> cache = new HashMap<>();

    public Integer retrieve(Integer key) {
        Future<Integer> result = cache.get(key);
        if (result == null) {
            FutureTask<Integer> task = new FutureTask<>(() -> compute(key));
            cache.put(key, task);
            result = task;
            task.run();
        }
        try {
            return result.get();
        } catch (InterruptedException | ExecutionException e) {
            throw new RuntimeException(e);
        }
    }

    private Integer compute(Integer value) {
        // 模擬代價很高的計算
        return value;
    }

}

當在map中讀取到result為null時,建一個FutureTask塞到map並進行計算,最後獲取結果。但實際上這樣的實現仍然有可能出現重復計算的問題,問題在於判斷map中是否有值,無值則插入的操作是一個復合操作。上面的代碼中這樣的無則插入的復合操作既不是原子的,也沒有同步。

putIfAbsent

上面的問題無非就只剩下了無則插入這樣的先檢查後執行的操作不是原子的也沒有同步。

事實上,解決的方法很簡單,在JDK8中Map提供putIfAbsent,也即若沒有則插入的方法。本身是不保證原子性、同步性的,但是在ConcurrentHashMap中的實現是具有原子語義的。我們可以將上面的代碼再次改寫為如下形式:


public class SimpleCacheDemo {

    private Map<Integer, Future<Integer>> cache = new ConcurrentHashMap<>();

    public Integer retrieve(Integer key) {
        FutureTask<Integer> task = new FutureTask<>(() -> compute(key));
        
        Future<Integer> result = cache.putIfAbsent(key, task);
        if (result == null) {
            result = task;
            task.run();
        }

        try {
            return result.get();
        } catch (InterruptedException | ExecutionException e) {
            throw new RuntimeException(e);
        }
    }

    private Integer compute(Integer value) {
        // 模擬代價很高的計算
        return value;
    }

}

這個實現的缺陷在於,每次都要new一個FutureTask出來。可以作一個小優化,通過先get判斷是否為空,如果為空再初始化一個FutrueTask用putIfAbsent扔到map中。

computeIfAbsent

實際上以上介紹的幾種實現在《Java並發編程實戰》中都有描述
這本大師之作畢竟寫作時還是JDK5和6的時代。在JDK8中,Map以及ConcurrentMap接口新增了computeIfAbsent的接口方法。在ConcurrentHashMap中的實現是具有原子語義的。所以實際上,上面的程序我們也可以不用FutureTask,直接用computeIfAbsent,代碼如下:


public class SimpleCacheDemo {

    private Map<Integer, Integer> cache = new ConcurrentHashMap<>();

    public Integer retrieve(Integer key) {
        return cache.computeIfAbsent(key, this::compute);
    }

    private Integer compute(Integer value) {
        // 模擬代價很高的計算
        return value;
    }

}

總結

上面用簡易的代碼展示了在開發小型應用中時常需要的基於Map的簡易緩存方案,考慮到的點在於線程安全、伸縮性以及避免重復計算等問題。如果代碼還有其他地方有這樣的需求,不妨抽象出一個小的框架出來。上面的代碼中沒有考慮到地方在於內存的使用消耗等,然而在實戰中這是不能忽視的一點。

參考資料

  • 《Java並發編程實戰》
  • 《Java並發編程的藝術》

--------------------------------------------------------------------P.S.一個知識點整理-------------------------------------------------------------

在最後一個例子中,用到了Map的computeIfAbsent()方法,

 1 default V computeIfAbsent(K key,
 2             Function<? super K, ? extends V> mappingFunction) {
 3         Objects.requireNonNull(mappingFunction);
 4         V v;
 5         if ((v = get(key)) == null) {
 6             V newValue;
 7             if ((newValue = mappingFunction.apply(key)) != null) {
 8                 put(key, newValue);
 9                 return newValue;
10             }
11         }
12 
13         return v;
14     }

這個方法是JDB1.8開始出現的。用了的Lambda表達式。

正如方法名鎖揭示的那樣,key值不存在的話,調用mappingFunction.apply(key)這個方法,進行key的相關處理,之後,會將key-value放進map裏,最後返回新生成的value值。我們用Lambda表達式重寫的也就是mappingFunction.apply(key)這個方法。

與之類似的computeIfPresent、compute都是8中新出現的方法,通過名字就可以理解,實現的代碼上面的類似。

【轉】基於Map的簡易記憶化緩存