Java缓存架构 guava cache
1. 简述
java cache 是用java实现的缓存工具,其中提供了高效的并发读写功能,对于缓存有两个方面非常重要,一个是缓存的线程安全特性、并发以及缓存的回收特性,今天我们就从线程安全和并发特性来来剖析guava,在分析线程安全、并发特性中,让我们自己来开发一个基于java缓存系统。
2. 实现一个具备线程安全、高并发java缓存系统
假设我们有一个JavaParserUtil类,这个类专门是负责把java文件转换为AST树,我们如何建设一个缓存系统
public class JavaParserUtil {
/**
* 将文件流转换成AST节点
*
* @param file 文件流
* @return AST节点
*/
public ParseResult<CompilationUnit> parse(File file) {
Reader reader = null;
try {
reader = new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8);
return new JavaParser().parse(reader);
} catch (ParseProblemException e) {
e.printStackTrace();
} catch (FileNotFoundException e) {
e.printStackTrace();
}
return null;
}
}
第一版
在第一版我们都能设计出这个版本,这个版本的在多线程环境下的并发度非常低,在耗费的时间中在线程堵塞在synchronized。
public class CacheOne {
private static final Map<File, ParseResult<CompilationUnit>> map = new HashMap<>();
public static synchronized ParseResult<CompilationUnit> getCompilationUnit(File file) {
ParseResult<CompilationUnit> value = map.get(file);
if (value == null) {
value = JavaParserUtil.parse(file);
map.put(file, value);
}
return value;
}
}
第二版
为了减少第一版的锁的消耗,我们可以使用双重检查机制来减少锁的消耗,关于双重检查请读者自己查阅资料,在这版使多线程的竞争条件集中在传入同一个file的不同的线程会产生线程阻塞。比上一个版本,所有file都会产生等待,优化了部分的锁消耗时间。
public class CacheTwo {
private static final Map<File, ParseResult<CompilationUnit>> map = new HashMap<>();
public static ParseResult<CompilationUnit> getCompilationUnit(File file) {
ParseResult<CompilationUnit> value = map.get(file);
if (value == null) {
synchronized (CacheTwo.class) {
value = map.get(file);
if (value == null) {
value = JavaParserUtil.parse(file);
map.put(file, value);
}
}
}
return value;
}
}
第三版
从第二版,我们可以看到一个问题,在线程给map设置了value,一个线程从map获取这个value,我们知道由于内存的可见性,这个线程可能不能及时看到value,导致在此获取锁,从而浪费了时间。我们用第三版来解决这个问题。
public class CacheThree {
private static final Map<File, ParseResult<CompilationUnit>> map = new HashMap<>();
private static volatile int num = 0;
public static ParseResult<CompilationUnit> getCompilationUnit(File file) {
ParseResult<CompilationUnit> value = null;
// 获取num的值,由于num是volatile,可以保证第二个线程看到上个线程修改的值
if (num > 0) {
value = getValue(file);
} else {
value = getValue(file);
}
return value;
}
private static ParseResult<CompilationUnit> getValue(File file){
ParseResult<CompilationUnit> value = map.get(file);
if (value == null) {
synchronized (CacheThree.class) {
value = map.get(file);
if (value == null) {
value = JavaParserUtil.parse(file);
map.put(file, value);
num++;
}
}
}
return value;
}
}
我们保证线程进来都能够读取num值,由于num值是volatile,其他线程可以看到map的最新值。
第四版
从前面三版的优化,我们可以发现吧map替换成ConcurrentHashMap可以满足线程可见性的问题,所以修改后如下。
public class CacheFourth {
private static final ConcurrentHashMap<File, ParseResult<CompilationUnit>> map = new ConcurrentHashMap<>();
public static ParseResult<CompilationUnit> getCompilationUnit(File file) {
return map.computeIfAbsent(file, kv -> JavaParserUtil.parse(file));
}
}
第五版
这一版已经能够满足高并发,线程安全的特性,但是会带来重复检查的问题,由于JavaParserUtil.parse(file)是会耗时的,我们可以使用FutureTask,来保证都有值,来解决重复计算的问题。
public class CacheFive {
private static final ConcurrentHashMap<File, FutureTask<ParseResult<CompilationUnit>>> map = new ConcurrentHashMap<>();
public static FutureTask<ParseResult<CompilationUnit>> getCompilationUnit(File file) {
return map.computeIfAbsent(file, kv -> new FutureTask<>(() -> {
return JavaParserUtil.parse(file);
}));
}
}
最终我们得到了最终的版本。
3 guava cache线程安全源码分析
我们首先从get方法看起,首先看count != 0,这个实现思路就是和我们版本3的思路一致,都是通过volatile变量来达到value可见能力。
V get(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {
checkNotNull(key);
checkNotNull(loader);
try {
if (count != 0) { // read-volatile
// don't call getLiveEntry, which would ignore loading values
ReferenceEntry<K, V> e = getEntry(key, hash);
if (e != null) {
long now = map.ticker.read();
V value = getLiveValue(e, now);
if (value != null) {
recordRead(e, now);
statsCounter.recordHits(1);
return scheduleRefresh(e, key, hash, value, now, loader);
}
ValueReference<K, V> valueReference = e.getValueReference();
if (valueReference.isLoading()) {
return waitForLoadingValue(e, key, valueReference);
}
}
}
// at this point e is either null or expired;
return lockedGetOrLoad(key, hash, loader);
} catch (ExecutionException ee) {
Throwable cause = ee.getCause();
if (cause instanceof Error) {
throw new ExecutionError((Error) cause);
} else if (cause instanceof RuntimeException) {
throw new UncheckedExecutionException(cause);
}
throw ee;
} finally {
postReadCleanup();
}
}
从源码上看,它也是创建一个中间对象也就是LoadingValueReference,类似我们自己实现的FutureTask,这部分执行时在锁环境下的,其实就类似于我们第5版的核心解决思路。