【JUC】读写锁

读写锁

文章目录

  • 读写锁
    • 1. ReentrantReadWriteLock概述
    • 2. 编码演示
      • 2.1 ReentrantLock实现
      • 2.2 ReentrantReadWriteLock实现
    • 3. ReentrantReadWriteLock
      • 3.1 锁降级
      • 3.2 锁降级的必要性
      • 3.3 饥饿问题
    • 4. StampedLock(邮戳锁也叫票据锁)
      • 4.1 特点
      • 4.2 三种访问模式
      • 4.3 缺点
      • 4.4 编码示例

1. ReentrantReadWriteLock概述

读写锁定义为:一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程

大多实际场景是“读/读”线程间并不存在互斥关系,只有"读/写"线程或"写/写"线程间的操作需要互斥的。因此引入ReentrantReadWriteLock。

特点:

  • 它只允许读读共存,而读写和写写依然是互斥的
  • 一个ReentrantReadWriteLock同时只能存在一个写锁但是可以存在多个读锁,但不能同时存在写锁和读锁(切菜还是拍蒜选一个)也即一个资源可以被多个读操作访问或一个写操作访问,但两者不能同时进行

ReentrantReadWriteLock:读写互斥,读读共享,读没有完成时候其它线程无法获得写锁

2. 编码演示

有一个缓存类Cache,需要进行读和写操作,如何使其性能达到最高(StampedLock请看StampedLock章节)?

2.1 ReentrantLock实现

public class Main {static class Cache {private final ReentrantLock lock = new ReentrantLock();Map<String, String> map = new HashMap<>();public void write(String key, String value){lock.lock();try {System.out.println(Thread.currentThread().getName() + "正在写入");map.put(key, value);TimeUnit.MILLISECONDS.sleep(500);System.out.println(Thread.currentThread().getName() + "完成写入");} catch (InterruptedException e) {throw new RuntimeException(e);} finally {lock.unlock();}}public String read(String key) {lock.lock();try {System.out.println(Thread.currentThread().getName() + "正在读取");TimeUnit.MILLISECONDS.sleep(200);System.out.println(Thread.currentThread().getName() + "完成读取");return map.get(key);} catch (InterruptedException e) {throw new RuntimeException(e);} finally {lock.unlock();}}}public static void main(String[] args) throws InterruptedException {CountDownLatch countDownLatch = new CountDownLatch(10);long startTime = System.currentTimeMillis();Cache cache = new Cache();for (int i = 0; i < 5; i++) {int finalI = i;new Thread(() -> {cache.write(String.valueOf(finalI), String.valueOf(finalI));countDownLatch.countDown();}, String.valueOf(i)).start();}for (int i = 5; i < 10; i++) {int finalI = i;new Thread(() -> {cache.read(String.valueOf(finalI));countDownLatch.countDown();}, String.valueOf(i)).start();}countDownLatch.await();long endTime = System.currentTimeMillis();System.out.println("总消耗时间:" + (endTime - startTime) + "ms");}
}

输出

0正在写入
0完成写入
1正在写入
1完成写入
2正在写入
2完成写入
4正在写入
4完成写入
3正在写入
3完成写入
5正在读取
5完成读取
6正在读取
6完成读取
7正在读取
7完成读取
8正在读取
8完成读取
9正在读取
9完成读取
总消耗时间:3632ms

分析:读写,写写之间确实需要锁进行互斥控制,但是读读之间不需要锁控制,而是可以同时执行的,如何优化读读?引出ReentrantReadWriteLock

2.2 ReentrantReadWriteLock实现

