• Java中的读写锁ReentrantReadWriteLock详解,存在一个小缺陷


    写在开头

    最近是和java.util.concurrent.locks包下的同步类干上了,素有 并发根基 之称的concurrent包中全是精品,今天我们继续哈,今天学习的主题要由一个大厂常问的Java面试题开始:

    小伙子,来说一说Java中的读写锁,你都用过哪些读写锁吧?

    这个问题小伙伴们遇到了该如何回答呢?心里琢磨去吧,哈哈😄,不过build哥的回答要用从ReentrantReadWriteLock开始说起了,这个类也就是今天的主角,而它们同样是来自于java.util.concurrent.locks之下!

    image

    读写锁诞生的背景

    在过去学习的过程中我们学过 synchronized、 ReentrantLock这种独占式锁,他们的好处是保证了线程的安全,缺点是同一时刻只能有一个线程持有锁,大大的影响了效率,而之前学过的Semaphore(信号量)这种呢,虽然支持同一时刻被多个线程获取,但它不能很好的保障线程安全性,我们需要的是一种效率高、安全性好的同步锁。

    考虑到真正的生产生活中,对于数据的读取要比写入更为频繁,伟大的开发者们,将读数据的时候设置为共享锁,支持多个线程持有读锁,而在写的时候,考虑到线程安全,采用独占锁,同一时候仅允许一个线程持有写锁,在这种背景下读写锁应运而生!

    读写锁:ReentrantReadWriteLock

    ReentrantReadWriteLock是ReadWriteLock 接口的默认实现类,从名字可以看得出它也是一种具有可重入性的锁,同时也支持公平与非公平的配置,底层有两把锁,一把是 WriteLock (写锁),一把是 ReadLock(读锁) 。读锁是共享锁,写锁是独占锁。读锁可以被同时读,可以同时被多个线程持有,而写锁最多只能同时被一个线程持有,也是基于AQS实现的底层锁获取与释放逻辑。

    image

    内部构造

    根据上面的构造图如果还没有搞清楚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-10
    线程:Thread-00
    线程:Thread-10
    线程:Thread-20
    线程:Thread-21
    线程:Thread-22
    线程:Thread-23
    线程:Thread-03
    线程:Thread-13
    线程:Thread-24
    线程:Thread-04
    线程:Thread-14
    线程:Thread-04
    线程:Thread-14
    线程:Thread-04

    通过输出内容,我们进一步得证,在ReentrantReadWriteLock在使用读锁时,可以支持多个线程获取读资源,而在调用写锁时,其他读线程和写线程均阻塞等待当前线程写完。

    存在的问题

    虽然ReentrantReadWriteLock优化了原有的独占锁对于程序读写的性能,但它仍然存在一个弊端,就是 “写饥饿” ,因为在写的时候,是独占模式,其他线程不能读也不能写,这时候若有大量的读操作的话,那这些线程也只能等待着,从而带来写饥饿。

    那这个问题怎么解决呢?我们在下一篇StampedLock(锁王)的讲解中,进行解答哈,敬请期待!

    结尾彩蛋

    如果本篇博客对您有一定的帮助,大家记得留言+点赞+收藏呀。原创不易,转载请联系Build哥!

    image

    如果您想与Build哥的关系更近一步,还可以关注“JavaBuild888”,在这里除了看到《Java成长计划》系列博文,还有提升工作效率的小笔记、读书心得、大厂面经、人生感悟等等,欢迎您的加入!

    image

  • 相关阅读:
    ASEMI肖特基二极管20V45参数,20V45尺寸,20V45大小
    让python代码找到文件路径的最好方法
    聊一聊Twitter的雪花算法
    SpringBoot整合Redis、StringRedisTemplate读写redis客户端
    网络爬虫软件学习
    如何在 Windows 10/11 上编辑 PDF [4 种简单方法]
    feign传输文件
    深眸科技迭代深度学习算法,以AI机器视觉技术扩围工业应用场景
    vue el-table字段点击出现el-input输入框,失焦保存
    【深度学习】第二章:全连接神经网络
  • 原文地址:https://www.cnblogs.com/JavaBuild/p/18162934