读写锁ReentrantReadWriteLockStampLock详解

        如何设计一把读写锁?ReentrantReadWriteLock

    读写锁设计思路

        读写状态的设计

        设计的精髓:用一个变量如何维护多种状态

        在 ReentrantLock 中,使用 Sync ( 实际是 AQS )的 int 类型的 state 来表示同步状态,表示锁被一个线程重复获取的次数。但是,读写锁 ReentrantReadWriteLock 内部维护着一对读写锁,如果要用一个变量维护多种状态,需要采用“按位切割使用”的方式来维护这个变量,将其切分为两部分:高16为表示读,低16为表示写。

        分割之后,读写锁是如何迅速确定读锁和写锁的状态呢?通过位运算。假如当前同步状态为S,那么:

       1. 写状态,等于 S & 0x0000FFFF(将高 16 位全部抹去)。 当写状态加1,等于S+1.

       2. 读状态,等于 S >>> 16 (无符号补 0 右移 16 位)。当读状态加1,等于S+(1<<16)

        根据状态的划分能得出一个推论:S不等于0时,当写状态(S&0x0000FFFF)等于0时,则读状态(S>>>16)大于0,即读锁已被获取。\

        代码实现

        java.util.concurrent.locks.ReentrantReadWriteLock.Sync

exclusiveCount(int c)
静态方法,获得持有写状态的锁的次数。
sharedCount(int c)
静态方法,获得持有读状态的锁的数量。不同于写锁,读锁可以同时被多个线程持有。而 每个线程持有的读锁支持重入的特性,所以需要对每个线程持有的读锁的数量单独计数,这就需要用到HoldCounter 计数器

        HoldCounter 计数器

        读锁的内在机制其实就是一个共享锁。一次共享锁的操作就相当于对HoldCounter 计数器的操作。获取共享锁,则该计数器 + 1,释放共享锁,该计数器 - 1。只有当线程获取共享锁后才能对共享锁进行释放、重入操作。

        通过 ThreadLocalHoldCounter 类,HoldCounter 与线程进行绑定。HoldCounter 是绑定线程的一个计数器,而 ThreadLocalHoldCounter 则是线程绑定的 ThreadLocal。

        HoldCounter是用来记录读锁重入数的对象

        ThreadLocalHoldCounter是ThreadLocal变量,用来存放不是第一个获取读锁的线程的其他线程的读锁重入数对象

1. 读写锁介绍

        读写锁ReadWriteLock,顾名思义一把锁分为读与写两部分,读锁允许多个线程同时获得,因为读操作本身是线程安全的。而写锁是互斥锁,不允许多个线程同时获得写锁。并且读与写操作也是互斥的。读写锁适合多读少写的业务场景。

2. ReentrantReadWriteLock介绍 

        针对这种场景,JAVA的并发包提供了读写锁ReentrantReadWriteLock,它内部,维护了一对相关的锁,一个用于只读操作,称为读锁;一个用于写入操作,称为写锁,描述如下:

线程进入读锁的前提条件:
        1、没有其他线程的写锁
        2、没有写请求或者有写请求,但调用线程和持有锁的线程是同一个
线程进入写锁的前提条件:
        1、没有其他线程的读锁
        2、没有其他线程的写锁
而读写锁有以下三个重要的特性:
        1、 公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。
        2、可重入 :读锁和写锁都支持线程重入。以读写线程为例: 读线程获取读锁后,能够再次获取读锁。写线程在获取 写锁之后能够再次获取写锁,同时也可以获取读锁。
        3、锁降级 遵循获取写锁、再获取读锁最后释放写锁的次序,写锁能够降级成为读锁

2.1 ReentrantReadWriteLock的使用

读写锁接口ReadWriteLock
一对方法,分别获得读锁和写锁 Lock 对象。
ReentrantReadWriteLock类结构
ReentrantReadWriteLock是可重入的读写锁实现类 。在它内部,维护了一对相关的锁,一个用于
只读操作,另一个用于写入操作。只要没有 Writer 线程,读锁可以由多个 Reader 线程同时持有。也
就是说, 写锁是独占的,读锁是共享的。

2.2 如何使用读写锁

