一、StampedLock类简介
StampedLock类,在JDK1.8时引入,是对读写锁ReentrantReadWriteLock的增强,该类提供了一些功能,优化了读锁、写锁的访问,同时使读写锁之间可以互相转换,更细粒度控制并发。
首先明确下,该类的设计初衷是作为一个内部工具类,用于辅助开发其它线程安全组件,用得好,该类可以提升系统性能,用不好,容易产生死锁和其它莫名其妙的问题。
1.1 StampedLock的引入
先来看下,为什么有了ReentrantReadWriteLock,还要引入StampedLock?
ReentrantReadWriteLock使得多个读线程同时持有读锁(只要写锁未被占用),而写锁是独占的。
但是,读写锁如果使用不当,很容易产生“饥饿”问题:
比如在读线程非常多,写线程很少的情况下,很容易导致写线程“饥饿”,虽然使用“公平”策略可以一定程度上缓解这个问题,但是“公平”策略是以牺牲系统吞吐量为代价的。(在ReentrantLock类的介绍章节中,介绍过这种情况)
1.2 StampedLock的特点
StampedLock的主要特点概括一下,有以下几点:
-
1、所有获取锁的方法,都返回一个邮戳(Stamp),Stamp为0表示获取失败,其余都表示成功;
-
2、所有释放锁的方法,都需要一个邮戳(Stamp),这个Stamp必须是和成功获取锁时得到的Stamp一致;
-
3、StampedLock是不可重入的;(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)
-
4、StampedLock有三种访问模式:
- ①Reading(读模式):功能和ReentrantReadWriteLock的读锁类似
- ②Writing(写模式):功能和ReentrantReadWriteLock的写锁类似
- ③Optimistic reading(乐观读模式):这是一种优化的读模式。
-
5、StampedLock支持读锁和写锁的相互转换
- 我们知道RRW中,当线程获取到写锁后,可以降级为读锁,但是读锁是不能直接升级为写锁的。
- StampedLock提供了读锁和写锁相互转换的功能,使得该类支持更多的应用场景。
-
6、无论写锁还是读锁,都不支持Conditon等待
我们知道,在ReentrantReadWriteLock中,当读锁被使用时,如果有线程尝试获取写锁,该写线程会阻塞。 但是,在Optimistic reading中,即使读线程获取到了读锁,写线程尝试获取写锁也不会阻塞,这相当于对读模式的优化,但是可能会导致数据不一致的问题。所以,当使用Optimistic reading获取到读锁时,必须对获取结果进行校验。
StampedLock 的性能之所以比 ReentrantReadWriteLock 还要好,其关键是 StampedLock 支持乐观读的方式。ReentrantReadWriteLock 支持多个线程同时读,但是当多个线程同时读的时候,所有的写操作会被阻塞;而 StampedLock 提供的乐观读,是允许一个线程获取写锁的,也就是说不是所有的写操作都被阻塞。
注意这里,我们用的是“乐观读”这个词,而不是“乐观读锁”,是要提醒你,乐观读这个操作是无锁的,所以相比较 ReadWriteLock 的读锁,乐观读的性能更好一些。
1.3 理解乐观读
乐观读往往与版本号挂钩,在获取数据时会同时记录与此对应的版本,然后写数据时查看当前的版本号与当时读取数据的版本号是否对应,如果匹配则更新数据以及版本号,否则需要重新获取数据。
//读取数据X,同时拿到的版本号为10 x = get(x), version=10 //修改数据 x = 20 //写回数据,如果put失败,说明version已过时 put(x, version)二、StampedLock使用
2.1 获取/释放悲观读锁示意代码
StampedLock lock = new StampedLock(); long stamp = lock.readLock(); try { //省略业务相关代码 } finally { lock.unlockRead(stamp); }2.2 获取/释放写锁示意代码
StampedLock lock = new StampedLock(); long stamp = lock.writeLock(); try { //省略业务相关代码 } finally { lock.unlockWrite(stamp); }2.3 StampedLock.tryOptimisticRead() 乐观读
StampedLock 支持 tryOptimisticRead() 方法(乐观读),读取完毕后需要做一次 戳校验 如果校验通过,表示这期间确实没有写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据安全。
StampedLock lock = new StampedLock(); long stamp = lock.tryOptimisticRead(); // 验戳 if(lock.validate(stamp)){ //获取数据, 并返回 } // 锁升级 - 读锁 stamp = lock.readLock(); try { //省略业务相关代码 } finally { lock.unlockRead(stamp); }2.4 StampedLock使用示例
@Slf4j public class DataContainerStamped { private int data; private final StampedLock lock = new StampedLock(); public DataContainerStamped(int data) { this.data = data; } public int read(int readTime) { long stamp = lock.tryOptimisticRead(); log.info("optimistic read locking...{}", stamp); try { TimeUnit.SECONDS.sleep(readTime); } catch (InterruptedException e) { e.printStackTrace(); } if (lock.validate(stamp)) { log.info("read finish...{}, data:{}", stamp, data); return data; } // 锁升级 - 读锁 log.info("updating to read lock... {}", stamp); try { stamp = lock.readLock(); log.info("read lock {}", stamp); TimeUnit.SECONDS.sleep(readTime); log.info("read finish...{}, data:{}", stamp, data); } catch (InterruptedException e) { e.printStackTrace(); }finally { log.info("read unlock {}", stamp); lock.unlockRead(stamp); } return data; } public void write(int newData) { long stamp = lock.writeLock(); log.info("write lock {}", stamp); try { TimeUnit.SECONDS.sleep(2); this.data = newData; } catch (InterruptedException e) { e.printStackTrace(); }finally { log.info("write unlock {}", stamp); lock.unlockWrite(stamp); } } }2.4.1 测试 读-读 可以优化
public static void main(String[] args) throws InterruptedException { DataContainerStamped dataContainer = new DataContainerStamped(1); new Thread(() -> dataContainer.read(1), "t1").start(); TimeUnit.SECONDS.sleep((long) 0.5); new Thread(() -> dataContainer.read(0), "t2").start(); }- 输出结果,可以看到实际没有加读锁
2.4.2 测试 读-写 时优化读补加读锁
public static void main(String[] args) throws InterruptedException { DataContainerStamped dataContainer = new DataContainerStamped(1); new Thread(() -> dataContainer.read(1), "t1").start(); TimeUnit.SECONDS.sleep((long) 0.5); new Thread(() -> dataContainer.write(0), "t2").start(); }输出结果:
15:06:46.747 [t1] INFO com.yibo.lock.DataContainerStamped - optimistic read locking...256 15:06:46.747 [t2] INFO com.yibo.lock.DataContainerStamped - write lock 384 15:06:47.764 [t1] INFO com.yibo.lock.DataContainerStamped - updating to read lock... 256 15:06:48.754 [t2] INFO com.yibo.lock.DataContainerStamped - write unlock 384 15:06:48.755 [t1] INFO com.yibo.lock.DataContainerStamped - read lock 513 15:06:49.761 [t1] INFO com.yibo.lock.DataContainerStamped - read finish...513, data:0 15:06:49.761 [t1] INFO com.yibo.lock.DataContainerStamped - read unlock 513三、StampedLock 使用注意事项
对于读多写少的场景 StampedLock 性能很好,简单的应用场景基本上可以替代 ReadWriteLock,但是 StampedLock 的功能仅仅是ReentrantReadWriteLock的子集,并不能替代ReentrantReadWriteLock,在使用的时候,还是有几个地方需要注意一下。
- 1、StampedLock不支持重入( Reentrant)
- 2、StampedLock的悲观读锁,写锁不支持条件变量
- 3、如果线程阻塞在StampedLock的readLock()或writeLock()上时,此时调用该阻塞线程的interrupt()会导致CPU飙升。
3.1 总结
可以看到相比直接用读锁,乐观读模式可以:
-
1、进入悲观读锁前先看下有没有进入写模式(说白了就是有没有已经获取了悲观写锁)
-
2、如果其他线程已经获取了悲观写锁,那么就只能老老实实的获取悲观读锁(这种情况相当于退化成了读写锁)
-
3、如果其他线程没有获取悲观写锁,那么就不用获取悲观读锁了,减少了一次获取悲观读锁的消耗和避免了因为读锁导致写锁阻塞的问题,直接返回读的数据即可(必须再tryOptimisticRead和validate之间获取好数据,否则数据可能会不一致了,试想如果过了validate再获取数据,这时数据可能被修改并且读操作也没有任何保护措施)。
四、StampedLock的实现思想
在StampedLock中使用了CLH自旋锁,如果发生了读失败,不立刻把读线程挂起,锁当中维护了一个等待线程队列。所有申请锁但是没有成功的线程都会记录到这个队列中,每一个节点(一个节点表示一个线程)保存一个标记位(locked),用于判断当前线程是否已经释放锁。新加入的节点会加入到队列的末尾,当前等待队列尾部的节点作为其前序节点,并使用类似如下代码(一个空的死循环)判断前序节点是否已经成功的释放了锁: while(pred.locked){ }
解释:pred表示当前试图获取锁的线程的前序节点,如果前序节点没有释放锁,则当前线程就执行该空循环并不断判断前序节点的锁释放,即类似一个自旋锁的效果,避免被系统挂起。当循环一定次数后,前序节点还没有释放锁,则当前线程就被挂起而不再自旋。
StampedLock原理:https://segmentfault.com/a/1190000015808032
参考: https://segmentfault.com/a/1190000015808032
https://www.cnblogs.com/moris5013/p/11882894.html
https://www.cnblogs.com/xidongyu/articles/12241190.html
https://www.cnblogs.com/zxporz/p/11642176.html