java併發編程實踐學習(5)構建塊爲計算結果建立高效,可伸縮的高速緩存

幾乎每一個服務器應用程序都會使用某種形式的高速魂村。複用已有的計算結果可以縮短等待時間,提高吞吐量,代價是佔用更多的內存。
在下面例子中Computerable

public interface Computable<A,V>{
    V computer(A arg) throws InterruptedException;
}

public class ExpensiveFunction implements Computable<String,BigInteger>{
    public BigInteger compute(String arg){
        //after 
        return new BigInteger(arg);
    }
}

public Memoizer1<A,V> implements Computable<A,V>{
    @GuardedBy("this")
    private final Map<A,V> cache = new HashMap<A,V>();
    private final Computable<A,V> c;
    public Memoizer1(Computable<A,V> c){
        this.c = c;
    }
    public synchronized V compute(A arg)throws InterruptedException{
        V result = cache.get(arg);
        if(result == null){
            result = c.compute(arg);
            cache.put(arg,result);
        }
        return result;
    }
}

但是這樣同步的方法會降低併發性,所以使用ConcurrentHashMap代替HashMap改進併發行爲,因爲ConcurrentHashMap是線程安全的。
然而這樣的做法還是有問題,當一個線程啓動了一個開銷很大的計算,其他線程不知道這個這個計算正在進行中,可能又會重複這個計算
這裏寫圖片描述
但是已經有一個類可以做這些事:FutuerTask.它代表了一個計算過程,可能結束,也可能在進行中。FutureTask.get只要結果可用,就會立即返回結果。否則一直阻塞到計算出結果。
所以可以改進爲

public class Memoizer3<A, V> implements Computable<A, V> {
    private final Map<A, Future<V>> cache = new ConcurrentHashMap<A, Future<V>>();
    private final Computable<A, V> c;

    public Memoizer3(Computable<A, V> c) {
        this.c = c;
    }

    public V compute(final A arg) throws InterruptedException {
        Future<V> f = cache.get(arg);
        if (f == null) {
            Callable<V> eval = new Callable<V>() {
                public V call() throws InterruptedException {
                    return c.compute(arg);
                }
            };
            FutureTask<V> ft = new FutureTask<V>(eval);
            f = ft;
            cache.put(arg, ft);
            ft.run(); // call to c.compute happens here
        }
        try {
            return f.get();
        } catch (ExecutionException e) {
            cache.remove(arg);
        }
        return null;
    }
}

但是這樣還存在一個漏洞–倆個線程可能同時計算相同的值。這是因爲複合操作(缺少即加入)運行在底層map中,不能通過加鎖來使它原子化。這可以通過ConcurrentHashMap中的原子化方法putIfAbsent解決。
緩存一個Future而不是同一個值會帶來緩存污染的可能性。爲了避免Memoizer如果發現計算被取消,就把Future從緩存中移除,如果發現RuntimeException也會移除。這些可以通過Future的自雷完成。他會爲每一個結果關聯一個過期時間,並週期性的掃描緩存中的過期訪問。

public class Memoizer <A, V> implements Computable<A, V> {
    private final ConcurrentMap<A, Future<V>> cache
            = new ConcurrentHashMap<A, Future<V>>();
    private final Computable<A, V> c;

    public Memoizer(Computable<A, V> c) {
        this.c = c;
    }

    public V compute(final A arg) throws InterruptedException {
        while (true) {
            Future<V> f = cache.get(arg);
            if (f == null) {
                Callable<V> eval = new Callable<V>() {
                    public V call() throws InterruptedException {
                        return c.compute(arg);
                    }
                };
                FutureTask<V> ft = new FutureTask<V>(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) {
                throw LaunderThrowable.launderThrowable(e.getCause());
            }
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章