• 优雅的缓存写法,以及synchronized 和 ReentrantLock性能 PK


    1、在平常开发中,经常有同步问题,特别是做缓存的时候,仅仅作为一个合格的java程序员缓存是一定用的很多的,并且绝大部分也都会用到同步,否则就不是一个合格的java程序员。

    1、缓存代码优化,我们开发中常见的写法有如下几种情况。

    准备代码

    public class LocalCacheTool {
    
        /**
         * 获取5分钟缓存实例
         */
        public static final CaffeineCache fiveMinCache() {
            return CacheHolder.FIVE_MIN_CACHE;
        }
    
        /**
         * 获取30分钟缓存实例
         * @return
         */
        public static final CaffeineCache thirtyMinCache() {
            return CacheHolder.THIRTY_MIN_CACHE;
        }
    
        /**
         * 缓存持有者,内部类
         * @author Administrator
         *
         */
        private static final class CacheHolder {
            private CacheHolder() {
            }
            /**
             *  5分钟缓存(可以理解就是一个map)
             */
            private static final CaffeineCache FIVE_MIN_CACHE = new CaffeineCache("fiveMinCache",
                    Caffeine.newBuilder().maximumSize(5000).expireAfterWrite(5, TimeUnit.MINUTES).build());
            
            /**
             * 30分钟缓存 (可以理解就是一个map)
             */
            private static final CaffeineCache THIRTY_MIN_CACHE = new CaffeineCache("thirtyMinCache",
                    Caffeine.newBuilder().maximumSize(5000).expireAfterWrite(30, TimeUnit.MINUTES).build());
        }

      
    private static final Object CACHE_USER_LOCK="lock:cache:five_min";
      
    private static final Object FIVE_MIN_LOCK="lock:cache:five_min"
    /**
    * 构建锁
    * @param lockId
    * @return
    */
    public static Object buildLock(String lockId) {
        return lockId.intern();
    }

        public static enum TimeOut {
            /**
             * 5分钟
             */
            FIVE_MIN(LocalCacheTool.fiveMinCache()),
    
            /**
             * 30分钟
             */
            THIRTY_MIN(LocalCacheTool.thirtyMinCache());
    
            private CaffeineCache cache;
    
            private TimeOut(CaffeineCache cache) {
                this.cache = cache;
            }
        }

    }

    1.1、低级程序员和挖坑程序员的写法

    从缓存获取用户

        public static Object getUserForCache() {
            Object user=fiveMinCache().get("cache_user");
            if(user!=null) {
                return user;
            }
            user=userMapper.selectUserByid(1);
            fiveMinCache().put("cache_user", user);
            return user;
        }

    1.2、还是低级程序员的写法

    从缓存获取用户

    public static Object getUserForCache() {
            Object user=fiveMinCache().get("cache_user");
            if(user!=null) {
                return user;
            }
            synchronized(CACHE_USER_LOCK) {
                //获取user逻辑
                user=userMapper.selectUserByid(1);
                fiveMinCache().put("cache_user", user);
            }
            return user;
        }
        

    1.3、中级程序员的写法

    public static Object getUserForCache() {
            Object user=fiveMinCache().get("cache_user");
            if(user!=null) {
                return user;
            }
            synchronized(CACHE_USER_LOCK) {
               user=fiveMinCache().get("cache_user"); 
                if(user!=null) { // 双重校验,防止重复调用下面的获取数据逻辑
                    return user;
                }
                //获取user逻辑
                user=userMapper.selectUserByid(1);
                fiveMinCache().put("cache_user", user);
            }
            return user;
        }

    1.4、良好程序员的写法

        /**
         * 采用模板方法设计模式,先封装一个通用的缓存方法,简化大部分重复代码
         * @param <T>
         * @param key
         * @param timeOut
         * @param supplier
         * @return
         */
        @SuppressWarnings("unchecked")
        public static <T> T syncComputeIfAbsent(String key, TimeOut timeOut, Supplier<T> supplier) {
            ValueWrapper v = timeOut.cache.get(key);
            if (v != null) {
                return (T) v.get();
            }
            synchronized (buildLock(key)) {
                v = timeOut.cache.get(key);
                if (v != null) {
                    return (T) v.get();
                }
                T data = supplier.get();
                fiveMinCache().putIfAbsent(key, data);
                return data;
            }
        }
      
         // 使用上面通用的的模板方法,
      //从缓存获取用户,从上面的低级程序员和中级程序员的约 10行 缩减到 1行 代码,还保证数据的安全一致性
    public static Object getUserForCache() { return syncComputeIfAbsent(LOCK_CACHE_USER, fiveMinCache(), ()->userMapper.selectUserByid(1)); }

    以上程序中使用到的都是 synchronized,用法比较简单好理解,还有人用 ReentrantReadWriteLock,说它们性能更好,下面做了一些性能测试

    2、synchronized 和 ReentrantLock 性能测试

    2.1、synchronized写法1  

      独立锁,一个缓存数据一把锁,动态构建锁

        @SuppressWarnings("unchecked")
        public static <T> T syncComputeIfAbsent(String key, TimeOut timeOut, Supplier<T> supplier) {
            ValueWrapper v = timeOut.cache.get(key);
            if (v != null) {
                return (T) v.get();
            }
            synchronized (buildLock(key)) {
                v = timeOut.cache.get(key);
                if (v != null) {
                    return (T) v.get();
                }
                T data = supplier.get();
                fiveMinCache().putIfAbsent(key, data);
                return data;
            }
        }
        

    2.2、synchronized写法2   

      所有缓存数据共用一把锁

        private static final Object FIVE_MIN_LOCK="lock:cache:five_min";
        @SuppressWarnings("unchecked")
        public static <T> T syncOneLockComputeIfAbsent(String key, TimeOut timeOut, Supplier<T> supplier) {
            ValueWrapper v = timeOut.cache.get(key);
            if (v != null) {
                return (T) v.get();
            }
            synchronized (FIVE_MIN_LOCK) {
                v = timeOut.cache.get(key);
                if (v != null) {
                    return (T) v.get();
                }
                T data = supplier.get();
                fiveMinCache().putIfAbsent(key, data);
                return data;
            }
        }

    2.3、 ReentrantReadWriteLock 写法 

        static final ReentrantReadWriteLock RWLOCK = new ReentrantReadWriteLock();
    
        public static <T> T casComputeIfAbsent(String key, TimeOut timeOut, Supplier<T> supplier) {
            ValueWrapper v = null;
            try {
                RWLOCK.readLock().lock();
                v = timeOut.cache.get(key);
                if (v != null) {
                    return (T) v.get();
                }
    
                try {
                    RWLOCK.readLock().unlock();// 在开始写之前,首先要释放读锁,否则写锁无法拿到
                    RWLOCK.writeLock().lock();// 获取写锁开始写数据
                    /*
                     * 再次判断该值是否为空,因为如果两个写线程如果都阻塞在这里,当一个线程 被唤醒后value的值不为null,当另外一个线程也被唤醒如果不判断就会执行两次写
                     */
                    T data = null;
                    if (v == null) {
                        data = supplier.get();
                        fiveMinCache().putIfAbsent(key, data);
                    }
                    RWLOCK.readLock().lock();// 写完之后重入降级为读锁 
                    return data;
                } finally {
                    RWLOCK.writeLock().unlock();// 最后释放写锁
                }
            } finally {
                RWLOCK.readLock().unlock();
            }
    
        }

    2.4、synchronized 和 ReentrantReadWriteLock 性能测试

        public static void main(String[] args) throws InterruptedException {
    
            ExecutorService excuts = Executors.newFixedThreadPool(50);
    
            final long st=System.currentTimeMillis();
            int r=300;//多少圈 ,多少重复请求 key
            int n=10000;//多少个key,
            AtomicInteger count=new AtomicInteger(r*n);
            for (int a = 1; a < (r+1); a++) {
                for (int i = 1; i < (n+1); i++) {
                    excuts.execute(() -> {
                        int c=count.decrementAndGet();
                        
                        /*
                         *  50线程,1000000 请求,耗时:1627 1616 1613
                         * 200线程,1000000 请求,耗时:1616 1613 1660
                         *  50线程,3000000 请求,耗时:4126 4098 4196
                         * 200线程,3000000 请求,耗时:4204 4284 4244
                         * 
                         */
    //                    syncComputeIfAbsent("testcache" + c, TimeOut.FIVE_MIN, () -> "aksdjaldkj");
                        
                        /*
                         * 
                         *   50线程,1000000 请求,耗时:1560 1517 1517
                         *  200线程,1000000 请求,耗时:1600 1691 1653
                         *   50线程,3000000 请求,耗时:3998 4084 4063
                         *  200线程,3000000 请求,耗时:4085 4043 4106
                         * 
                         */
    //                    syncOneLockComputeIfAbsent("testcache" + c, TimeOut.FIVE_MIN, () -> "aksdjaldkj");
                        /*
                         *  50线程,1000000 请求,耗时:4193  4377 4325 4183 4215
                         * 200线程,1000000 请求,耗时:4418  4461 4539 4425 4383 4373 4510 4416
                         * 500线程,1000000 请求,耗时:4555  4496 4448 4351 4466 4361 4412 4493
                         *  50线程,3000000 请求,耗时:11597  11505 11898
                         */
                        casComputeIfAbsent("testcache" + c, TimeOut.FIVE_MIN, () -> "aksdjaldkj");
                        
                        
                        TimeOut.FIVE_MIN.cache.get("testcache" + c);
                        if(c==0) {
                            long en=System.currentTimeMillis();
                            System.out.println("---------sync耗时:"+(en-st));
                        } 
                    });
                }
            }
            
        }

    简单总结:

    1、代码优雅的写法,在工作中的好处有多大:

    代码大大简化、使用时大大降低逻辑复杂度,使用时只需要知道是用缓存获取数据和给缓存提供数据,至于怎么缓存的,以及线程安全问题......不需要去管

    2、synchronized 和 ReentrantLock性能测试结果

    至少在上面的例子中:

    synchronized 完胜 ReentrantLock,为什么呢?这个是必须要理解 他们各自的基本原理的,其实 synchronized 在jdk7 还是 8 开始进行优化后包含了锁的升级过程中有一段就就类似ReentrantLock的机制......

    3、留下的疑问和思考

      3.1、synchronized写法1独立锁 和 写法2固定锁 性能为什么差不多?这里的测试其实是测不出来的,先留点疑问在这,呵呵

      3.4、代码优雅的写法 和 锁的优化使用 已经到极致了吗?还能继续优化吗?比如在 获取数据时,这个地方考虑异步,甚至还有其他方案........ ,至少我心里肯定有的,但是在目前市场上还没有发现实现的框架或工具(异步的有)

    以上涉及的内容和留下的问题还是挺多的,1是时间问题,2是内容比较多,3是留点悬念,4是欢迎各位朋友前来咨询或者讨论学习

  • 相关阅读:
    Oracle DB 使用单行函数定制输出
    NDK编译多个cpp
    使用NDK编译的时候出现 undefined reference to
    linux SSSocket 简单封装
    OCP-1Z0-051-V9.02-70题
    OCP-1Z0-051-V9.02-69题
    OCP-1Z0-051-V9.02-68题
    OCP-1Z0-051-V9.02-67题
    OCP-1Z0-051-V9.02-66题
    OCP-1Z0-051-V9.02-65题
  • 原文地址:https://www.cnblogs.com/abab/p/13719499.html
Copyright © 2020-2023  润新知