一、摘要

在上篇文章中,我们讲到ReentrantLock可以保证了只有一个线程能执行加锁的代码。

但是有些时候,这种保护显的有点过头,比如下面这个方法,它仅仅就是只读取数据,不修改数据,它实际上允许多个线程同时调用的。

public class Counter {

    private final Lock lock = new ReentrantLock();

    private int count;

    public int get() {
// 加锁
lock.lock();
try {
return count;
} finally {
// 释放锁
lock.unlock();
}
}
}

站在程序性能的角度,实际上我们想要的是这样的效果。

  • 1.读和读之间不互斥,因为只读操作不会有数据安全问题
  • 2.写和写之间互斥,避免一个写操作影响另外一个写操作,引发数据计算错误问题
  • 3.读和写之间互斥,避免读操作的时候写操作修改了内容,引发数据脏读问题

总结起来就是,允许多个线程同时读,但只要有一个线程在写,其他线程就必须排队等待。

在 JDK 中有一个读写锁ReadWriteLock,使用它就可以解决这个问题,它可以保证以下两点:

  • 1.只允许一个线程写入,其他线程既不能写入也不能读取
  • 2.没有写入时,多个线程允许同时读,可以提高程序并发性能

实际上,读写锁ReadWriteLock里面有两个锁实现,一个是读操作相关的锁,称为共享锁,当多个线程同时操作时,不会让多个线程进行排队等待,大大的提升了程序并发读的执行效率;另一个是写操作相关的锁,称为排他锁,当多个线程同时操作时,只允许一个线程写入,其他线程进入排队等待;两者进行组合操作,就可以实现上面的预期效果。

下面我们一起来看看它的基本用法!

二、ReadWriteLock 基本用法

2.1、读和读共享

读和读之间不互斥,当多个线程进行读的时候,不会让多个线程进行排队等待。

我们可以看一个简单的例子!

public class Counter {

    private final ReadWriteLock lock = new ReentrantReadWriteLock();

    private int count;

    public void read() {
// 加读锁
lock.readLock().lock();
try {
String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date());
System.out.println(time + " 当前线程:" + Thread.currentThread().getName() + "获得了读锁,count:" + count);
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放读锁
lock.readLock().unlock();
}
}
}
public class MyThreadTest {

    public static void main(String[] args) {
Counter counter = new Counter();
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
counter.read();
}
}); Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
counter.read();
}
}); threadA.start();
threadB.start();
}
}

看一下运行结果:

2023-10-23 16:12:28:119 当前线程:Thread-0获得了读锁,count:0
2023-10-23 16:12:28:119 当前线程:Thread-1获得了读锁,count:0

从日志时间上可以很清晰的看到,尽管加锁了,并且休眠了 5 秒,但是两个线程还是几乎同时执行try()方法里面的代码,证明了读和读之间是不互斥的,可以显著提高程序的运行效率。

2.2、写和写之间互斥

写和写之间互斥,当多个线程进行写的时候,只允许一个线程写入,其他线程进入排队等待。

我们可以看一个简单的例子!

public class Counter {

    private final ReadWriteLock lock = new ReentrantReadWriteLock();

    private int count;

    public void write() {
// 加写锁
lock.writeLock().lock();
try {
count++;
String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date());
System.out.println(time + " 当前线程:" + Thread.currentThread().getName() + "获得了写锁,count:" + count);
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放写锁
lock.writeLock().unlock();
}
}
}
public class MyThreadTest {

    public static void main(String[] args) {
Counter counter = new Counter();
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
counter.write();
}
}); Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
counter.write();
}
}); threadA.start();
threadB.start();
}
}

看一下运行结果:

2023-10-23 16:29:59:103 当前线程:Thread-0获得了写锁,count:1
2023-10-23 16:30:04:108 当前线程:Thread-1获得了写锁,count:2

从日志时间上可以很清晰的看到,两个线程进行串行执行,证明了写和写之间是互斥的。

2.3、读和写之间互斥

读和写之间互斥,当多个线程交替进行读写的时候,操作上互斥,只有一个线程能进入,其他线程进入排队等待。

我们可以看一个简单的例子!

public class Counter {

    private final ReadWriteLock lock = new ReentrantReadWriteLock();

    private int count;

