簡體   English   中英

ThreadLocal HashMap vs ConcurrentHashMap用於線程安全的未綁定緩存

[英]ThreadLocal HashMap vs ConcurrentHashMap for thread-safe unbound caches

我正在創建一個具有以下特征的memoization緩存:

  • 高速緩存未命中將導致計算和存儲條目
    • 這個計算非常昂貴
    • 這種計算是冪等的
  • 無界限(條目從未刪除),因為:
    • 輸入將導致最多500個條目
    • 每個存儲的條目都很小
    • 緩存相對短缺(通常不到一小時)
    • 總的來說,內存使用不是問題
  • 將有成千上萬的讀取 - 在緩存的生命周期中,我預計99.9%+緩存命中率
  • 必須是線程安全的

什么會有一個優越的性能,或在什么條件下一個解決方案優於另一個解決方案?

ThreadLocal HashMap:

class MyCache {
    private static class LocalMyCache {
        final Map<K,V> map = new HashMap<K,V>();

        V get(K key) {
            V val = map.get(key);
            if (val == null) {
                val = computeVal(key);
                map.put(key, val);
            }
            return val;
        }
    }

    private final ThreadLocal<LocalMyCache> localCaches = new ThreadLocal<LocalMyCache>() {
        protected LocalMyCache initialValue() {
            return new LocalMyCache();
        }
    };

    public V get(K key) {
        return localCaches.get().get(key);
    }
}

ConcurrentHashMap的:

class MyCache {
    private final ConcurrentHashMap<K,V> map = new ConcurrentHashMap<K,V>();

    public V get(K key) {
        V val = map.get(key);
        if (val == null) {
            val = computeVal(key);
            map.put(key, val);
        }
        return val;
    }
}

我認為如果有很多線程,ThreadLocal解決方案最初會因為每個線程的所有緩存未命中而緩慢,但是超過數千次讀取,攤銷成本將低於ConcurrentHashMap解決方案。 我的直覺是否正確?

還是有更好的解決方案?

使用ThreadLocal作為緩存是一個不好的做法

在大多數容器中,線程通過線程池重用,因此永遠不會是gc。 這會帶來一些有線的東西

使用ConcurrentHashMap你必須管理它以防止內存泄漏

如果你堅持,我建議使用周或軟參考並在富有maxsize之后逐出

如果您正在尋找內存緩存解決方案(不要重新發明輪子),請嘗試使用guava緩存http://docs.guava-libraries.googlecode.com/git/javadoc/com/google/common/cache/CacheBuilder.html

這個計算非常昂貴

我認為這是你創建緩存的原因,這應該是你最關心的問題。

雖然解決方案的速度可能略有不同<< 100 ns,但我懷疑能夠在線程之間共享結果更為重要。 即ConcurrentHashMap可能是您的應用程序的最佳選擇,因為從長遠來看,它可能會為您節省更多的CPU時間。

簡而言之,與多次計算同一事物的成本相比,您的解決方案的速度可能很小(對於多個線程)

請注意,ConcurrentHashMap實現不是線程安全的,可能導致一個項目被計算兩次。 如果直接存儲結果而不使用顯式鎖定,那么實現它是非常復雜的,如果性能是一個問題,你當然希望避免這種情況。

值得注意的是ConcurrentHashMap具有高度可擴展性,並且在高爭用下運行良好。 我不知道ThreadLocal是否會表現更好。

除了使用庫之外,您還可以從實踐清單5.19中的Java Concurrency中獲得一些靈感。 這樣做是為了節省Future<V>在你的地圖,而不是V 這有助於在保持高效(無鎖)的同時使整個方法線程安全。 我粘貼下面的實現以供參考,但本章值得一讀,以了解每個細節都很重要。

public interface Computable<K, V> {

    V compute(K arg) throws InterruptedException;
}

public class Memoizer<K, V> implements Computable<K, V> {

    private final ConcurrentMap<K, Future<V>> cache = new ConcurrentHashMap<K, Future<V>>();
    private final Computable<K, V> c;

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

    public V compute(final K 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 new RuntimeException(e.getCause());
            }
        }
    }
}

鑒於實現這兩者相對容易,我建議你嘗試它們並在穩態負載下進行測試看看哪一個對你的應用程序表現最佳。

我的猜測是ConcurrentHashMap會快一點,因為它不必像ThreadLocal那樣對Thread.currentThread()進行本機調用。 但是,這可能取決於您正在存儲的對象以及它們的哈希編碼的效率。

我可能還值得嘗試將並發映射的concurrencyLevel調整為您需要的線程數。 默認為16。

兩種解決方案中的查找速度可能相似。 如果沒有其他問題,我更喜歡ThreadLocal,因為多線程問題的最佳解決方案是單線程。

但是,您的主要問題是您不希望同一個鍵的並發計算; 所以每個鍵應該有一個鎖; 這種鎖通常可以通過ConcurrentHashMap實現。

所以我的解決方案就是

class LazyValue
{
    K key;

    volatile V value;

    V getValue() {  lazy calculation, doubled-checked locking }
}


static ConcurrentHashMap<K, LazyValue> centralMap = ...;
static
{
    for every key
        centralMap.put( key, new LazyValue(key) );
}


static V lookup(K key)
{
    V value = localMap.get(key);
    if(value==null)
        localMap.put(key, value=centralMap.get(key).getValue())
    return value;
}

性能問題無關緊要,因為解決方案不相同。

線程之間不共享ThreadLocal哈希映射,因此線程安全問題甚至不會出現,但它也不符合您的規范,而這些規范並沒有說明每個線程都有自己的緩存。

對線程安全的要求意味着在所有線程之間共享一個緩存,這完全排除了ThreadLocal。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM