zoukankan      html  css  js  c++  java
  • 建立高速缓存机制-java版

    前言

    ​ 一台计算机的核心是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)方法获取计算结果。

  • 相关阅读:
    Android仿网易client实现抽屉式拖拉菜单界面
    使用SRVCTL时报错:error while loading shared libraries
    permission denied for this window type
    pytest文档22-fixture详细介绍-作为参数传入,error和failed区别
    pytest文档22-fixture详细介绍-作为参数传入,error和failed区别
    pytest文档22-fixture详细介绍-作为参数传入,error和failed区别
    xml文件错误
    xml文件错误
    xml文件错误
    mysql replace into 的使用情况
  • 原文地址:https://www.cnblogs.com/process-h/p/14533597.html
Copyright © 2011-2022 走看看