【Java同時プログラミング】ConcurrentHashMap注意事項


ConcurrentHashMapは通常、HashtableやCollectionsのような他のスレッドの安全なMapコンテナを置き換えるために、より同時効率の高いMapと見なされる.synchronizedMap.実際,スレッドが安全なコンテナ,特にMapでは,アプリケーションシーンが想像していなかったことが多く,1つのトラフィックがコンテナの複数のオペレーション,すなわち複合オペレーションに関与し,同時実行時にスレッドが安全なコンテナは自身のデータが破壊されないことしか保証できないが,トラフィックの挙動が正しいかどうかは保証できない場合が多い.
例を挙げると、テキスト中の単語の出現回数を統計し、単語の出現回数を1つのMapに記録し、コードは以下の通りである.
private final Map wordCounts = new ConcurrentHashMap<>();
 
public long increase(String word) {
    Long oldValue = wordCounts.get(word);
    Long newValue = (oldValue == null) ? 1L : oldValue + 1;
    wordCounts.put(word, newValue);
    return newValue;
}

複数のスレッドがこのincrease()メソッドを同時に呼び出すと、increase()の実装は誤りである.複数のスレッドが同じwordで呼び出されると、相互の結果が上書きされ、実際に発生した回数よりも記録される回数が少なくなる可能性が高いからである.
 
この問題をロックで解決するほか、ConcurrentMapインタフェースを使用して定義する方法もあります.
public interface ConcurrentMap extendsMap {
    V putIfAbsent(K key, V value);
    boolean remove(Object key, Object value);
    boolean replace(K key, V oldValue, V newValue);
    V replace(K key, V value);
}

これは多くの人に無視されているインタフェースで、このインタフェースを誤って使う人もよく見られます.ConcurrentMapインタフェースは、CAS(Compare and Set)ベースのいくつかの操作を定義しています.簡単ですが、非常に役に立ちます.次のコードはConcurrentMapで上記の問題を解決します.
private final ConcurrentMap wordCounts = newConcurrentHashMap<>();
 
public long increase(String word) {
    Long oldValue, newValue;
    while(true) {
        oldValue = wordCounts.get(word);
        if(oldValue == null) {
            // Add the word firstly, initial the value as 1
            newValue = 1L;
            if(wordCounts.putIfAbsent(word, newValue) == null) {
                break;
            }
        }else{
            newValue = oldValue + 1;
            if(wordCounts.replace(word, oldValue, newValue)) {
                break;
            }
        }
    }
    return newValue;
}

コードは少し複雑で、主にConcurrentMapでvalueがnullの値を保存できないため、wordが存在しない場合と存在しない場合を同時に処理しなければならない.
 
上記のインプリメンテーションは、呼び出すたびにLongオブジェクトの取り外しと箱詰め操作に関連します.明らかに、より良いインプリメンテーションはAtomicLongを採用し、次はAtomicLong後のコードを採用します.
private final ConcurrentMap wordCounts = newConcurrentHashMap<>();
 
public long increase(String word) {
    AtomicLong number = wordCounts.get(word);
    if(number == null) {
        AtomicLong newNumber = newAtomicLong(0);
        number = wordCounts.putIfAbsent(word, newNumber);
        if(number == null) {
            number = newNumber;
        }
    }
    return number.incrementAndGet();
}

この実装には、現在存在しない単語を複数のスレッドで同時に追加すると、複数のnewNumberオブジェクトが生成される可能性が高いが、最終的には1つのnewNumberのみが有用であり、他のものは破棄される.このアプリケーションでは、これは問題ではなく、AtomicLongを作成するコストは高くなく、存在しない語を追加するだけで発生します.しかし、キャッシュなどのシーンを変えると、キャッシュ内のオブジェクトの取得コストが一般的に高く、通常はキャッシュが失効することが多いため、オブジェクトの重複作成を避ける価値があります.次のコードでは、この状況をどのように処理するかを示します.

private final ConcurrentMap> cache = newConcurrentHashMap<>();
 
publicExpensiveObj get(finalString key) {
    Future future = cache.get(key);
    if(future == null) {
        Callable callable = newCallable() {
            @Override
            publicExpensiveObj call() throwsException {
                return newExpensiveObj(key);
            }
        };
        FutureTask task = newFutureTask<>(callable);
 
        future = cache.putIfAbsent(key, task);
        if(future == null) {
            future = task;
            task.run();
        }
    }
 
    try{
        returnfuture.get();
    }catch(Exception e) {
        cache.remove(key);
        throw new RuntimeException(e);
    }
}

解決策は実はProxyオブジェクトで本当のオブジェクトを包装することであり、一般的なlazy loadの原理と似ている.FutureTaskを使用するのは、主に同期を保証するために、1つのProxyが複数のオブジェクトを作成しないようにするためです.上のコードの異常処理は正確ではないことに注意してください.
最後に補足します.
条件付きスレッドセキュリティ同期の集合パッケージ synchronizedMapおよび synchronizedListは、条件付きスレッドセキュリティと呼ばれる場合もある.すべての単一の操作はスレッドセキュリティであるが、複数の操作からなる操作シーケンスは、操作シーケンスにおける制御フローが前の操作の結果に依存するため、データ競合を引き起こす可能性がある.次のコードは、共通のput-if-absent文ブロックを示します.1つのエントリがMapにない場合、このエントリを追加します.残念なことに、containsKey()メソッドがput()メソッドに戻って呼び出されるまでの間、同じキーを持つ値にも別のスレッドが挿入される可能性があります.1回の挿入のみを確認するには、Map mを同期する同期ブロックで文のペアをパッケージする必要があります.
Map m = Collections.synchronizedMap(new HashMap());
List l = Collections.synchronizedList(new ArrayList());
// put-if-absent idiom -- contains a race condition
// may require external synchronization
if (!map.containsKey(key))
map.put(key, value);

信頼性の錯覚synchronizedList synchronizedMapが提供する条件付きスレッドセキュリティも、——の開発者が、これらのセットが同期されているため、スレッドが安全であると仮定し、混合操作を正確に同期することに油断する危険性をもたらします.その結果、表面的にはこれらのプログラムは負荷が軽い場合に正常に動作するが、負荷が重いとNullPointerExceptionまたはConcurrentModificationException を放出し始める.
https://blog.csdn.net/gjt19910817/article/details/47353909
https://blog.csdn.net/brandohero/article/details/39590351