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

java,reentrantreadwritelock · 浏览次数 : 0

小编点评

**读写锁的背景** 在过去学习中,我们学习了同步、ReentrantLock等独占式锁,它们可以保证线程的安全,但它们也限制了线程的并行性。为了解决这个问题,java.util.concurrent.locks包提供读写锁。 **ReentrantReadWriteLock 的主要功能** ReentrantReadWriteLock 是 ReadLock 和 WriteLock 的组合,它可以同时支持多个线程进行读写操作,但它们必须按顺序进行。 **ReentrantReadWriteLock 的内部构造** ReentrantReadWriteLock 的内部构造根据以下构造图进行: ```java private final ReentrantReadWriteLock.ReadLock readerLock; private final ReentrantReadWriteLock.WriteLock writerLock; ``` * **readerLock** 是一个共享锁,它允许多个线程进行读操作。 * **writerLock** 是一个独占锁,它允许只有一个线程进行写操作。 **ReentrantReadWriteLock 的用法** 1. 创建一个 ReentrantReadWriteLock 对象。 2. 获取读锁: lock.readLock().lock(); 3. 获取写锁: lock.writeLock().lock(); 4. 完成读操作后,释放读锁: lock.readLock().unlock(); 5. 完成写操作后,释放写锁: lock.writeLock().unlock(); **读写锁的优缺点** **优点:** * 支持多个线程进行读写操作。 * 优化了原有的独占锁对于程序读写的性能。 **缺点:** * 写操作只能由一个线程进行。 * 当读操作并发时,写操作会阻塞。 * 如果存在大量的读操作,则可能导致写饥饿。 **ReentrantReadWriteLock 的改进版本** 除了 ReentrantReadWriteLock,java.util.concurrent.locks 包还提供以下改进版本: * **StampedLock** * **FairSync** * **NonfairSync**

正文

写在开头

最近是和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-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哥!

image

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

image

与Java中的读写锁ReentrantReadWriteLock详解,存在一个小缺陷相似的内容:

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

写在开头 最近是和java.util.concurrent.locks包下的同步类干上了,素有 并发根基 之称的concurrent包中全是精品,今天我们继续哈,今天学习的主题要由一个大厂常问的Java面试题开始: 小伙子,来说一说Java中的读写锁,你都用过哪些读写锁吧? 这个问题小伙伴们遇到了该

[转帖]解决Java中的java.io.IOException: Broken pipe问题

https://www.cnblogs.com/Chary/p/16835248.html Java 中java.io.IOException: Broken pipe 认识broken pipe pipe是管道的意思,管道里面是数据流,通常是从文件或网络套接字读取的数据。 当该管道从另一端突然关闭

[转帖]【JVM】类加载机制

什么是类的加载 将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Class对象,用来封装类在方法区内的数据结构。类的加载的最终产品是位于堆区中的Class对象,Class对象封装了类在方法区内的数据结构,并且向Java程序员提供

[转帖]JVM类加载机制

概述 虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。 类的加载指的是将类的.class文件中的二进制数据读入到内存中,将其放在运行时数据区的方法区内,然后在堆区创建一个java.lang.Cl

java datetime数据类型去掉时分秒

本文详细介绍了Java DateTime数据类型去掉时分秒的方法示例,为了便于读者理解,给出了两个示例,同时本文也介绍了如何在Java中创建日期和时间对象。

Java设计模式总结

前言1 刚才整理博客的时候,发觉草稿箱里面躺了一篇文章。这篇文章来自于6年前,2018年,我还在读书的时候。当时csdn,博客园还是行业top,近些年掘金,思否,个人ip站的崛起,也预示着互联网进程的演变。 过了6年之久,这篇文章还没有发布,趁现在有空,补充并且发布一下。 前言2 面试的过程中多次提

如何用ReadWriteLock实现一个通用的缓存中心?

摘要:在并发场景中,Java SDK中提供了ReadWriteLock来满足读多写少的场景。 本文分享自华为云社区《【高并发】基于ReadWriteLock开了个一款高性能缓存》,作者:冰 河。 写在前面 在实际工作中,有一种非常普遍的并发场景:那就是读多写少的场景。在这种场景下,为了优化程序的性能

Spring源码之XML文件中Bean标签的解析1

## 读取XML文件,创建对象 xml文件里包含Bean的信息,为了避免多次IO,需要一次性读取xml文件中所有bean信息,加入到Spring工厂。 #### 读取配置文件 ```java new ClassPathResource("applicationContext.xml") ``` Cl

深入理解java和dubbo的SPI机制

1 SPI简介 1.1 SPI(Service Provider Interface) 本质:将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。 java SPI:用来设计给服务提供商做插件使用的。基于策略模式来实现动态加载的机制。我

在表格开发中,如何选择适合自己的处理工具?

引言 GcExcel和EasyExcel都是卓越的高性能Excel处理库。GcExcel是由葡萄城公司开发,可用于Java和.Net平台;而EasyExcel是阿里巴巴开发的基于Java的开源Excel处理库。 在本文中,我们将对GcExcel和EasyExcel进行比较,帮助读者在实际场景中做出明