LSFシリーズ-計算結果を1つのMapにキャッシュする
6320 ワード
概要
本文の主体的な内容は,たぶん簡単な需要から来ている.
と書く
計算結果を1つのMapにキャッシュする
そして、対応する同時問題、高効率伸縮性、キャッシュ汚染問題を分析し、最後に我々が満足できるMapを実現する.
最も簡単な実装
まず、最も簡単な実現を置く.私は信じて、大部分の人(私を含む)を含めて、大部分の情況はこのように実現しました..
package current;
import java.util.HashMap;
public class Foo {
private HashMap<String,String> dataCache = new HashMap<String, String>();
public String getData(String key){
String data = dataCache.get(key);
if(data == null){
String newData = fetchData(key);
dataCache.put(key,data);
return newData;
} else {
return data;
}
}
/**
*
*/
private String fetchData(String key){
return key + ":data";
}
}
実は、このような実現は基本的に高い合併の大部分の情況を満たすことができます..もちろん、私たちとしては、絶対にここまではしません.ほとんどの人が問題を発見したと信じています
と書く
HashMapはスレッドが安全ではない.
解決策は2つあります
と書く
1 getDataメソッドを同期する.synchronizedキーを付ける
2 HashMapをConcurrentHashMapに変更します.
一般的には、JDKが持参した同期容器ConcurrentHashMapを使用すると信じられています.
と書く
ここでは、ConcurrentHashMapが独自のアクセスロックを実現していないことを明確にしなければならない.だから、もしあなたがこの方面の必要があるならば、synchronizedを使うのはあなたの唯一の選択です.
以下は改良後のコードです
package current;
import java.util.concurrent.ConcurrentHashMap;
public class Foo {
private ConcurrentHashMap<String,String> dataCache = new ConcurrentHashMap<String, String>();
public String getData(String key){
String data = dataCache.get(key);
if(data == null){
String newData = fetchData(key);
dataCache.put(key,data);
return newData;
} else {
return data;
}
}
/**
*
*/
private String fetchData(String key){
return key + ":data";
}
}
ここまで、终わったのかな..高同時性の場合、同じkeyを2回初期化する可能性がある(キャッシュオブジェクトの一部が1回しか初期化できない場合、この脆弱性は大きなセキュリティリスクをもたらす).この問題に対して,FutureTaskを実現するための閉鎖を導入することができる.
FutureTaskを用いて2回初期化の問題を解決する.
対応するコードは以下の通りです.
package current;
import java.util.concurrent.*;
public class Foo {
private ConcurrentHashMap<String,FutureTask<String>> dataCache = new ConcurrentHashMap<String, FutureTask<String>>();
public String getData(final String key){
FutureTask<String> future = dataCache.get(key);
if(future == null){
future = new FutureTask<String>(new Callable<String>() {
@Override
public String call() throws Exception {
return fetchData(key);
}
});
dataCache.put(key,future);
future.run();
}
try {
return future.get();
} catch (InterruptedException e) {
// , futuretask , future.run ,
dataCache.remove(key);
} catch (ExecutionException e) {}
return null;
}
/**
*
*/
private String fetchData(String key){
return key + ":data";
}
}
このコードは、とても完璧に見えます.他のスレッドが前のリクエストがfetchDataにあるのを見た場合もrunの終了を待つ.でも、まだ終わってない.同期できなかった場所がある
FutureTask<String> future = dataCache.get(key);
if(future == null){
future = new FutureTask<String>(new Callable<String>() {
@Override
public String call() throws Exception {
return fetchData(key);
}
});
dataCache.put(key,future);
高同時では、同じkeyのfutureが2回作成され、それぞれputがcacheに上書きする.ここの非同期は以前と同じであることに注意してください.
String data = dataCache.get(key);
if(data == null){
String newData = fetchData(key);
dataCache.put(key,data);
このコードの非同期には非常に本質的な違いがある.後者はfetchDataが完了するまでputであるが、前者はcacheにfutureを登録するだけである.そしてすぐにputしました.両者の時間消費は全く異なる.
上記の複合操作(実はput if absent)を解決するために、解決方法は非常に簡単で、ConcurrentHashMapが持参したputIfAbsent法を用いる.
最終バージョン
package current;
import java.util.concurrent.*;
public class Foo {
private ConcurrentHashMap<String,FutureTask<String>> dataCache = new ConcurrentHashMap<String, FutureTask<String>>();
public String getData(final String key){
FutureTask<String> future = dataCache.get(key);
if(future == null){
future = new FutureTask<String>(new Callable<String>() {
@Override
public String call() throws Exception {
return fetchData(key);
}
});
FutureTask old = dataCache.putIfAbsent(key,future);
if(old == null){
future.run();
} else {
future = old;
}
}
try {
return future.get();
} catch (InterruptedException e) {
// , futuretask , future.run ,
dataCache.remove(key);
} catch (ExecutionException e) {}
return null;
}
/**
*
*/
private String fetchData(String key){
return key + ":data";
}
}
getDataメソッドにwhile(true)ループを追加するところもあるようです.fetchDataにエラーが発生した場合は、取得を続行します.これはあまりにも覇気的だ.これはもう悪くないと思います.99.99%のニーズを満たすことができます
まとめ
実は、ほとんどの場合、データをキャッシュするだけで、ConcurrentHashMapのレベルになると思います.futureを導入するのは非常に少ない.これは自分のシステムの状況を見てみましょう.