一、摘要

在上篇文章中,我们讲到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. rust入坑指南之ownership

    作者:京东零售 王梦津 I. 前言 Rust,不少程序员的白月光,这里我们简单罗列一些大牛的评价. Linus Torvalds:Linux内核的创始人,对Rust的评价是:"Rust的主要 ...

  2. 2023年了,做SEO还有必要吗?

    作者:京东科技 吴磊 搜索引擎工作原理 在搜索引擎网站的后台会有一个非常庞大的数据库,里面存储了海量的关键词,而每个关键词又对应着很多网址,这些网址是被称之为"搜索引擎蜘蛛"或&q ...

  3. Ant Design Vue栅格Grid的使用

    栅格系统的设计理念 建议横向排列的盒子数量最多四个,最少一个. 因此我们的span一般设置为3或者4 小屏幕的话就另当别论了 栅格系统的简单介绍 1.通过row在水平方向建立一组column(简写 c ...

  4. Map结构映射,避免每一个字段赋值

    var query1 = (from fore in forecastShippingDate join ship in shipOutOfStock on fore.Id equals ship.F ...

  5. Unity SetActive Event

    网上查了一下Unity的SetActive变化事件没有找到,我想到用另一种思路来实现这个事件通知,它可用来调试是何处把某个gameobject隐藏掉了 Unity提供了这两个函数,OnEnable,O ...

  6. PostFix+Dovecot 部署邮件系统

    Postfix 是一种电子邮件服务器是一个开放源代码的软件. Postfix 是MTA邮件传输代理软件.是sendmail提供替代品的一个尝试,在Internet世界中,大部分的电子邮件都是通过sen ...

  7. C/C++ 内存遍历与KMP特征搜索

    内存遍历,枚举数据,实现特征码扫描. 内存遍历: 每次读入4096字节,然后每16个字符换一次行,遍历内存 0x00401000 - 0x7FFFFFFF. #include <stdio.h& ...

  8. CSS文本,字体设置与元素边框,阴影,显示模式

    什么是元素的显示模式 网页中的标签很多,在不同的地方使用不同类型的标签. 元素显示模式就是标签以什么方式进行显示.如:div自己占一行,span一行可以放多个. HTML元素一般分为块元素和行内元素两 ...

  9. P4145 上帝造题的七分钟 2 / 花神游历各国 题解

    题目链接:上帝造题的七分钟2/花神游历各国 差不多的题:[Ynoi Easy Round 2023] TEST_69 注意到对某个点来说暴力单点即为反复的:\(x=\sqrt{x}\),最终为 \(1 ...

  10. hv_balloon: Balloon request will be partially fulfilled. Balloon floor reached

    windows 的hyper-v 安装了 centos 或者龙蜥 操作系统,会一直提示这个信息: hv_balloon: Balloon request will be partially fulfi ...