ConcurrentHashMap(スレッド安全な Map) — マルチスレッド環境
並行処理で「キー→値」を共有したいなら、まずは ConcurrentHashMap。ロックを最小限に抑えた設計で、高い並行性と安定性を両立します。初心者でも安全に使えるポイントとコード例をまとめます。
特性と注意点
- スレッド安全: 複数スレッドから同時に put/get/remove しても内部構造が壊れない。
- 高速性: 取得(get)は基本ノンブロッキング、更新(put/remove)は必要な箇所だけ局所ロック。
- イテレータの性質: 走査は「弱一貫性」。並行更新中でも例外なく進むが、最新が全部見えるとは限らない。
- null 非許容: キーも値も null を入れられない(HashMap と違う点)。
- 順序非保証: 挿入順やソート順は持たない(必要なら LinkedHashMap/TreeMap を用途に応じて選ぶ)。
- キーの不変性: equals/hashCode が安定する不変オブジェクトをキーにするのが鉄則。
基本コード例(入門)
import java.util.concurrent.ConcurrentHashMap;
public class CHMBasics {
public static void main(String[] args) {
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
// 追加・取得
map.put("A001", "Apple");
System.out.println(map.get("A001")); // Apple
// 存在確認
System.out.println(map.containsKey("A001")); // true
// 削除
map.remove("A001");
System.out.println(map.get("A001")); // null
// null は不可(例外になる)
// map.put("K", null); // NG
}
}
Java- ポイント: 使い方は Map と同じ感覚。null は入れない、順序は期待しない。
原子操作(同時更新でも安全なビルディングブロック)
- putIfAbsent: ないときだけ追加(重複作成を防ぐ)
map.putIfAbsent(key, value);
Java- computeIfAbsent: ないときだけ計算して入れる(オンデマンド生成)
map.computeIfAbsent(key, k -> heavyCreate(k));
Java- compute / computeIfPresent: 値を計算で更新(読み→書きの競合をひと塊に)
map.compute(key, (k, v) -> v == null ? initValue() : update(v));
Java- merge: 既存値とマージ(カウンタや集合マージに便利)
map.merge(key, 1, Integer::sum); // カウンタ加算
Java- replace / remove(key, value): 期待値一致時だけ更新・削除(CAS 的な安全更新)
map.replace(key, oldValue, newValue);
map.remove(key, expectedValue);
Java- forEach(並行走査): 位置づけは弱一貫性。大規模でも安全に回せる
map.forEach(1, (k, v) -> process(k, v)); // 並列度ヒント=1(実装依存)
Java例題1: スレッド安全なメモ化キャッシュ(computeIfAbsent)
import java.util.concurrent.ConcurrentHashMap;
public class Memoizer<K, V> {
private final ConcurrentHashMap<K, V> cache = new ConcurrentHashMap<>();
public V getOrCompute(K key, java.util.function.Function<K, V> fn) {
return cache.computeIfAbsent(key, fn); // ないときだけ一度だけ計算
}
public static void main(String[] args) {
Memoizer<String, String> memo = new Memoizer<>();
String v = memo.getOrCompute("user:42", k -> loadFromDb(k)); // 同時呼び出しでも重複計算なし
System.out.println(v);
}
static String loadFromDb(String k) {
// 重い処理の代わり
return "DATA:" + k;
}
}
Java- ポイント: マルチスレッドで同じキーを同時に要求しても「一度だけ」計算される。
例題2: カウンタ(merge と LongAdder)
シンプルなカウンタ(merge)
ConcurrentHashMap<String, Integer> counts = new ConcurrentHashMap<>();
void inc(String key) {
counts.merge(key, 1, Integer::sum); // 原子に加算
}
Java高頻度加算に強い設計(LongAdder 併用)
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.LongAdder;
ConcurrentHashMap<String, LongAdder> adderMap = new ConcurrentHashMap<>();
void incFast(String key) {
adderMap.computeIfAbsent(key, k -> new LongAdder()).increment();
}
long sum(String key) {
LongAdder la = adderMap.get(key);
return la == null ? 0 : la.sum();
}
Java- ポイント: 激しい競合下では LongAdder の分散カウンタが有利。
走査と並行更新の扱い
- 弱一貫性イテレータ: 走査中に追加・削除があっても例外にならず進む。ただし「すべての更新が必ず見える」わけではない。
- サイズや完了保証: size()、isEmpty() は近似的。走査で「n件ちょうど」などの強い前提を置かない。
- スナップショットが必要なら: 適切なタイミングでコピー(例: new HashMap<>(chm))してから扱う。
実務での選定指針
- ConcurrentHashMap を選ぶ場面:
- 複数スレッドから頻繁に読み書きする共有辞書。
- キャッシュやセッション状態の管理。
- 集計・カウンタなどの高並行更新。
- 他の選択肢との比較:
- Collections.synchronizedMap: 全操作を大域ロック → 並行性が低く遅くなりがち。
- Immutable(不変)構造+差し替え: 読み取り中心で更新まれなら有効(CopyOnWrite 的設計)。
- ConcurrentSkipListMap: 順序・範囲検索が必要なとき(O(log n))。
テンプレート集
- 基本宣言
ConcurrentHashMap<Key, Value> map = new ConcurrentHashMap<>();
Java- 存在しないときだけ作る
Value v = map.computeIfAbsent(key, k -> create(k));
Java- 期待値一致で更新/削除(楽観的制御)
boolean ok = map.replace(key, oldVal, newVal);
boolean removed = map.remove(key, expectedVal);
Java- 安全な加算(競合に強い)
map.merge(key, 1, Integer::sum);
Java- ビジネスロジックをひと塊で更新
map.compute(key, (k, v) -> v == null ? start() : advance(v));
Javaよくある落とし穴と回避策
- 落とし穴: null を入れようとして例外。
- 回避: null を使わず、欠損は「値なし(get が null)」で表現する。
- 落とし穴: get→加工→put の別々操作で競合。
- 回避: compute/merge を使って「読み・計算・書き」を原子にまとめる。
- 落とし穴: キーの equals/hashCode が可変。
- 回避: 不変キーを使用。レコードや不変クラスで設計。
- 落とし穴: 走査の完全性を期待(「必ず全件見る」前提)。
- 回避: 弱一貫性を理解。必要ならスナップショットコピーして処理。
- 落とし穴: 順序や LRU を期待。
- 回避: CHM は順序なし。LRU は LinkedHashMap を使う等、用途に応じて構成。
まとめ
- ConcurrentHashMap はマルチスレッドで「速くて安全」な Map。取得は基本ノンブロッキング、更新は原子操作を活用して競合を最小化する。
- null 非許容・弱一貫性イテレータなどの性質を理解し、compute/merge/putIfAbsent で「同時更新でも壊れない」ロジックにする。
- キーは不変、完全性が必要ならスナップショット。用途に応じて他の Map と使い分ける。
