欢送来到《并发王者课》,本文是该系列文章中的第15篇。
在上篇文章中,咱们介绍了Java中锁的根底Lock接口。在本文中,咱们将介绍Java中锁的另外一个重要的基本型接口,即ReadWriteLock接口。
在摸索Java中的并发时,ReadWriteLock无疑是重要的,然而了解它却并不容易。如果你此前已经检索材料,应该会发现大部分的文章对它的形容都比拟艰涩难懂,或连篇累牍的源码排列,或隔靴搔痒的喋喋不休,既说不到重点,也说不清前因后果。
所以,在本文中咱们会将介绍的重点放在对思路的了解上,而不是对源码的解读上。对于源码以及其背地的常识,咱们将在前面的更高级的系列中进行解说。
一、了解ReadWriteLock存在的价值
了解ReadWriteLock,首页要了解它存在的意义是什么。换言之,它要解决什么问题。为此,咱们无妨从下图着手一探到底。
不知你看明确了没有,这幅图所表白的有三层含意:
- 大量线程在竞争同一份资源;
- 这些线程中有的是读申请,有的是写申请;
- 在多个线程的申请中,读申请显著高于写申请。
这样的场景是否似曾相识?没错,它就是典型的缓存利用场景。
家喻户晓,缓存的存在是为了进步利用的读写性能。一方面,咱们须要通过缓存拦挡大量的读数据的申请。另一方面,咱们也须要不定期地更新缓存。但总体而言,更新缓存的次数远远小于读缓存的次数。
在这个过程中,关键问题在于,为了保持数据一致性,咱们在读写缓存的时候,不能让读申请拿到脏数据,这就须要用到锁。然而,更要害的问题在于,尽管读写之间须要互斥,但读与读之间不能够互斥。
总结来说,这个问题次要有上面这几个要点:
- 数据容许多个线程同时读取,但只容许一个线程进行写入;
- 在读取数据的时候,不能够存在写操作或者写申请;
- 在写数据的时候,不能够存在读申请。
如果你对此依然有些迷茫,那么上面这张图倡议你珍藏,这张图正是ReadWriteLock对问题的概述和它的解决方案,也是诠释ReadWriteLock最好的一幅图。
在你没有了解ReadWriteLock之前,你会感觉它非常艰涩且源码干燥。然而,一旦你了解它要解决的问题,以及它所提供的计划后,你会发现它的设计居然如此奇妙。它居然设计了两种截然不同的锁,其中一把正如咱们此前认知的那样是线程互斥的,而另一把锁居然能够为多个线程所共享!两把锁的完满配合,解决了并发读写的场景问题。
在豁然开朗后,所谓源码不过是队列与共享,它们是ReadWriteLock的一种实现形式,而不是阻挡你了解的绊脚石。
二、自主实现ReadWriteLock
在了解了ReadWriteLock背地的问题和它的解决思路之后,咱们就能够齐全抛开JDK中的源码本人实现一把读写锁。
public class ReadWriteLock{ private int readers = 0; private int writers = 0; private int writeRequests = 0; public synchronized void lockRead() throws InterruptedException{ while(writers > 0 || writeRequests > 0){ wait(); } readers++; } public synchronized void unlockRead(){ readers--; notifyAll(); } public synchronized void lockWrite() throws InterruptedException{ writeRequests++; while(readers > 0 || writers > 0){ wait(); } writeRequests--; writers++; } public synchronized void unlockWrite() throws InterruptedException{ writers--; notifyAll(); }}
在读锁lockRead()
中,是不容许有写申请或写操作的。如果有,那么读申请将进入期待。
而在lockWrite()
中,同时不容许读申请和其余写操作的存在,此时只容许有一个写申请。
以上就是读写锁简略的自主实现形式。当然,它是不欠缺的,只是根本的示例。它没有思考到根本的线程重入问题,真实情况也比它简单很多,但你了解它的意思就好。
三、Java中的ReadWriteLock是如何实现的
最初,咱们再来看JDK中的ReadWriteLock实现的一些基本思路。ReadWriteLock和咱们上篇所说的Lock接口以及其余类的根本关系如下图所示:
能够看到,JDK中的读写锁的实现是在ReentrantReadWriteLock这个类中。ReentrantReadWriteLock蕴含了两个外部类:ReadLock和WriteLock,而这两个类又实现了Lock接口。
读写锁的降级与降级
读写锁的降级与降级是ReentrantReadWriteLock中的一个重要知识点,也是高频的面试题。
从读锁到写锁,称之为锁的降级,反之为锁的降级。了解读写锁的降级和降级,最直观的形式是写代码验证。
代码片段1,先获取读锁,再获取写锁。
public class ReadWriteLockDemo { public static void main(String[] args) { ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); readWriteLock.readLock().lock(); System.out.println("曾经获取读锁..."); readWriteLock.writeLock().lock(); System.out.println("曾经获取写锁..."); }}
输入后果如下:
曾经获取读锁...
代码片段2,先获取写锁,再获取读锁:
public class ReadWriteLockDemo { public static void main(String[] args) { ReadWriteLock readWriteLock = new ReentrantReadWriteLock(); readWriteLock.writeLock().lock(); System.out.println("曾经获取写锁..."); readWriteLock.readLock().lock(); System.out.println("曾经获取读锁..."); }}
输入后果如下:
曾经获取写锁...曾经获取读锁...Process finished with exit code 0
这样一来,后果曾经非常明了。ReentrantReadWriteLock反对锁的降级,但不反对锁的降级。
读写锁中的公平性
在后面的文章中,咱们讲过线程饥饿的由来和结果,所以良好的并发工具类在设计时都会思考到公平性,ReentrantReadWriteLock也是如此。
在ReentrantReadWriteLock中,同时提供了偏心和非偏心两种模式,且默认为非偏心模式。从上面摘取的源码片段中,能够清晰地看到。
public ReentrantReadWriteLock() { this(false); } /** /** * Creates a new {@code ReentrantReadWriteLock} with * default (nonfair) ordering properties. */public ReentrantReadWriteLock() { this(false);}/** * Creates a new {@code ReentrantReadWriteLock} with * the given fairness policy. * * @param fair {@code true} if this lock should use a fair ordering policy */public ReentrantReadWriteLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); readerLock = new ReadLock(this); writerLock = new WriteLock(this);}
小结
以上就是对于读写锁的全部内容。在本文中,咱们从缓存问题登程,接着从ReadWriteLock中寻找答案,以便能从更轻松的角度了解ReadWriteLock的前因后果。
了解ReadWriteLock的要害不在于对源码的分析,而在于对其思路的了解。
另外,咱们简略地介绍了ReentrantReadWriteLock中的一些要害知识点,但诸如其背地的AQS等并没有开展陈说。对此也不用焦急,咱们会在前面有具体的剖析介绍。
注释到此结束,祝贺你又上了一颗星✨
夫子的试炼
- 尝试在示例代码中减少对读写线程的重入反对。
延长浏览与参考资料
- 示例代码参考
- 《并发王者课》纲要与更新进度总览
对于作者
关注公众号【技术八点半】,及时获取文章更新。传递有品质的技术文章,记录平凡人的成长故事,偶然也聊聊生存和现实。晚上8:30推送作者品质原创,早晨20:30推送行业深度好文。
如果本文对你有帮忙,欢送点赞、关注、监督,咱们一起从青铜到王者。