1. 为什么引入StampedLock JDK8中新增StampedLock。
从ReentrantLock到ReentrantReadWriteLock,再到StampedLock,读操作并发度依次提高。
ReentrantReadWriteLock采用“悲观读”策略,当第一个读线程抢到共享锁,第二个、第三个读线程还可以抢到共享锁,使得写线程一直无法获取互斥写锁,会导致写线程“饿死”。
读写锁的公平/非公平实现中,尽量避免这种情形,但还有可能发生。
StampedLock通过读写不互斥进一步提高读的并发量。
读写不互斥的问题在于:会产生不可重复读,两次读取结果不一样。
即读取的时候,可能另一个线程正在修改该值,还没有完成,读完之后,写线程也操作结束,此时读线程读到的数据和真实的数据不一致。
2. StampedLock的解决方式 StampedLock引入了“乐观读”策略:
- 读的时候不加读锁,读出来发现数据被修改了,再升级为“悲观读”;即读错了再严格读一次,避免写线程被饿死。
- StampedLock是一个读写锁,悲观读和悲观写的锁状态需要同步,互斥,锁状态操作需要是原子操作。 首先需要在state中划分出读锁和写锁。
3. state中的具体表示 state用于表示锁状态: private transient volatile long state;
第1-7位记录共享锁读锁,每获取一个读锁,该7位+1,如果溢出,每溢出一次,readerOverflow+1; 每释放一个读锁,该值-1,如果不够,借位readerOverflow的值。 第8位记录互斥锁写锁状态,如果存在写锁,该位记1,不存在记0; 第8-64位用于记录乐观锁版本号。 每获取一次写锁,state+WBIT,即从第8位开始的值。释放写锁,并不让state-WBIT,保证乐观锁版本号的单调递增,防止乐观锁出现ABA问题,即在tryOptimisticRead和validate(stamp)之间获取写锁并释放写锁。 如果从第8位到第64位都是1,则再加WBIT,将导致state是0,因为64位越界,此时将state复位ORIGIN: 即:
获取写锁的时候,state+WBIT,释放的时候state+WBIT,第一次让写锁位置位1,第二次让写锁位置位0,同时乐观锁位表示的值增大。
当写锁位是1的时候,悲观读锁位都是0,readerOverflow也是0。
当写锁位是0的时候,如果悲观锁抢锁,则写锁位的7位以及readerOverflow用于记录cowait链表获取共享读锁的状态。
4. StampedLock流程
4.1 tryOptimisticRead方法 先判断是否存在写锁,如果存在,就返回0L,表示乐观锁获取失败。 如果不存在写锁,就获取此时的乐观锁版本号,以用于稍后的validate(stamp)校验乐观锁的成功与否。
4.2 悲观读锁以及StampedLock实现的CLH队列 WMODE是互斥锁,链表中是单节点,RMODE是共享读锁,使用cowait进行横链连接同是RMODE的节点。
加我微信:babadeerya520,索取资料,解锁更多涨薪技能!