併發編程——構建高效切可伸縮的結果緩存

使用HsahMap
首先我們定義一個Computable接口,該接口包含一個compute()方法,該方法是一個耗時很久的數值計算方法。Memoizer1是第一個版本的緩存,該版本使用hashMap來保存之前計算的結果,compute方法將首先檢查需要的結果是否已經在緩存中,如果存在則返回之前計算的值,否則重新計算並把結果緩存在HashMap中,然後再返回。

interface Computable<A, V> {
    V compute(A arg) throws InterruptedException;//耗時計算
}

public class Memoizer1<A, V> implements Computable<A, V> {
    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;
    }
}

HashMap不是線程安全的,因此要確保兩個線程不會同時訪問HashMap,Memoizer1採用了一種保守的方法,即對整個方法進行同步。這種方法能確保線程安全性,但會帶來一個明顯的可伸縮問題:每次只有一個線程可以執行compute。

使用ConcurrentHashMap的版本
我們可以用ConcurrentHashMap代替HashMap來改進Memoizer1中糟糕的併發行爲,由於ConcurrentHashMap是線程安全的,因此在訪問底層Map時就不需要進行同步,因此避免了對compute()方法進行同步帶來的串行性:

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

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

    public V compute(A arg) throws InterruptedException {
        V result = cache.get(arg);
        if (result == null) {
            result = c.compute(arg);
            cache.put(arg, result);
        }
        return result;
    }
}

但是這個版本的緩存還是有問題的,如果線程A啓動了一個開銷很大的計算,而其他線程並不知道這個線程正在進行,那麼很可能會重複這個計算。

FutureTask版本1

我們可以在map中存放Future對象而不是最終計算結果,Future對象相當於一個佔位符,它告訴用戶,結果正在計算中,如果想得到最終結果,請調用get()方法。Future的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;
    }
}

Memoizer3解決了上一個版本的問題,如果有其他線程在計算結果,那麼新到的線程會一直等待這個結果被計算出來,但是他又一個缺陷,那就是仍然存在兩個線程計算出相同值的漏洞。這是一個典型的”先檢查再執行”引起的競態條件錯誤,我們先檢查map中是否存在結果,如果不存在,那就計算新值,這並不是一個原子操作,所以兩個線程仍有可能在同一時間內調用compute來計算相同的值。

FutureTask版本2
Memoizer3存在這個問題的原因是,複合操作”若沒有則添加”不具有原子性,我們可以改用ConcurrentMap中的原子方法putIfAbsent,避免了Memoizer3的漏洞。

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());
            }
        }
    }
}

當緩存對象是Future而不是值的時候,將導致緩存污染問題,因爲某個計算可能被取消或失敗,當出現這種情況時,我們應該把對象從map中移除,然後再重新計算一遍。這個緩存系統的使用十分簡單,只需傳入一個Computable對象即可構造緩存,要得到計算結果,調用compute()方法即可,該方法會把計算過的結果緩存起來。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章