各キーごとにシングルトンとなるキャシュを保持するマップの実装
TODO
- 説明を書く
- サンプルコードを書く
- サンプルコードの実行結果を貼る
- テストコードを書く
参考
コード
CacheMap.java
package heignamerican.mt;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Supplier;
public class CacheMap {
/**
* キーごとにシングルトンである値を生成してキャッシュを返す。 マルチスレッドセーフ
*
* @param key
* キー
* @param other
* SingleHolder が {@code key} 以外のコンストラクタを保つ場合の例示
* @return
* キーごとの値
*/
public static Heavy get(final String key, final String other) {
// XXX ここは、computeIfAbsent するか putIfAbsent → get するかでちょっとだけ性能特性が異なる
return cacheMap.computeIfAbsent(key, (_key) -> new SingleHolder(key, other)).get();
}
private static final ConcurrentMap<String, SingleHolder> cacheMap = new ConcurrentHashMap<>();
/**
* <p>
* Singleton のキャッシュを一度だけ生成して返す。
*
* <p>
* {@link Heavy} 専用で、このクラスのインスタンス 1 に対して、{@link Heavy} のキャッシュを 1 件だけ持つ。<br>
* 交換可能にするのはちょっとめんどそう。 特に「コンストラクタの引数 > 一意性を表現するインスタンス群」 の場合。<br>
*
*/
private static class SingleHolder {
private final String key;
private final String other;
private SingleHolder(final String key, final String other) {
this.key = key;
this.other = other;
}
private Heavy get() {
return innerSupplier.get();
}
private Supplier<Heavy> innerSupplier = () -> createAndCache();
private synchronized Heavy createAndCache() {
class HeavyFactory implements Supplier<Heavy> {
private final Heavy instance = new Heavy(key, other);
@Override
public Heavy get() {
return instance;
}
}
if (!HeavyFactory.class.isInstance(innerSupplier)) {
innerSupplier = new HeavyFactory();
}
return innerSupplier.get();
}
}
}
Heavy.java
package heignamerican.mt;
/**
* {@link #key} に対して唯一のシングルトンインスタンスとしてキャッシュしたいクラスの例。
*/
public class Heavy {
private final String key;
private final String other;
public Heavy(final String key, final String other) {
this.key = key;
this.other = other;
}
@Override
public String toString() {
return "Heavy [key=" + key + ", other=" + other + "]";
}
}
サンプルコードと説明
MT 実行での挙動確認のため、余計なコードを加えたバージョン
CacheMap の以下の2箇所を書き換えることで、性能特性の違いを確認できます。
- ※1 computeIfAbsent するか、putIfAbsent して get するか
- ※2 SingleHolder初期化時の遅延の有無
ざっくり言うと、computeIfAbsent は初回生成時のみ synchronized に近い挙動で完全にブロックします。
同一の key(name) に対してのみブロックしてくれたら最適だったのですが
そう都合よいものではないようです。
putIfAbsent の方は、SingleHolder をたくさん作って捨てる(可能性がある)のですが、ブロックは短くなります。
(まあ SingleHolder による Heavy インスタンス生成遅延があるので大した差はないと思いますが…)
※2 は ※1 の違いを明確に出すための書き換えで、プロダクトコードでの選択肢は ※1 の差異だけです。
以下サンプルコード
CacheMap.java
package heignamerican.mt.poc;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Supplier;
import heignamerican.mt.poc.Heavy.Key;
public class CacheMap {
public static Heavy get(final Key key, final AutoCloseable connection) {
// ※1
//cacheMap.putIfAbsent(key.name, new SingleHolder(key, connection));
//return cacheMap.get(key.name).get();
return cacheMap.computeIfAbsent(key.name, (_key) -> new SingleHolder(key, connection)).get();
}
private static final ConcurrentMap<String, SingleHolder> cacheMap = new ConcurrentHashMap<>();
private static class SingleHolder {
private final Key key;
private AutoCloseable connection;
public SingleHolder(final Key key, final AutoCloseable connection) {
this.key = key;
this.connection = connection;
// ※2
//SampleProgram.log(key, "waiting for SingletonHolder");
//SampleProgram.sleeping(1);
SampleProgram.log(key, "SingleHolder created.");
}
private Heavy get() {
return innerSupplier.get();
}
private Supplier<Heavy> innerSupplier = () -> createAndCache();
private synchronized Heavy createAndCache() {
class HeavyFactory implements Supplier<Heavy> {
private final Heavy instance = new Heavy(key, connection);
@Override
public Heavy get() {
return instance;
}
}
if (!HeavyFactory.class.isInstance(innerSupplier)) {
innerSupplier = new HeavyFactory();
} else {
SampleProgram.log(key, "Heavy already exists.");
}
connection = null;
return innerSupplier.get();
}
}
}
Heavy.java
package heignamerican.mt.poc;
public class Heavy {
/*
* 別にキーは String でも良いのですが、ここでは MT での挙動確認のため番号も付けています
*/
public final Key key;
public Heavy(final Key key, final AutoCloseable connection) {
this.key = key;
Sample.log(key, "waiting for Heavy");
Sample.sleeping(2);
// connection からデータを大量に取得し、メモリをふんだんに使用する類のやつ
Sample.log(key, "Heavy created.");
}
public static class Key {
public final int number;
public final String name;
public Key(int number, String name) {
this.number = number;
this.name = name;
}
@Override
public String toString() {
return "Key[" + number + "," + name + "]";
}
}
}
SampleProgram.java
package heignamerican.mt.poc;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
import heignamerican.mt.poc.Heavy.Key;
public class SampleProgram {
public static void main(String[] args) {
final List<Key> keys = withIndexPlus("aa", "aa", "aa", "aa", "bb", "bb", "cc", "cc", "aa", "bb", "cc");
System.out.println(keys);
System.out.println("✄====================✄");
final ExecutorService service = Executors.newFixedThreadPool(8);
for (Key key : keys) {
service.execute(() -> {
log(key, "start.");
CacheMap.get(key, null);
});
}
service.shutdown();
}
private static List<Key> withIndexPlus(final String... names) {
return IntStream.range(0, names.length)
.mapToObj(i -> new Key(i + 1, names[i]))
.collect(Collectors.toList());
}
private static final long start = System.currentTimeMillis();
public static void log(Key key, String message) {
System.out.printf("%5d(ms) %s [%2d,%s] ... %s%n",
System.currentTimeMillis() - start, // 経過ミリ秒
Thread.currentThread().getName(), // 実行スレッド名
key.number, key.name, // この処理で使用した key
message);
}
public static void sleeping(int sec) {
try {
Thread.sleep(sec * 1000);
} catch (InterruptedException e) {
}
}
}