public class Main {static class Cache {//        private final ReentrantLock lock = new ReentrantLock();private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();Map<String, String> map = new HashMap<>();public void write(String key, String value){
//            lock.lock();readWriteLock.writeLock().lock();try {System.out.println(Thread.currentThread().getName() + "正在写入");map.put(key, value);TimeUnit.MILLISECONDS.sleep(500);System.out.println(Thread.currentThread().getName() + "完成写入");} catch (InterruptedException e) {throw new RuntimeException(e);} finally {
//                lock.unlock();readWriteLock.writeLock().unlock();}}public String read(String key) {
//            lock.lock();readWriteLock.readLock().lock();try {System.out.println(Thread.currentThread().getName() + "正在读取");TimeUnit.MILLISECONDS.sleep(200);System.out.println(Thread.currentThread().getName() + "完成读取");return map.get(key);} catch (InterruptedException e) {throw new RuntimeException(e);} finally {
//                lock.unlock();readWriteLock.readLock().unlock();}}}public static void main(String[] args) throws InterruptedException {CountDownLatch countDownLatch = new CountDownLatch(10);long startTime = System.currentTimeMillis();Cache cache = new Cache();for (int i = 0; i < 5; i++) {int finalI = i;new Thread(() -> {cache.write(String.valueOf(finalI), String.valueOf(finalI));countDownLatch.countDown();}, String.valueOf(i)).start();}for (int i = 5; i < 10; i++) {int finalI = i;new Thread(() -> {cache.read(String.valueOf(finalI));countDownLatch.countDown();}, String.valueOf(i)).start();}countDownLatch.await();long endTime = System.currentTimeMillis();System.out.println("总消耗时间:" + (endTime - startTime) + "ms");}
}

输出

0正在写入
0完成写入
2正在写入
2完成写入
1正在写入
1完成写入
4正在写入
4完成写入
3正在写入
3完成写入
5正在读取
6正在读取
8正在读取
9正在读取
7正在读取
6完成读取
8完成读取
9完成读取
5完成读取
7完成读取
总消耗时间:2798ms

3. ReentrantReadWriteLock

允许多个线程同时读,但是只允许一个线程写,在线程获取到写锁的时候,其他写操作和读操作都会处于阻塞状态,读锁和写锁也是互斥的,所以在读的时候是不允许写的,读写锁比传统的synchronized速度要快很多,原因就是在于ReentrantReadWriteLock支持读并发,读读可以共享

3.1 锁降级

目的:锁降级是为了让当前线程感知到数据的变化,目的是保证数据可见性

ReentrantReadWriteLock锁降级: 将写入锁降级为读锁(类似Linux文件读写权限理解,就像写权限要高于读权限一样),程度变强叫做升级,反之叫做降级

写锁的降级,降级成为了读锁

  1. 如果同一个线程持有了写锁,在没有释放写锁的情况下,它还可以继续获得读锁。这就是写锁的降级,降级成为了读锁
  2. 规则惯例,先获取写锁,然后获取读锁,再释放写锁的 次序
  3. 如果释放了写锁,那么就完全转换为读锁。

在这里插入图片描述

public class Main {static int resource = 0;public static void main(String[] args) throws InterruptedException {ReentrantReadWriteLock lock = new ReentrantReadWriteLock();ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();ReentrantReadWriteLock.ReadLock readLock = lock.readLock();writeLock.lock();readLock.lock();resource++;writeLock.unlock();System.out.println("先写锁后读锁且交替:" + resource);readLock.unlock();readLock.lock();writeLock.lock();System.out.println("先读锁后写锁且交替:" + resource);resource++;readLock.unlock();writeLock.unlock();}
}

输出且程序锁死,不会终止程序

先写锁后读锁且交替:1

使用写锁的过程中可以加入读锁,反之则不行;用读锁的过程中,必须读完才能再使用写锁,否则会导致程序卡死

写锁和读锁是互斥的(这里的互斥是指线程间的互斥,当前线程可以获取到写锁又获取到读锁,但是获取到了读锁不能继续获取写锁),这是因为读写锁要保持写操作的可见性。因为,如果允许读锁在被获取的情况下对写锁的获取,那么正在运行的其他读线程无法感知到当前写线程的操作。

综上,

