优雅的缓存写法,以及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是欢迎各位朋友前来咨询或者讨论学习

posted @ 2020-09-23 17:01  李京霖  阅读(343)  评论(0编辑  收藏  举报