引言
ReentrantReadWriteLock可以让多个读线程同时持有读锁(只要写锁未被占用),而写锁是独占的,所以在读多写少的场景上可以提高吞吐量,比如hdfs文件系统。但是,读写锁如果使用不当,很容易产生“饥饿”问题,比如在读线程非常多,写线程很少的情况下,很容易导致写线程“饥饿”,虽然使用“公平”策略可以一定程度上缓解这个问题,但是“公平”策略是以牺牲系统吞吐量为代价的。
导致写线程饥饿的情况:当线程 A 持有读锁读取数据时,线程 B 要获取写锁修改数据就只能到队列里排队。此时又来了线程 C 读取数据,那么线程 C 就可以获取到读锁,而要执行写操作线程 B 就要等线程 C 释放读锁。由于该场景下读操作远远大于写的操作,此时可能会有很多线程来读取数据而获取到读锁,那么要获取写锁的线程 B 就只能一直等待下去,最终导致饥饿。
其实本人理解读写锁其实已经是一个非常成熟的并发解决方案了(读多写少场景),hdfs namenode使用的就是全局读写锁,但是默认采用的是公平读写锁机制。既然存在问题,肯定就会出现各种解决方案,本篇文章主要介绍下解决方案之StampedLock的简单原理。
ReentrantReadWriteLock
ReentrantReadWriteLock内部通过AbstractQueuedSynchronizer来实现同步语义,在ReentrantReadWriteLock中拥有读锁与写锁。读锁是一种共享锁,允许多个线程同时获取锁,写锁是一种独占锁,同一时刻只允许一个线程获取。
通过梳理源码可以发现:在读多写少的情况下,使用默认的非公平读写锁,线程想要获取到写锁变得极为困难,因为当前可能一直存在读锁,同步队列中的第一个节点一直会是共享节点,这样就无法获取到写锁。具体源码分析过程省略,可查看相关博客进行梳理和学习。
StampedLock
StampedLock是JUC并发包里面JDK1.8版本新增的一个锁,是对读写锁的增强与优化。StampedLock内存实现是基于CLH锁,提供了三种模式来控制读写操作:写锁 writeLock、悲观读锁 readLock、乐观读 Optimistic reading。
StampedLock是基于CLH锁原理实现的, CLH是一种基于排队思想实现的自旋锁,可以保证FIFO(先进先出)的服务顺序,所以会避免写线程饥饿问题,其实就是其中实现了一个队列,每次不管是读锁也好写锁也好,未拿到锁就加入队列,然后每次解锁后队列头存储的线程节点获取锁,以此避免饥饿。StampedLock详细原理学习可参考:https://cloud.tencent.com/developer/article/1470988
我们重点关注下乐观读 Optimistic reading的实现原理,看下面的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | class Point { private int x, y; final StampedLock sl = new StampedLock(); //计算到原点的距离 double distanceFromOrigin() { // 乐观读 long stamp = sl.tryOptimisticRead(); // 读入局部变量, // 读的过程数据可能被修改 int curX = x, curY = y; //判断执行读操作期间, //是否存在写操作,如果存在, //则sl.validate返回false if (!sl.validate(stamp)) { // 升级为悲观读锁 stamp = sl.readLock(); try { curX = x; curY = y; } finally { //释放悲观读锁 sl.unlockRead(stamp); } } return Math.sqrt(curX * curX + curY * curY); } } |
首先采用乐观读sl.tryOptimisticRead,如果执行乐观读操作的期间,存在写操作,会把乐观读升级为悲观读锁sl.readLock。这个做法挺合理的,否则你就需要在一个循环里反复执行乐观读,直到执行乐观读操作的期间没有写操作(只有这样才能保证 x 和 y 的正确性和一致性),而循环读会浪费大量的 CPU。升级为悲观读锁,代码简练且不易出错,建议你在具体实践时也采用这样的方法。
看到这里,会觉得stampedLock已经可以很完美的解决了读写锁的问题。但是继续研究stampedLock的源码,你会发现有很多实现的问题,例如它不支持可重入,内部的悲观读锁、写锁都不支持条件变量,readLock() 、writeLock()方法不会响应中断,可能会导致cpu使用率升高问题(可以使用可中断的悲观读锁 readLockInterruptibly() 和写锁 writeLockInterruptibly())等等其他问题。所以考虑使用时,一定要把其中的细节搞清楚,避免引入其他问题。
欢迎关注本人公众号,一起讨论技术问题,公众号专注于分享大数据相关的技术原理。