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

2024-04-28 15:12

本文主要是介绍Java中的读写锁ReentrantReadWriteLock详解,存在一个小缺陷,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

写在开头

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

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

在这里插入图片描述

这篇关于Java中的读写锁ReentrantReadWriteLock详解,存在一个小缺陷的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/943573

相关文章

Linux quotacheck命令教程:如何检查和修复文件系统的磁盘配额(附案例详解和注意事项)

Linux quotacheck命令介绍 quotacheck命令是用于扫描文件系统以检查磁盘配额的一致性。它生成、检查和修复配额文件。这个命令通常在系统引导时运行,或者在手动更改了配额设置后运行。 Linux quotacheck命令适用的Linux版本 quotacheck命令在大多数Linux发行版中都可以使用,包括Debian、Ubuntu、Alpine、Arch Linux、Kal

docker-java 操作docker

部署docker 10分钟学会Docker的安装和使用_docker安装-CSDN博客文章浏览阅读2.5w次,点赞44次,收藏279次。文章目录Docker简介Docker安装Windows安装Linux安装CentOS安装Ubuntu安装最近花了些时间学习docker技术相关,在此做一些总结,供往后复查和像了解docker的学习。Docker简介简而言之,Docker 是一个可供开发者通过

MySQL 8.0 全新特性详解

MySQL 8.0带来了许多令人兴奋的新特性和优化功能,下面我将逐一详细介绍每个特性: 一、原生数据字典 MySQL 8.0 引入了原生数据字典,取代了之前使用的.frm、.par、.opt等文件来存储元数据。这一改进使得元数据的访问和管理更加高效和直接。原生数据字典提供了对数据库对象元数据的统一视图,从而简化了数据库的管理和维护工作。通过查询数据字典,管理员可以快速了解数据库的结构、

Leetcode---1.两数之和 (详解加哈希表解释和使用)

文章目录 题目 [两数之和](https://leetcode.cn/problems/two-sum/)方法一:暴力枚举代码方法二:哈希表代码 哈希表哈希表的基本概念哈希函数(Hash Function):冲突(Collision):链地址法(Chaining):开放地址法(Open Addressing): 哈希表的操作插入(Insert):查找(Search):删除(Delete):

2.10学习笔记 java任务调度

http://www.ibm.com/developerworks/cn/java/j-lo-taskschedule/ java任务调度可以使用: Timer ScheduledExecutor 开源工具包 Quartz 开源工具包 JCronTab以上是根据时间定时执行的,下面有一个简单的不断读栈线程并执行的调度: http://lavasoft.blog.51cto.com

8.16 lru缓存java版

lru详细介绍及简单代码实现: http://blog.csdn.net/beiyetengqing/article/details/7855933 以下是本人的加强的lru缓存类,增加单例获取、缓存超时机制和修复一个clear()的bug package com.george.xblog.utils;import java.util.Hashtable;import java.util.

8.3facebook分享后不回调结果原因,java标签代码

facebook分享后不回调原因 出现原因: android:launchMode=”singleInstance” 加载方式不允许重新创建。 修改成: android:launchMode=”singleTask” java标签可以跳出指定位置 package com.ping;public class lhkgdf {public static void main(String[

SpringBoot解析MyBatis预编译SQL

pom.xml <profile><!-- 开发环境 --><id>dev</id><activation><!-- 默认激活 --><activeByDefault>true</activeByDefault></activation><properties><spring.profiles.active>dev</spring.profiles.active></properties>

[转载]Java面试基础概念总结

面向对象软件开发的优点有哪些? 答:开发模块化,更易维护和修改;代码之间可以复用;增强代码的可靠性、灵活性和可理解性。多态的定义? 答:多态是编程语言给不同的底层数据类型做相同的接口展示的一种能力。一个多态类型上的操作可以应用到其他类型的值上面。继承的定义? 答:继承给对象提供了从基类获取字段和方法的能力。继承提供了代码的重用行,也可以在不修改类的情况下给现存的类添加新特性抽象的定义?抽象和

java并发编程ThreadLocal的使用

ThreadLocal与synchronized 有着相反的概念,前者在多线程使用时会创建新的对象,后者保证对象在多线程是唯一的。 看代码好理解: public class Test {ThreadLocal<Long> longLocal = new ThreadLocal<Long>();ThreadLocal<String> stringLocal = new ThreadLocal