package com.laoyang.Thread.readwritelock;import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;public class ReadWriteLockExample {private final ReadWriteLock lock = new ReentrantReadWriteLock();private final Lock readLock = lock.readLock();//读锁  共享锁private final Lock writeLock = lock.writeLock();//写锁private final String[] data = new String[10];public void write(int index, String value) {writeLock.lock();try {System.out.println(Thread.currentThread().getName()+"获取写锁");try {Thread.sleep(5000);} catch (InterruptedException e) {throw new RuntimeException(e);}read(2);data[index] = value;} finally {System.out.println(Thread.currentThread().getName()+"释放写锁");writeLock.unlock();}}public String read(int index) {readLock.lock();try {System.out.println(Thread.currentThread().getName()+"获取读锁");try {Thread.sleep(3000);} catch (InterruptedException e) {throw new RuntimeException(e);}return data[index];} finally {System.out.println(Thread.currentThread().getName()+"释放读锁");readLock.unlock();}}public static void main(String[] args) {ReadWriteLockExample rwl = new ReadWriteLockExample();// 测试读读,读写,写写场景
//        new Thread(()->{
//            //rwl.read(2);
//            rwl.write(2,"rwl");
//          //  rwl.read(2);
//        }).start();//测试读读
//        new Thread(()->rwl.read(2)).start();
//        new Thread(()->rwl.read(2)).start();
//        new Thread(()->rwl.write(2,"rwl")).start();//测试写写new Thread(()->rwl.write(2,"rwl")).start();new Thread(()->rwl.write(2,"rwl")).start();}
}

2.3 注意事项

        读锁不支持条件变量

        重入时升级不支持:持有读锁的情况下去获取写锁,会导致获取永久等待
        重入时支持降级: 持有写锁的情况下可以去获取读锁

2.4 应用场景

        以下是使用ReentrantReadWriteLock的常见场景:

                1、读多写少:ReentrantReadWriteLock适用于读操作比写操作频繁的场景,因为它允许多个读线程同时访问共享数据,而写操作是独占的。

                2、缓存:ReentrantReadWriteLock可以用于实现缓存,因为它可以有效地处理大量的读操作,同时保护缓存数据的一致性。

