java实现缓存,解析

1 篇文章 0 订阅
1 篇文章 0 订阅

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版的核心解决思路。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值