近日,业务上要求要对一块缓存进行高效率的读写。一开始采用了读写锁的设计,后来发现单个线程单次需要进行成千上万次的读,导致了过多的加解锁的开销,效率实在不敢恭维。加锁的主要原因是多线程先判断再操作导致判定失效问题,最近一直在考虑如何进行免锁设计,结合之前看过的final内存语义,设计了一个没有锁的实现方式,然后用了一万个线程进行并发测试,暂时通过了测试,下面说说我的思路,如果有问题,大家可以狂喷一下。
首先读写锁的方式,大家应该是没问题的。我先阐述一下问题。
由上图可以看出,线程1判定的状态在读取缓存时已经失效了,导致读取的时候得到空指针异常。
下面是我的demo代码
public class NoLockCache{ private class Lock { private final boolean isValid; private final HashMap<Integer, Integer> hashMap; public Lock(boolean isValid, HashMap<Integer, Integer> hashMap) { this.isValid = isValid; this.hashMap = hashMap; } public Integer get() { if (isValid) { LockSupport.parkNanos(10000000); return hashMap.get(1); } else { return 0; } } } private volatile Lock lock; public NoLockCache() { HashMap<Integer, Integer> hashMap = new HashMap<>(); hashMap.put(1, 1); lock = new Lock(true, hashMap); } public Integer get() { return lock.get(); } public void release() { lock = new Lock(false, new HashMap<>()); HashMap<Integer, Integer> hashMap = new HashMap<>(); hashMap.put(1, 1); lock = new Lock(true, hashMap); } }
免锁设计
|
上面NoLockCache的get操作,委托给内部类Lock。
Lock的两个成员变量都是final类型的,这两个final类型的变量可以保证在初始化Lock时,isValid和缓存hashMap处在一个一致的状态(可以参考并发编程艺术第三章的final语义或者博客http://www.cnblogs.com/CLFR/p/6262433.html),别的线程看到Lock只有两个状态,isValid是false且缓存是空,或者isValid是true且缓存被填充状态。有人会说了,那Lock的get方法不也是先判断,再操作吗?其实这时候的Lock已经加载在栈中,这时候的Lock方法get结束前,Lock会维持一个快照的状态,不会抛出NPE。