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是欢迎各位朋友前来咨询或者讨论学习