Java では常にスレッドについて意識する必要がある。
Map にキャッシュデータを入れたい場合、HashMap を使ってはいけない。HashMap はスレッドセーフではないからだ。
synchronized で保護してもいいが、いちいちするのも面倒だし、問題が起きがちなんで、
Map m = Collections.synchronizedMap(new HashMap(...));
のように、Collections.synchronizedMap()
でよしなにする方法がある。この方法でラップすると、メソッドアクセスの際に synchronized がかかるので安全になる。
ConcurrentHashMap と HashMap
HashMap をマルチスレッドからアクセスする場合、get メソッドを使った場合でも synchronized をかけざるをえず、パフォーマンスが出ない。
ConcurrentHashMap ならば、取得時にはロックがかからないので、キャッシュには ConcurrentHashMap を使うことが望ましい。
Java 8 以後における ConcurrentHashMap によるキャッシュ実装
Java 8 では ConcurrentHashMap に computeIfAbsent メソッドが追加されている。これを利用すれば簡単にキャッシュを更新することが可能となる。
指定されたキーがまだ値に関連付けられていない場合、指定されたマッピング関数を使用してその値の計算を試行し、nullでない場合はそれをこのマップに入力します。メソッドの呼出し全体は原子的に実行されるため、関数はキーごとに多くても1回しか適用されません。他のスレッドがこのマップに対して試行する更新オペレーションの一部は計算の進行中にブロックされる可能性があるため、計算は短く簡単にしてください。また、計算でこのマップの他のマッピングを更新しようとしないでください。これを利用することにより、今まで記述が難しかったキャッシュ処理の記述が極めて簡単になっている。
http://docs.oracle.com/javase/jp/8/api/java/util/concurrent/ConcurrentHashMap.html#computeIfAbsent-K-java.util.function.Function-
import java.util.concurrent.ConcurrentHashMap;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class CachedMD5 {
private final ConcurrentHashMap<byte[], byte[]> cache = new ConcurrentHashMap<>();
private final MessageDigest md;
public CachedMD5() throws NoSuchAlgorithmException{
this.md = MessageDigest.getInstance("MD5");
}
public byte[] get(byte[] src) {
return cache.computeIfAbsent(src, it -> {
System.out.println("Calcurating...");
return md.digest(it);
});
}
public static void main(String[] args) throws Exception {
CachedMD5 cached = new CachedMD5();
byte[] bytesOfMessage = "hogehogefugafuga".getBytes("UTF-8");
System.out.println(cached.get(bytesOfMessage));
System.out.println(cached.get(bytesOfMessage));
}
}