ConcurrentHashMapの使い方


日常の開発では、リソースプールはよく出会うシーンであり、簡単な実現は必要に応じてリソースを作成し、mapにキャッシュし、その後、このリソースを使用するときに直接mapから取得することである.
最も単純で信頼性の高い実装はHashedMap+synchronized(またはLock)を利用することである.
この方式は間違いなく正しいが,ロックの粒度が大きく,高同時性では性能が悪い.
改善の1つの典型的な考え方はJUCの中のコンカレントツールConcurrentHashMapを利用して、ロックの粒度を下げて、コンカレント性を高めます
http://dmy999.com/article/34/correct-use-of-concurrenthashmapでは、主なコードは次のとおりです.

private ConcurrentMap records = new ConcurrentHashMap();

private Record getOrCreate(String id) {
    Record rec = records.get(id);
    if (rec == null) {
        // record does not yet exist
        Record newRec = new Record(id);
        rec = records.putIfAbsent(id, newRec);
        if (rec == null) {
            // put succeeded, use new value
            rec = newRec;
        }
    }
    return rec;
}

この方法は直接ConcurrentMapを使用し,外層のロックはなく,
putIfAbsentメソッドは同じkeyのオブジェクトを繰り返し入れることはなく、
ConcurrentMapの内部実装もメモリの可視性を保証する.
基本的に正しいように見えます.ほとんどの場合も正常に動作する.
しかし、この書き方には致命的な問題があります.それは
Record newRec = new Record(id);
この場所は同時の場合に複数回実行する可能性がある.リソースが複数回作成することを意味する.
このリソースに外部依存性がない場合は、複数回作成しても問題ありませんが、
外部リソース(ファイル,socket)に依存する場合、複数回作成するとエラーが発生する.
このようなリソースの複数回の作成を避けるにはdouble checkedを用いて実現することができる.
double checkedは古くから有名なdouble checked locking問題がありますが
この問題については、
http://en.wikipedia.org/wiki/Double-checked_locking
)の発生は主にjavaメモリモデルによるものですが、volatileを使用するとlockの問題を回避できます.
以前は多くの著作でvolatileの代価が高すぎると言及されていたかもしれませんが、jvmの発展(1.5以降)、volatileの実現に伴い
軽量で効率的になるため、具体的な説明は
http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#dcl
ページに切り捨てられた大きな論述に注意する
Double checkedは最も早く単例モードを解決するために生成され、現在javaの単例モードはすでにより良い
実装形態、例えばstatic変数(dclのwiki記述)、例えばenum単例(effective java 2 nd参照)
しかし、リソースプールシーンではDouble checkedを使用して、リソースが一度だけ作成されることを保証します.
同時に同期のコストを最小限に抑える
コードは次のとおりです.

private ConcurrentMap records = new ConcurrentHashMap();

private Record getOrCreate(String id) {
    Record rec = records.get(id);
    if (rec == null) {
        synchronized (this) {
            rec = records.get(id);
            if (rec == null) {
                // record does not yet exist
                Record newRec = new Record(id);
                rec = records.putIfAbsent(id, newRec);
                if (rec == null) {
                    // put succeeded, use new value
                   rec = newRec;
                }
            }
        }
    }
    return rec;
}

注意ここではsychronizedを使用していますが、ConcurrentHashMapの代わりにHashedMapを使用することはできません.
この書き方はマルチスレッドがMapのgetとputメソッドに同時にアクセスする可能性があるので、
HashedMapのgetとputメソッドの同時環境でもエラーが発生する.