写在开头
最近是和java.util.concurrent.locks包下的同步类干上了,素有 并发根基
之称的concurrent包中全是精品,今天我们继续哈,今天学习的主题要由一个大厂常问的Java面试题开始:
小伙子,来说一说Java中的读写锁,你都用过哪些读写锁吧?
这个问题小伙伴们遇到了该如何回答呢?心里琢磨去吧,哈哈😄,不过build哥的回答要用从ReentrantReadWriteLock开始说起了,这个类也就是今天的主角,而它们同样是来自于java.util.concurrent.locks之下!
读写锁诞生的背景
在过去学习的过程中我们学过 synchronized、 ReentrantLock这种独占式锁,他们的好处是保证了线程的安全,缺点是同一时刻只能有一个线程持有锁,大大的影响了效率,而之前学过的Semaphore(信号量)这种呢,虽然支持同一时刻被多个线程获取,但它不能很好的保障线程安全性,我们需要的是一种效率高、安全性好的同步锁。
考虑到真正的生产生活中,对于数据的读取要比写入更为频繁,伟大的开发者们,将读数据的时候设置为共享锁,支持多个线程持有读锁,而在写的时候,考虑到线程安全,采用独占锁,同一时候仅允许一个线程持有写锁,在这种背景下读写锁应运而生!
读写锁:ReentrantReadWriteLock
ReentrantReadWriteLock是ReadWriteLock 接口的默认实现类,从名字可以看得出它也是一种具有可重入性的锁,同时也支持公平与非公平的配置,底层有两把锁,一把是 WriteLock (写锁),一把是 ReadLock(读锁) 。读锁是共享锁,写锁是独占锁。读锁可以被同时读,可以同时被多个线程持有,而写锁最多只能同时被一个线程持有,也是基于AQS实现的底层锁获取与释放逻辑。
内部构造
根据上面的构造图如果还没有搞清楚ReentrantReadWriteLock的底层构造的话,那我们跟入源码中取一探究竟吧!
【源码分析】
// 内部结构 private final ReentrantReadWriteLock.ReadLock readerLock; private final ReentrantReadWriteLock.WriteLock writerLock; final Sync sync; /*1、用以继承AQS,获得AOS的特性,以及AQS的钩子函数*/ abstract static class Sync extends AbstractQueuedSynchronizer { // 具体实现 } /*非公平模式,默认为这种模式*/ static final class NonfairSync extends Sync { // 具体实现 } /*公平模式,通过构造方法参数设置*/ static final class FairSync extends Sync { // 具体实现 } /*读锁,底层是共享锁*/ public static class ReadLock implements Lock, java.io.Serializable { private final Sync sync; protected ReadLock(ReentrantReadWriteLock lock) { sync = lock.sync; } // 具体实现 } /*写锁,底层是独占锁*/ public static class WriteLock implements Lock, java.io.Serializable { private final Sync sync; protected WriteLock(ReentrantReadWriteLock lock) { sync = lock.sync; } // 具体实现 } // 构造方法,初始化两个锁 public ReentrantReadWriteLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); readerLock = new ReadLock(this); writerLock = new WriteLock(this); } // 获取读锁和写锁的方法 public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; } public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
上面为底层的主要构造内容,ReentrantReadWriteLock中共写了5个静态内部类,各有功效,在上面的注释中也有提及。
使用案例
那么这个读写锁如何使用呢?我们写一个小小的测试案例,也感受一下。
【测试案例】
public class Test { private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); private int data = 0; /** * 写方法 * @param value */ public void write(int value) { //注意,获取锁的操作要在try/finally外面 lock.writeLock().lock(); // 获取写锁 try { data = value; System.out.println("线程:"+Thread.currentThread().getName() + "写" + data); } finally { lock.writeLock().unlock(); // 释放写锁 } } public void read() { lock.readLock().lock(); // 获取读锁 try { System.out.println("线程:" + Thread.currentThread().getName() + "读" + data); } finally { lock.readLock().unlock(); // 释放读锁 } } public static void main(String[] args) { Test test = new Test(); // 创建读线程 Thread readThread1 = new Thread(() -> { for (int i = 0; i < 5; i++) { test.read(); } }); Thread readThread2 = new Thread(() -> { for (int i = 0; i < 5; i++) { test.read(); } }); // 创建写线程 Thread writeThread = new Thread(() -> { for (int i = 0; i < 5; i++) { test.write(i); } }); readThread1.start(); readThread2.start(); writeThread.start(); try { readThread1.join(); readThread2.join(); writeThread.join(); } catch (InterruptedException e) { e.printStackTrace(); } } }
输出:
线程:Thread-1读0 线程:Thread-0读0 线程:Thread-1读0 线程:Thread-2写0 线程:Thread-2写1 线程:Thread-2写2 线程:Thread-2写3 线程:Thread-0读3 线程:Thread-1读3 线程:Thread-2写4 线程:Thread-0读4 线程:Thread-1读4 线程:Thread-0读4 线程:Thread-1读4 线程:Thread-0读4
通过输出内容,我们进一步得证,在ReentrantReadWriteLock在使用读锁时,可以支持多个线程获取读资源,而在调用写锁时,其他读线程和写线程均阻塞等待当前线程写完。
存在的问题
虽然ReentrantReadWriteLock优化了原有的独占锁对于程序读写的性能,但它仍然存在一个弊端,就是 “写饥饿”
,因为在写的时候,是独占模式,其他线程不能读也不能写,这时候若有大量的读操作的话,那这些线程也只能等待着,从而带来写饥饿。
那这个问题怎么解决呢?我们在下一篇StampedLock(锁王)的讲解中,进行解答哈,敬请期待!
结尾彩蛋
如果本篇博客对您有一定的帮助,大家记得留言+点赞+收藏呀。原创不易,转载请联系Build哥!
如果您想与Build哥的关系更近一步,还可以关注“JavaBuild888”,在这里除了看到《Java成长计划》系列博文,还有提升工作效率的小笔记、读书心得、大厂面经、人生感悟等等,欢迎您的加入!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· winform 绘制太阳,地球,月球 运作规律
· 上周热点回顾(3.3-3.9)