    public void read() {
// 加读锁
lock.readLock().lock();
try {
String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date());
System.out.println(time + " 当前线程:" + Thread.currentThread().getName() + "获得了读锁,count:" + count);
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放读锁
lock.readLock().unlock();
}
} public void write() {
// 加写锁
lock.writeLock().lock();
try {
count++;
String time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSS").format(new Date());
System.out.println(time + " 当前线程:" + Thread.currentThread().getName() + "获得了写锁,count:" + count);
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放写锁
lock.writeLock().unlock();
}
}
}
public class MyThreadTest {

    public static void main(String[] args) {
Counter counter = new Counter();
Thread threadA = new Thread(new Runnable() {
@Override
public void run() {
counter.read();
}
}); Thread threadB = new Thread(new Runnable() {
@Override
public void run() {
counter.write();
}
}); threadA.start();
threadB.start();
}
}

看一下运行结果:

2023-10-23 16:36:08:786 当前线程:Thread-0获得了读锁,count:0
2023-10-23 16:36:13:791 当前线程:Thread-1获得了写锁,count:1

从日志时间上可以很清晰的看到,两个线程进行串行执行,证明了读和写之间是互斥的。

三、小结

总结下来,ReadWriteLock有以下特点:

  • 允许多个线程在没有写入时同时读取,可以提高读取效率
  • 当存在写入情况时,只允许一个线程写入,其他线程进入排队等待
  • 适合读多写少的场景

对于同一个数据,有大量线程读取,但仅有少数线程修改,使用ReadWriteLock可以显著的提升程序并发执行效率。

例如,一个论坛的帖子,浏览可以看做读取操作,是非常频繁的,而回复可以看做写入操作,它是不频繁的,这种情况就可以使用ReadWriteLock来实现。

本文主要围绕ReadWriteLock的基本使用做了一次知识总结,如果有不正之处,请多多谅解,并欢迎批评指出。

四、参考

1、https://www.cnblogs.com/xrq730/p/4855631.html

2、https://www.liaoxuefeng.com/wiki/1252599548343744/1306581002092578

