• 欢迎访问开心洋葱网站,在线教程,推荐使用最新版火狐浏览器和Chrome浏览器访问本网站,欢迎加入开心洋葱 QQ群
  • 为方便开心洋葱网用户,开心洋葱官网已经开启复制功能!
  • 欢迎访问开心洋葱网站,手机也能访问哦~欢迎加入开心洋葱多维思维学习平台 QQ群
  • 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏开心洋葱吧~~~~~~~~~~~~~!
  • 由于近期流量激增,小站的ECS没能经的起亲们的访问,本站依然没有盈利,如果各位看如果觉着文字不错,还请看官给小站打个赏~~~~~~~~~~~~~!

建立高速缓存机制-java版

JAVA相关 寻找的路上 2400次浏览 0个评论

前言

​ 一台计算机的核心是CPU,它是计算机系统的运算和控制核心。由于它处理运算速度快,所以基本都会给CPU配置一级缓存,当CPU要读取一个数据时,首先从缓存中查询,如果没有在从内存或者磁盘块中找。
​ 同样的,作为一个服务器应用程序,为了让应用程序运行更快速,响应更给力,我们会给它设置一些数据缓存,这样可以提高应用程序的吞吐量、缩短客户端的响应时间。

建立缓存过程分析

​ 我们从java最常用的方案开始——一个简单的HashMap。

public interface Computable<A, V> {
    V compute(A arg) throws InterruptedException;
}
public class ExpensiveFunction implements Computable<String, BigInteger> {
    @Override
    public BigInteger compute(String arg) throws InterruptedException {
        // after deep thought...
        return new BigInteger(arg);
    }
}

​ Computable<A, V>接口描述了一个功能,输入类型是A,输出结果的类型是V。ExpensiveFunction实现了Computable。需要花比较长的时间来计算结果。所以我们计划把计算过的值都放进一个HashMap中,这样下一次有同一个A值进来时,直接获取A的计算结果。

2.1 Synchronized版

public class Memoizer1<A, V> implements Computable<A, V> {
    private final Map<A, V> cache = new HashMap<A, V>();
    private final Computable<A, V> c;
    public Memoizer1(Computable<A, V> c) {
        this.c = c;
    }
    @Override
    public synchronized V compute(A arg) throws InterruptedException {
        V result = cache.get(arg);
        if (result == null) {
            result = c.compute(arg);
            cache.put(arg, result);
        }
        return result;
    }
}

​ Memoizer1实现了第一个版本,HashMap不是线程安全的,所以使用synchronzied关键字来保证线程安全,如果cache变量中有计算结果,直接从cache取,不需要再次计算,省下许多时间。但使用synchronzied使得一次只有一个线程能够执行compute。如果一个线程正在计算结果,那其他调用compute的线程可能被阻塞很长时间,造成性能下降,这不是我们希望通过缓存得到的性能优化结果。

2.2 ConcurrentHashMap版

public class Memoizer2<A, V> implements Computable<A, V> {
    private final Map<A, V> cache = new ConcurrentHashMap<>();
    private final Computable<A, V> c;
    public Memoizer2(Computable<A, V> c) {
        this.c = c;
    }

    @Override
    public V compute(A arg) throws InterruptedException {
        V result = cache.get(arg);
        if (result == null) {
            result = c.compute(arg);
            cache.put(arg, result);
        }
        return result;
    }
}

​ Memoizer2用ConcurrentHashMap取代HashMap,改进了Memoizer1中那种糟糕的并发行为。因为ConcurrentHashMap是线程安全的,所以不需要使用Synchronized关键字,而是使用内部hash桶的分段锁机制。
​ Memoizer2与Memoizer1相比,毫无疑问具有了更好的并发性:多线程可以真正并发访问了。但是作为高速缓存仍然存在缺陷:当两个线程同时调用compute时,如果是计算同一个值,此时compute是需要很大的开销的,在一个线程还在计算中时,其它线程不知道,所以可能会重复计算。而我们希望的是:如果A线程正在计算arg,那B就不要重复计算arg,等A计算好,直接取arg对应的V就好了。

2.3 ConcurrentHashMap + FutureTask版

public class Memoizer3<A, V> implements Computable<A, V> {
    private final Map<A, Future<V>> cache = new ConcurrentHashMap<>();
    private final Computable<A, V> c;
    public Memoizer3(Computable<A, V> c) {
        this.c = c;
    }
    @Override
    public V compute(A arg) throws InterruptedException {
        Future<V> f = cache.get(arg);
        if (f == null) {
            Callable<V> eval = () -> {
                return c.compute(arg);
            };
            FutureTask<V> ft = new FutureTask<>(eval);
            f = ft;
            cache.put(arg, ft);
            ft.run(); // 调用 c.compute发生在这里
        }
        try {
            return f.get();
        } catch (ExecutionException e) {
            e.printStackTrace();
            throw new RuntimeException(e.getMessage());
        }
    }
}

​ Memoizer3为缓存的值重新定义可存储Map,用ConcurrentHashMap<A, Future >取代ConcurrentHashMap<A,V>。Memoizer3首先检查一个相应的计算是否开始,如果不是,就创建一个FutureTash,并把它注册到map中,并开始计算,如果是,那么它就会等待正在计算的结果。
​ Memoizer3的实现近乎是完美的:它展示了非常好的并发性,能很快返回已经计算过的结果,如果新到的线程请求的是其它线程正在计算的结果,它也会耐心的等待。
​ Memoizer3只有一个问题,就是仍然存在这种可能性:2个线程同时计算arg,此时由于compute中的if代码块是非原子性的复合操作,2个线程会同时进入到if代码块中,依旧会同时计算同一个arg。但ConcurrentHashMap中提供了一个原子化的putIfAbsent方法,可以消除Memoizer3的隐患。

2.4 最终版

public class Memoizer<A, V> implements Computable<A, V> {
    private final Map<A, Future<V>> cache = new ConcurrentHashMap<>();
    private final Computable<A, V> c;
    public Memoizer(Computable<A, V> c) {
        this.c = c;
    }

    @Override
    public V compute(A arg) throws InterruptedException {
        Future<V> f = cache.get(arg);
        if (f == null) {
            Callable<V> eval = () -> {
                return c.compute(arg);
            };
            FutureTask<V> ft = new FutureTask<>(eval);
            f = ft;
            cache.putIfAbsent(arg, ft);
            ft.run(); // 调用 c.compute发生在这里
        }
        try {
            return f.get();
        } catch (ExecutionException e) {
            e.printStackTrace();
            throw new RuntimeException(e.getMessage());
        }
    }
}

​ Memoizer可以说是缓存的完美实现了:支持高并发,同一个参数计算也不会重复执行(多亏于ConcurrentHashMap的putIfAbsent原子化操作)。最终调用者通过调用Future.get(arg)方法获取计算结果。


开心洋葱 , 版权所有丨如未注明 , 均为原创丨未经授权请勿修改 , 转载请注明建立高速缓存机制-java版
喜欢 (0)

您必须 登录 才能发表评论!

加载中……