读写锁在缓存中的应用
package com.laoyang.Thread.readwritelock;import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;public class Cache {static Map<String, Object> map = new HashMap<String, Object>();static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();static Lock r = rwl.readLock();static Lock w = rwl.writeLock();// 获取一个key对应的valuepublic static final Object get(String key) {r.lock();try {return map.get(key);} finally {r.unlock();}}// 设置key对应的value,并返回旧的valuepublic static final Object put(String key, Object value) {w.lock();try {return map.put(key, value);} finally {w.unlock();}}// 清空所有的内容public static final void clear() {w.lock();try {map.clear();} finally {w.unlock();}}}
        上述示例中,Cache组合一个非线程安全的HashMap作为缓存的实现,同时使用读写锁的读锁和写锁来保证Cache是线程安全的。在读操作get(String key)方法中,需要获取读锁,这使得并发访问该方法时不会被阻塞。写操作put(String key,Object value)方法和clear()方法,在更新 HashMap时必须提前获取写锁,当获取写锁后,其他线程对于读锁和写锁的获取均被阻塞,而 只有写锁被释放之后,其他读写操作才能继续。 Cache使用读写锁提升读操作的并发性,也保证每次写操作对所有的读写操作的可见性,同时简化了编程方式。

3. 锁降级 

        锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。锁降级可以帮助我们拿到当前线程修改后的结果而不被其他线程所破坏,防止更新丢失。

3.1 锁降级的使用示例       
        因为数据不常变化,所以多个线程可以并发地进行数据处理,当数据变更后,如果当前线程感知到数 据变化,则进行数据的准备工作,同时其他处理线程被阻塞,直到当前线程完成数据的准备工作。
        锁降级中读锁的获取是否必要呢?答案是必要的。主要是为了保证数据的可见性 ,如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(记作线程T)获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新。
        RentrantReadWriteLock不支持锁升级 (把持读锁、获取写锁,最后释放读锁的过程)。目的也是保证数据可见性,如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新了数据,则其更新对其他获取到读锁的线程是不可见的。
美团面试三连
面试官:了解锁吗?
小明:了解,还经常用过。
面试官:说说synchronized和lock的区别吧
小明:synchronized是可重入锁,由于lock是一个接口,重入性取决于实现,synchronized不支持中
断,而lock可以。。。。。。。。。。。。。。。。 面试官:好了,那有没有比这两种锁更快的锁呢?
小明:在读多写少的情况下,读写锁比他们的效率更高。
面试官:那有没有比读写锁更快的锁呢?
小明:。。。。。。。。。。

 

4. StampedLock介绍 

        如果我们深入分析ReentrantReadWriteLock,会发现它有个潜在的问题:如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,即读的过程中不允许写,这是一种悲观的读锁。
为了进一步提升并发执行效率,Java 8引入了新的读写锁: StampedLock
        StampedLock和ReentrantReadWriteLock相比,改进之处在于: 读的过程中也允许获取写锁后 写入!在原先读写锁的基础上新增了一种叫乐观读(Optimistic Reading)的模式。该模式并不会加锁,所以不会阻塞线程,会有更高的吞吐量和更高的性能。

        4.1 StampedLock的使用

        StampLock三种访问模式
        Writing(独占写锁):writeLock 方法会使线程阻塞等待独占访问,可类比ReentrantReadWriteLock 的写锁模式,同一时刻有且只有一个写线程获取锁资源;
        Reading(悲观读锁):readLock方法,允许多个线程同时获取悲观读锁,悲观读锁与独占写锁互斥,与乐观读共享。
        Optimistic Reading(乐观读):这里需要注意了, 乐观读并没有加锁 ,也就是不会有 CAS 机制并且没有阻塞线程。仅当当前未处于 Writing 模式 tryOptimisticRead 才会返回非 0 的邮戳(Stamp), 如果在获取乐观读之后没有出现写模式线程获取锁,则在方法validate返回 true ,允许多个线程获取乐观读以及读锁,同时允许一个写线程获取写锁。
        最好使用如下模板,否则容易出现bug:
思考: 为何 StampedLock 性比 ReentrantReadWriteLock 好?
关键在于StampedLock 提供的乐观读。ReentrantReadWriteLock 支持多个线程同时获取读锁,但是当多个线程同时读的时候,所有的写线程都是阻塞的。 StampedLock 的乐观读允许一个写线程获 取写锁,所以不会导致所有写线程阻塞,也就是当读多写少的时候,写线程有机会获取写锁,减少了 线程饥饿的问题,吞吐量大大提高。
         思考: 允许多个乐观读和一个写线程同时进入临界资源操作,那读取的数据可能是错的怎么办?
        
乐观读不能保证读取到的数据是最新的,所以将数据读取到局部变量的时候需要通过lock.validate(stamp) 校验是否被写线程修改过,若是修改过则需要上悲观读锁,再重新读取数据到局部变量。

4.2 演示乐观读

package com.laoyang.Thread.readwritelock;import java.util.concurrent.locks.StampedLock;/*** @author Fox*/
public class StampedLockTest {public static void main(String[] args) throws InterruptedException {Point point = new Point();//第一次移动x,ynew Thread(() -> point.move(100, 200)).start();Thread.sleep(100);new Thread(() -> point.distanceFromOrigin()).start();Thread.sleep(500);//第二次移动x,ynew Thread(()-> point.move(300,400)).start();}
}class Point {private final StampedLock stampedLock = new StampedLock();private double x;private double y;public void move(double deltaX, double deltaY) {// 获取写锁long stamp = stampedLock.writeLock();System.out.println("获取到writeLock");try {x += deltaX;y += deltaY;} finally {// 释放写锁stampedLock.unlockWrite(stamp);System.out.println(("释放writeLock"));}}/*** 计算当前坐标到原点的距离* 乐观读* @return*/public double distanceFromOrigin() {// 获得一个乐观读  (无锁)long stamp = stampedLock.tryOptimisticRead();// 注意下面两行代码不是原子操作// 假设x,y = (100,200)// 此处已读取到x=100,但x,y可能被写线程修改为(300,400)double currentX = x;System.out.println("第1次读,x:{},y:{},currentX:{}" + x + y+ currentX);try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}
//         此处已读取到y,如果没有写入,读取是正确的(100,200)
//         如果有写入,读取是错误的(100,400)double currentY = y;System.out.println("第2次读,x:{},y:{},currentX:{},currentY:{}"+x+ y+ currentX+ currentY);// 检查乐观读锁后是否有其他写锁发生if (!stampedLock.validate(stamp)) {// 获取一个悲观读锁stamp = stampedLock.readLock();try {currentX = x;currentY = y;System.out.println("最终结果,x:{},y:{},currentX:{},currentY:{}"+x+ y+ currentX+ currentY);} finally {// 释放悲观读锁stampedLock.unlockRead(stamp);}}return Math.sqrt(currentX * currentX + currentY * currentY);}}

4.3 使用场景和注意事项

        对于读多写少的高并发场景 StampedLock的性能很好,通过乐观读模式很好的解决了写线程“饥饿”的问题,我们可以使用StampedLock 来代替ReentrantReadWriteLock ,但是需要注意的是 StampedLock 的功能仅仅是 ReadWriteLock 的子集,在使用的时候,还是有几个地方需要注意一下。

       1、 StampedLock 写锁是不可重入的,如果当前线程已经获取了写锁,再次重复获取的话就会死锁,使用过程中一定要注意;

        2、悲观读、写锁都不支持条件变量 Conditon ,当需要这个特性的时候需要注意;

        3. 如果线程阻塞在 StampedLock 的 readLock() 或者 writeLock() 上时,此时调用该阻塞线程的 interrupt() 方法,会导致 CPU 飙升。所以,使用 StampedLock 一定不要调用中断操作,如果需要支持中断功能,一定使用可中断的悲观读锁 readLockInterruptibly() 和写锁 writeLockInterruptibly()。

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

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

相关文章

ChatGPT AIGC 完成Excel跨多表查找操作vlookup+indirect

VLOOKUP和INDIRECT的组合在Excel中用于跨表查询,其中VLOOKUP函数用于在另一张表中查找数据,INDIRECT函数则用于根据文本字符串引用不同的工作表。具体操作如下: 1.假设在工作表1中,A列有你要查找的值,B列是你希望查询的工作表名称。 2.在工作表1的C列输入以下公式:=VLO…

Unity基础课程之物理引擎6-关于物理材质的使用和理解

每个物体都有着不同的摩擦力。光滑的冰面摩擦力很小&#xff0c;而地毯表面的摩擦力则很大。另外每种材料也有着不同的弹性&#xff0c;橡皮表面的弹性大&#xff0c;硬质地面的弹性小。在Unity中这些现象都符合日常的理念。虽然从原理上讲&#xff0c;物体的摩擦力和弹性有着更…

【交付高质量,用户高增长】-用户增长质量保证方法论 | 京东云技术团队

前言 俗话说&#xff0c;“测试是质量的守护者”&#xff0c;但单凭测试本身却远远不够。大多数情况下&#xff0c;测试像“一面镜子”&#xff0c;照出系统的面貌&#xff0c;给开发者提供修改代码的依据&#xff0c;这个“照镜子”的过程&#xff0c;就是质量评估的过程&…

在 VSCode 中使用 PlantUML

最近&#xff0c;因为工作需要绘制一些逻辑图&#xff0c;我自己现在使用的是 PlantUML 或者 mermaid&#xff0c;相比之下前者更加强大。不过它的环境也麻烦一些&#xff0c;mermaid 在一些软件上已经内置了。但是 PlantUML 一般需要自己本地安装或者使用远程服务器&#xff0…

Paddle GPU版本需要安装CUDA、CUDNN

完整的教程 深度学习环境配置&#xff1a;linuxwindows系统下的显卡驱动、Anaconda、Pytorch&Paddle、cuda&cudnn的安装与说明 - 知乎这篇文档的内容是尽量将深度学习环境配置(使用GPU)所需要的内容做一些说明&#xff0c;由于笔者只在windows和linux下操作过&#xf…

浏览器本地存储之Cookie和webStorage

浏览器本地存储主要包括 Cookie 和 Web Storage 两种机制。它们都是用来在客户端存储数据&#xff0c;以便在浏览器会话之间保持信息或在同一会话中的页面之间共享信息。 一、Cookie 1.1 概念 cookie是客户端与服务器端进行会话使用的一个能够在浏览器本地化存储的技术。简言…

nocos注册中心使用教程

1.下载和安装 进入到官网下载就好了 解压 启动 2.新建提供者模块 2.1新建提供者模块cloudalibaba-provider-payment9001 2.1.1在父项目中新加入依赖 <dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-alibaba-depend…

2022 年中职组“ 网络安全 ”赛项-web加固阶段题目

前言 大家好&#xff0c;本章节我将复现一次web加固阶段的操作&#xff0c;给大家看看该怎么操作和截图的具体事项&#xff0c;懂的大佬可以在评论区留言改进&#xff0c;感谢大家的支持&#xff01;接下来就跟随我的步伐一起来操作吧&#xff01; 阶段题目概览 环境搭建 底层…

【Eclipse】Plug-in Development 插件的安装

先按路线找到需要的页面&#xff1a;eclipse–Window–Preferences–Java–Editor–Content Assist 在Work with框中输入&#xff1a;http://download.eclipse.org/releases/2019-06 PS&#xff1a;后面的2019-06是eclipse发行的时间 选择&#xff1a;General Purpose Tools 下…

rhel8 nmcli学习

rhel8我自己用过的配置网路方法有以下几个&#xff1a; &#xff08;1)手动配置ifcfg文件&#xff0c;通过NM来生效。 (2)手动配置ifcfg文件&#xff0c;通过重启NetworkManager.service生效。 (3)通过NM自带工具配置网络&#xff0c;比如nmcli。 (4)使用命令 nutui命令&am…

4x4矩阵键盘设计Verilog矩阵式键盘控制,视频/代码

名称&#xff1a;4x4矩阵键盘设计Verilog矩阵式键盘控制 软件&#xff1a;Quartus 语言&#xff1a;Verilog 代码功能&#xff1a; 键盘控制电路设计&#xff0c;设计一个4x4矩阵式键盘控制电路&#xff0c;并实现按键的显示。 演示视频&#xff1a;4x4矩阵键盘设计Verilo…

【Java】jvm 元空间、常量池(了解)

JDK1.8 以前的 HotSpot JVM 有方法区&#xff0c;也叫永久代&#xff08;permanent generation&#xff09;方法区用于存放已被虚拟机加载的类信息&#xff0c;常量、静态遍历&#xff0c;即编译器编译后的代码JDK1.7 开始了方法区的部分移除&#xff1a;符号引用&#xff08;S…

docker之Harbor私有仓库

目录 一、什么是Harbor 二、Harbor的特性 三、Harbor的构成 1、六个组件 2、七个容器 四、私有镜像仓库的上传与下载 五、部署docker-compose服务 把项目中的镜像数据进行打包持久数据&#xff0c;如镜像&#xff0c;数据库等在宿主机的/data/目录下&#xff0c; 一、什么…

Kafka 开启SASL/SCRAM认证 及 ACL授权(二)ACL

Kafka 开启SASL/SCRAM认证 及 ACL授权(二)ACL。 官网地址:https://kafka.apache.org/ kafka authentorization:https://docs.confluent.io/platform/current/kafka/authorization.html 一、开启ZK ACL(可选,内网环境,用户无机器访问权限时) 给kafka meta都加上zk的ac…

Android 内容提供者和内容观察者:数据共享和实时更新的完美组合

任务要求 一个作为ContentProvider提供联系人数据另一个作为Observer监听联系人数据的变化&#xff1a; 1、创建ContactProvider项目&#xff1b; 2、在ContactProvider项目中用Sqlite数据库实现联系人的读写功能&#xff1b; 3、在ContactProvider项目中通过ContentProvid…

网络基础(2)

UDP 1.传输层2.再谈端口号3.端口号范围划分4.认识知名端口号(Well-Know Port Number)5.netstat6.pidof7.UDP协议端格式8.UDP的特点9.面向数据报10.UDP的缓冲区11.UDP使用注意事项12.基于UDP的应用层协议 &#x1f31f;&#x1f31f;hello&#xff0c;各位读者大大们你们好呀&am…

kettle应用-从数据库抽取数据到excel

本文介绍使用kettle从postgresql数据库中抽取数据到excel中。 首先&#xff0c;启动kettle 如果kettle部署在windows系统&#xff0c;双击运行spoon.bat或者在命令行运行spoon.bat 如果kettle部署在linux系统&#xff0c;需要执行如下命令启动 chmod x spoon.sh nohup ./sp…

【论文精读】NMP: End-to-end Interpretable Neural Motion Planner

toc 1 背景信息 团队&#xff1a;Uber&#xff0c;多伦大大学 年份&#xff1a;2019 论文链接&#xff1a;https://arxiv.org/abs/2101.06679 2 Motivation 深度学习方案受限于累积误差suffers from the compounding error&#xff0c;而且可解释性差interpretability is d…

【Java 进阶篇】JavaScript DOM 编程:理解文档对象模型

在 web 开发中&#xff0c;DOM&#xff08;文档对象模型&#xff09;是一个重要的概念。DOM 是一种将网页文档表示为树状结构的方式&#xff0c;允许开发者使用 JavaScript 来访问和操作网页的内容。本篇博客将详细介绍 DOM&#xff0c;包括什么是 DOM、如何访问 DOM 元素、如何…

Raven2靶机渗透

文章目录 主机扫描web渗透 主机扫描 先ip a查看一下kali虚拟机本机ip&#xff0c;然后用nmap -sn扫描同网段的ip&#xff1a; nmap -sn 192.168.101.0/24如图&#xff0c;扫描到靶机IP为192.168.101.129&#xff1a; 扫描到靶机IP后探测开放的服务&#xff1a; nmap -A 19…