多线程系列(十) -ReadWriteLock用法详解的更多相关文章

  1. DAX/PowerBI系列 - 查询参数用法详解(Query Parameter)

    PowerBI  - 查询参数用法详解(Query Parameter) 很多人都不知道查询参数用来干啥,下面总结一下日常项目中常用的几个查询参数的地方.(本人不太欢hardcode的东西) 使用查询 ...

  2. 多线程java的concurrent用法详解(转载)

    我们都知道,在JDK1.5之前,Java中要进行业务并发时,通常需要有程序员独立完成代码实现,当然也有一些开源的框架提供了这些功能,但是这些依然没有JDK自带的功能使用起来方便.而当针对高质量Java ...

  3. java多线程管理 concurrent包用法详解

        我们都知道,在JDK1.5之前,Java中要进行业务并发时,通常需要有程序员独立完成代码实现,当然也有一些开源的框架提供了这些功能,但是这些依然没有JDK自带的功能使用起来方便.而当针对高质量 ...

  4. 精通awk系列(12):awk getline用法详解

    回到: Linux系列文章 Shell系列文章 Awk系列文章 getline用法详解 除了可以从标准输入或非选项型参数所指定的文件中读取数据,还可以使用getline从其它各种渠道获取需要处理的数据 ...

  5. ASP.NET MVC深入浅出系列(持续更新) ORM系列之Entity FrameWork详解(持续更新) 第十六节:语法总结(3)(C#6.0和C#7.0新语法) 第三节:深度剖析各类数据结构(Array、List、Queue、Stack)及线程安全问题和yeild关键字 各种通讯连接方式 设计模式篇 第十二节: 总结Quartz.Net几种部署模式(IIS、Exe、服务部署【借

    ASP.NET MVC深入浅出系列(持续更新)   一. ASP.NET体系 从事.Net开发以来,最先接触的Web开发框架是Asp.Net WebForm,该框架高度封装,为了隐藏Http的无状态模 ...

  6. jQuery 事件用法详解

    jQuery 事件用法详解 目录 简介 实现原理 事件操作 绑定事件 解除事件 触发事件 事件委托 事件操作进阶 阻止默认事件 阻止事件传播 阻止事件向后执行 命名空间 自定义事件 事件队列 jque ...

  7. Ubuntu kill命令用法详解

    转自:Ubuntu kill命令用法详解 1. kill   作用:根据进程号杀死进程   用法: kill [信号代码] 进程ID   root@fcola:/# ps -ef | grep sen ...

  8. Android GLSurfaceView用法详解(二)

    输入如何处理       若是开发一个交互型的应用(如游戏),通常需要子类化 GLSurfaceView,由此可以获取输入事件.下面有个例子: java代码: package eoe.ClearTes ...

  9. 教程-Delphi中Spcomm使用属性及用法详解

    Delphi中Spcomm使用属性及用法详解 Delphi是一种具有 功能强大.简便易用和代码执行速度快等优点的可视化快速应用开发工具,它在构架企业信息系统方面发挥着越来越重要的作用,许多程序员愿意选 ...

  10. selenium用法详解

    selenium用法详解 selenium主要是用来做自动化测试,支持多种浏览器,爬虫中主要用来解决JavaScript渲染问题. 模拟浏览器进行网页加载,当requests,urllib无法正常获取 ...

随机推荐

  1. [转帖]超线程 Smt 究竟可以快多少?

    https://www.51cto.com/article/686171.html 刚才我们关闭SMT是把CPU10-CPU19全关了,只留下每对里面的1个CPU,也就是留下了CPU0-CPU9. 默 ...

  2. [转帖]Redis 核心篇:唯快不破的秘密

    文章系转载,方便整理和归纳,源文地址:https://z.itpub.net/article/detail/4B5A03BDDBE9A2BC3E080E278FE4D21E 以下文章来源于码哥字节 , ...

  3. [转帖]linux下 进程io队列,IO队列和IO调度

    IO体系概览 先看看本文主题IO调度和IO队列处于整个IO体系的哪个位置,这个IO体系是非常重要的,了解IO体系我们可以对整个IO过程有个全面的认识.虽然一下两下并不清楚IO体系各个部分的细节,但是我 ...

  4. 《SAIS Supervising and Augmenting Intermediate Steps for Document-Level Relation Extraction》论文阅读笔记

    代码   原文地址   预备知识: 1.什么是标记索引(token indices)? 标记索引是一种用于表示文本中的单词或符号的数字编码.它们可以帮助计算机理解和处理自然语言.例如,假如有一个字典{ ...

  5. css3文字阴影和盒子阴影

    文字阴影 文字阴影的语法格式: text-shadow:水平向右的偏移值 向下的偏移值 迷糊度 阴影的颜色,水平向右的偏移值 向下的偏移值 迷糊度 阴影的颜色; 可以有多个阴影,但是在实际的项目中最多 ...

  6. 插件时间格式处理moment如何使用

    第1步下载插件 cnpm i moment -S 第2步 在main.js中去使用 在main.js中 注册全局过滤器 fmtdata是等会你用的 可以自定义 fmtdata直接可以调用.是一个过滤器 ...

  7. 原生js拖拽元素(onmouseup不能够触发的原因)

    我们经常会遇见拖拽某一个元素的场景,拖拽也是很常用的: 这次拖拽遇见一个问题,有时在拖拽的时候吗,鼠标松开,元素仍然可以拖拽: 经过查阅资料,发现: 会触发H5原生的拖拽事件.并且不会监听到onmou ...

  8. 【VMware vSAN】使用命令行从vSAN集群中移除ESXi主机并加入到新的vSAN集群。

    说明 本文只是陈述了一种方法,不必评判谁对谁错谁好谁坏,选择适合自己的即可. 环境 站点名称 vCenter版本 vSAN集群 集群主机 主机版本 磁盘组 vcsa67.lab.com vCenter ...

  9. Go中字符串处理:fmt.Sprintf与string.Builder的比较

    在Go语言中,我们通常会遇到两种主要的方式来处理和操作字符串:使用fmt.Sprintf函数和string.Builder类型.尽管两者都可以实现字符串的格式化和连接,但它们在性能和用法上有一些关键区 ...

  10. Go 泛型之明确使用时机与泛型实现原理

    目录 一.引入 二.何时适合使用泛型? 场景一:编写通用数据结构时 场景二:函数操作的是 Go 原生的容器类型时 场景三:不同类型实现一些方法的逻辑相同时 三.Go 泛型实现原理 Stenciling ...