  • ReentrantReadWriteLock读的过程不允许写,只有等待读线程都释放了读锁后才能获取写锁,也就是写入必须等待,这是一种悲观的读锁

3.2 锁降级的必要性

CacheData {Object data;volatile boolean cacheValid;final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();void processCachedData() {rwl.readLock().lock(); // R1if (!cacheValid) {rwl.readLock().unlock(); // R1rwl.writeLock().lock(); // W1try {data = new Object();cacheValid = true;rwl.readLock(); // R2 在释放写锁前立刻抢夺读锁} finally {rwl.writeLock().unlock(); // W1}}try {// use(data);} finally {rwl.readLock().unlock(); // R2}}
}
  1. 代码中声明了一个volatile类型的cacheValid变量,保证其可见性

  2. 首先获取读锁,如果cache不可用,则释放读锁。获取写锁,在更改数据之前,再检查一次cachevalid的值,然后修改数据,将achevalid置为true,然后在释放写锁前立刻抢夺获取读锁;此时,cache中数据可用,处理cache中数据,最后释放读锁。这个过程就是一个完整的锁降级的过程,目的是保证数据可见性

总结:一句话,同一个线程自己持有写锁时再去拿读锁,其本质相当于重入。

  • 如果违背锁降级的步骤
    • 如果当前的线程C在修改完cache中的数据后,没有获取读锁而是直接释放了写锁,那么假设此时另一个线程D获取了写锁并修改了数据,那么C线程无法感知到数据已被修改,则数据出现错误
  • 如果遵循锁降级的步骤
    • 线租C在释放写锁之前获取读锁,那么线程D在获取写锁时将被阻塞,直到线理C完成数据处理过程。释放读锁。这样可以保证返回的数据是这次更新的数据,该机制是专门为了缓存设计的

3.3 饥饿问题

由上诉可知,当持有读锁时,将无法获取写锁,那么当大量读锁请求竞争资源时,写操作必须等待读锁全部释放才能获取到写锁,导致一直无法写入,这便是写饥饿问题,该问题将由JDK8中的StampedLock解决

4. StampedLock(邮戳锁也叫票据锁)

ReentrantReadWriteLock的读锁被占用的时候,其他线程尝试获取写锁的时候会被阻塞。但是,StampedLock采取乐观获取锁后,其他线程尝试获取写锁时不会被阻塞,这其实是对读锁的优化,所以,在获取乐观读锁后,还需要对结果进行校验

  • stamp,戳记,long类型,代表了锁的状态。当stamp返回零时,表示线程获取锁失败。并且当释放锁或转换锁的时候,都要传入最初获取的stamp值
  • 对短的只读代码段,使用乐观模式通常可以减少争用并提高吞吐量
  • 该锁目前生产上还较少使用

4.1 特点

  • 所有获取锁的方法,都返回一个邮戳 (Stamp),Stamp为零表示获取失败,其余都表示成功
  • 所有释放锁的方法,都需要一个邮戳 (Stamp),这个Stamp必须是和成功获取锁时得到的Stamp一致
  • StampedLock是不可重入的,危险(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)

4.2 三种访问模式

  • Reading(读模式悲观):功能和ReentrantReadWriteLock的读锁类似
  • Writing(写模式):功能和ReentrantRerdWriteLock的写锁类似
  • Optimistic reading (乐观读模式): 无锁机制,类似于数据库中的乐观锁,支持读写并发,很乐观认为读取时没人修改,假如被修改再实现升级为悲观读模式

4.3 缺点

  • StampedLock不支持重入
  • StampedLock的悲观读和写锁都不支持条件变量(Condition)
  • 使用StampedLock一定不要调用中断操作,即不要调用interrupt()方法

4.4 编码示例

public class Main {static class Cache {private final StampedLock stampedLock = new StampedLock();Map<String, String> map = new HashMap<>();public void write(String key, String value){long lockStamped = stampedLock.writeLock();try {System.out.println(Thread.currentThread().getName() + "正在写入");map.put(key, value);TimeUnit.MILLISECONDS.sleep(500);System.out.println(Thread.currentThread().getName() + "完成写入");} catch (InterruptedException e) {throw new RuntimeException(e);} finally {stampedLock.unlockWrite(lockStamped);}}public String read(String key) {long optimisticLockStamped;do {optimisticLockStamped = stampedLock.tryOptimisticRead();System.out.println(Thread.currentThread().getName() + "正在读取");try {TimeUnit.MILLISECONDS.sleep(2000);} catch (InterruptedException e) {throw new RuntimeException(e);}if (stampedLock.validate(optimisticLockStamped)) {System.out.println(Thread.currentThread().getName() + "完成读取");return map.get(key);}System.out.println(Thread.currentThread().getName() + "读取失败,自旋");} while (true);}}public static void main(String[] args) throws InterruptedException {CountDownLatch countDownLatch = new CountDownLatch(15);long startTime = System.currentTimeMillis();Cache cache = new Cache();for (int i = 0; i < 5; i++) {int finalI = i;new Thread(() -> {cache.write(String.valueOf(finalI), String.valueOf(finalI));countDownLatch.countDown();}, String.valueOf(i)).start();}for (int i = 5; i < 10; i++) {int finalI = i;new Thread(() -> {cache.read(String.valueOf(finalI));countDownLatch.countDown();}, String.valueOf(i)).start();}for (int i = 10; i < 15; i++) {int finalI = i;new Thread(() -> {cache.write(String.valueOf(finalI), String.valueOf(finalI));countDownLatch.countDown();}, String.valueOf(i)).start();}countDownLatch.await();long endTime = System.currentTimeMillis();System.out.println("总消耗时间:" + (endTime - startTime) + "ms");}
}

输出:

0正在写入
5正在读取
7正在读取
8正在读取
6正在读取
9正在读取
0完成写入
4正在写入
4完成写入
3正在写入
3完成写入
2正在写入
2完成写入
7读取失败,自旋
7正在读取
5读取失败,自旋
6读取失败,自旋
6正在读取
8读取失败,自旋
8正在读取
9读取失败,自旋
9正在读取
5正在读取
1正在写入
1完成写入
10正在写入
10完成写入
11正在写入
11完成写入
12正在写入
8读取失败,自旋
8正在读取
7读取失败,自旋
7正在读取
9读取失败,自旋
9正在读取
5读取失败,自旋
5正在读取
6读取失败,自旋
6正在读取
12完成写入
13正在写入
13完成写入
14正在写入
14完成写入
9读取失败,自旋
9正在读取
5读取失败,自旋
7读取失败,自旋
6读取失败,自旋
6正在读取
8读取失败,自旋
8正在读取
7正在读取
5正在读取
5完成读取
7完成读取
8完成读取
6完成读取
9完成读取
总消耗时间:8073ms

读取过程中也可以写入,只要读取过程中存在写入,则该次读取失败,进行自旋,再次读取

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/109644.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2023年全网最新 Windows10 搭建 Python 环境教程

目录 一、查看计算机操作系统的位数二、安装Python2.1 下载Python安装包2.2 在Windows 64位系统中安装Python2.3 测试Python是否安装成功 三、Windows环境下安装第2个Python(不同版本) ----不需要安装多个Python版本的读者此小节可以忽略 一、查看计算机操作系统的位数 目前&a…

【脉冲通信】用于空间应用的飞秒脉冲通信的符号误码率模型研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

【C++笔记】多态的原理、单继承和多继承关系的虚函数表、 override 和 final、抽象类、重载、覆盖(重写)、隐藏(重定义)的对比

1.final关键字 引出&#xff1a;设计一个不能被继承的类。有如下方法&#xff1a; class A { private:A(int a0):_a(a){} public:static A CreateOBj(int a0){return A(a);} protected:int _a; } //简介限制&#xff0c;子类构成函数无法调用父类构造函数初始化 //子类的构造…

IDEA如何拉取gitee项目?

1.登录gitee 说明&#xff1a;打开idea&#xff0c;在设置上面搜索框输入gitee&#xff0c;然后登录gitee注册的账号。 2. 创建gitee仓库 说明&#xff1a;创建idea中的gitee仓库。 3.寻找项目文件 说明&#xff1a;为需要添加gitee仓库的项目进行添加。 4.项目右键 说明&a…

IPIDEA代理IP如何帮助企业采集市场信息

在当今数字化的时代&#xff0c;市场信息对于企业的发展至关重要。然而&#xff0c;如何高效地收集市场信息成为了每个企业都需要面对的问题。爬虫技术的出现为企业提供了一种高效、便捷的信息采集方式。然而&#xff0c;由于爬虫的请求频率较高&#xff0c;目标网站可能会将频…

vue使用pdf-dist实现pdf预览以及水印

vue使用pdf-dist实现pdf预览以及水印 一.使用pdf-dist插件将PDF文件转换为一张张canvas图片 npm install pdf-dist二.页面引入插件 const pdfJS require("pdfjs-dist"); pdfJS.GlobalWorkerOptions.workerSrc require("pdfjs-dist/build/pdf.worker.entry&…

浅谈云原生

目录 1. 云原生是什么&#xff1f; 2. 云原生四要素 2.1 微服务 2.2 容器化 2.3 DevOps 2.4 持续交付 3. 具体的云原生技术有哪些&#xff1f; 3.1 容器 (Containers) 3.2 微服务 (Microservices) 3.3 服务网格 (Service Meshes) 3.4 不可变基础设施 (Immutable Inf…

支持PC端、手机端、数据大屏端的Spring Cloud智慧工地云平台源码

技术架构&#xff1a;微服务JavaSpring Cloud VueUniApp MySql 智慧建筑工地云平台主要利用大数据、物联网等技术&#xff0c;整合工地信息、材料信息、工程进度等&#xff0c;实现对建筑项目的全程管理。它可以实现实时监测和控制&#xff0c;有效解决施工中的问题&#xff0c…

软件考试学习笔记(希赛)

软考学习笔记-软件设计师 1. 软考基本介绍1.1 软考分数制1.2软考考试分类介绍1.3软件考试报名网站1.4考试内容1.4.1上午考试内容-综合知识1.4.2下午考试内容-软件设计 2.数据的表示2.1进制转换2.1.1R进制------》十进制转换2.1.2十进制-----》R进制转换2.1.3二进制与八进制与16…

网络通信协议-HTTP、WebSocket、MQTT的比较与应用

在今天的数字化世界中&#xff0c;各种通信协议起着关键的作用&#xff0c;以确保信息的传递和交换。HTTP、WebSocket 和 MQTT 是三种常用的网络通信协议&#xff0c;它们各自适用于不同的应用场景。本文将比较这三种协议&#xff0c;并探讨它们的主要应用领域。 HTTP&#xff…

Gazebo仿真 【ROS: noetic】

参考链接&#xff1a;《ROS机器人开发实践》_胡春旭 目标&#xff1a; 了解如何使用URDF文件创建一个机器人模型&#xff0c;然后使用xacro文件优化该模型&#xff0c;并且放置到rvizArbotiX或Gazebo仿真环境中&#xff0c;以实现丰富的ROS功能。 4.5 Gazebo仿真环境 1&#x…

《动手学深度学习 Pytorch版》 9.1 门控循环单元(GRU)

我们可能会遇到这样的情况&#xff1a; 早期观测值对预测所有未来观测值具有非常重要的意义。 考虑一个极端情况&#xff0c;其中第一个观测值包含一个校验和&#xff0c;目标是在序列的末尾辨别校验和是否正确。在这种情况下&#xff0c;第一个词元的影响至关重要。我们希望有…

PS修改背景色,线框底图

1、打开图片&#xff0c;ctrlj复制一层 2、图像-调整-反相 3、ctrll调整色阶&#xff0c;将中间的色块向右移&#xff0c;灰色线和字体的会变黑

游戏类app有哪些变现方式?

游戏类app有多变现策略&#xff0c;一些是一些主要的方式&#xff1a;#APP广告变现# AdSet官方资讯-上海神蓍信息科技有限公司 一、游戏销售 一次性购买&#xff1a;玩家支付一次性费用购买游戏&#xff0c;之后可以免费游玩。这种模式常见于主机游戏和PC游戏。 游戏包&…

VR数字政务为我们带来了哪些便捷之处?

每每在政务大厅排队的时候&#xff0c;总是在想未来政务服务会变成什么样子呢&#xff1f;会不会变得更加便捷呢&#xff1f;今天我们就来看看VR数字政务&#xff0c;能够为我们带来哪些便捷之处吧&#xff01; 传统的政务服务中&#xff0c;不仅办事流程复杂&#xff0c;而且每…

单链表的相关操作(初阶--寥寥万字不成敬意)

目录 链表的概念 链表的相关操作&#xff1a; 链表的创建&#xff1a; 打印链表&#xff1a; 申请新节点&#xff1a; 链表的尾插&#xff1a; &#xff01;&#xff01;&#xff01;对于传参中二级指针的解释&#xff1a; 链表的头插&#xff1a; 链表的尾删&#xff…

保护隐私就是在保护自己!如何在Android上更改应用程序权限

如果你关心隐私&#xff0c;知道如何在Android上更改应用程序权限将成为一项非常重要的技能。即使是最好的安卓应用程序也可以对手机的功能和数据进行广泛的访问&#xff0c;因此准确控制它们的使用范围会有所帮助。 一旦你在手机上加载了应用程序&#xff0c;你可能会注意到它…

【LeetCode】35. 搜索插入位置

1 问题 给定一个排序数组和一个目标值&#xff0c;在数组中找到目标值&#xff0c;并返回其索引。如果目标值不存在于数组中&#xff0c;返回它将会被按顺序插入的位置。 请必须使用时间复杂度为 O(log n) 的算法。 示例 1: 输入: nums [1,3,5,6], target 5 输出: 2 示例…

在线课堂知识系统源码系统+前端+后端完整搭建教程

大家好啊&#xff0c;今天罗峰来给大家分享一款在线课堂知识系统源码系统。这款系统的功能十分强大。可以使用手机随时随地地学习&#xff0c;有专业的导师答疑解惑。支持视频&#xff0c;音频&#xff0c;图文章节。以下是部分核心代码图&#xff1a; 系统特色功能一览&#x…

Linux上Docker的安装以及作为非运维人员应当掌握哪些Docker命令

目录 前言 1、安装步骤 2、理解镜像和容器究竟是什么意思 2.1、为什么我们要知道什么是镜像&#xff0c;什么是容器&#xff1f; 2.2、什么是镜像&#xff1f; 2.3、什么是容器&#xff1f; 2.4、Docker在做什么&#xff1f; 2.5、什么是镜像仓库&#xff1f; 2、Dock…