【JUC】七、读写锁

文章目录

  • 1、读写锁
  • 2、读写锁的体验
  • 3、读写锁的特点
  • 4、锁的演变
  • 5、读写锁的降级
  • 6、复习:悲观锁和乐观锁

1、读写锁

JUC下的锁包的ReadWriteLock接口,以及其实现类ReentrantReadWriteLock

在这里插入图片描述

  • ReadWriteLock 维护了一对相关的锁,即读锁写锁,使得并发和吞吐相比一般的排他锁有了很大提升
  • 读锁属于共享锁
  • 写锁属于独占锁
  • 相比前面的ReentrantLock适用于一般场合,ReadWriteLock 适用于读多写少的场景

关于ReadWriteLock接口的两个方法:

  • 返回用于读的锁
Lock readLock()
  • 返回用于写的锁
Lock writeLock()

2、读写锁的体验

先看没有读写锁时,开多个线程对同一个资源进行读和写:

/资源类
class MyCache{//map模拟redisprivate volatile Map<String,Object> map = new HashMap<>();//写public void put(String key,Object value){System.out.println(Thread.currentThread().getName() + "线程正在进行写操作==>" + key);//暂停一会儿try {TimeUnit.MICROSECONDS.sleep(300);} catch (InterruptedException e) {e.printStackTrace();}//放数据map.put(key,value);System.out.println(Thread.currentThread().getName() + "线程写完了==>" + key);}//取public Object get(String key){Object result = null;System.out.println(Thread.currentThread().getName() + "线程正在进行读操作-->" + key);//暂停一会儿try {TimeUnit.MICROSECONDS.sleep(300);} catch (InterruptedException e) {e.printStackTrace();}result = map.get(key);System.out.println(Thread.currentThread().getName() + "线程读完了-->" + key);return result;}
}

创建5个线程来读,5个线程来写:

public class ReadWriteLockDemo {public static void main(String[] args) {MyCache myCache = new MyCache();//创建5个线程来写数据for (int i = 1; i < 6; i++) {final int num = i;  //临时变量,直接put一个变量i报错new Thread(() -> {myCache.put(num +"",num+"");},String.valueOf(i)).start();}//创建5个线程来读数据for (int i = 1; i < 6; i++) {final int num = i;new Thread(() -> {myCache.get(num +"");},String.valueOf(i)).start();}}}

运行发现:没写完就开始读,此时肯定读不到
在这里插入图片描述

加入读锁和写锁:

//资源类
class MyCache{//map模拟redisprivate volatile Map<String,Object> map = new HashMap<>();//创建读写锁的对象private ReadWriteLock rwLock = new ReentrantReadWriteLock();//写public void put(String key,Object value){//添加写锁rwLock.writeLock().lock();try {System.out.println(Thread.currentThread().getName() + "线程正在进行写操作==>" + key);//暂停一会儿,别瞬间写完TimeUnit.MICROSECONDS.sleep(300);//放数据map.put(key,value);System.out.println(Thread.currentThread().getName() + "线程写完了==>" + key);} catch (InterruptedException e) {e.printStackTrace();} finally {//释放写锁rwLock.writeLock().unlock();}}//取public Object get(String key){//添加读锁rwLock.readLock().lock();Object result = null;try {System.out.println(Thread.currentThread().getName() + "线程正在进行读操作-->" + key);//暂停一会儿,别读完太快,以证明读锁确实可以共享TimeUnit.MICROSECONDS.sleep(300);result = map.get(key);System.out.println(Thread.currentThread().getName() + "线程读完了-->" + key);} catch (InterruptedException e) {e.printStackTrace();}finally {rwLock.readLock().unlock();}return result;}
}

和上面一样,再执行main:创建5个线程来读,5个线程来写:

在这里插入图片描述

3、读写锁的特点

  • 读读共享:允许多个线程同时对同一个资源进行读,不用等前面的线程释放读锁,后面的线程就能获取到读锁,并执行加了读锁的代码
  • 读写互斥:一个线程获取了读锁,未释放前,不允许另一个线程同时来获取写锁进行写操作
  • 写写互斥:不允许多个线程对同一个资源进行写,必须等到前面线程释放写的锁,后面的线程才能获取到写锁并执行加了写锁的代码

写个demo开两个线程去获取读锁和写锁,调试验证下,这里两个线程都不释放自己获取到的读锁或者写锁:

public class ReadWriteDemo2 {public static void main(String[] args) {//可重入读写锁ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();//读锁ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();//写锁ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();//获取读锁readLock.lock();System.out.println("reading....");new Thread(() -> {//另一线程获取写锁writeLock.lock();System.out.println("write....");}).start();//释放写锁//writeLock.unlock();//释放读锁//readLock.unlock();}
}

在这里插入图片描述

前面线程先获取写锁,另一线程去获取读锁:

public class ReadWriteDemo2 {public static void main(String[] args) {//可重入读写锁ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();//读锁ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();//写锁ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();//获取写锁writeLock.lock();System.out.println("write....");new Thread(() -> {//另一线程获取读锁readLock.lock();System.out.println("reading....");}).start();//释放写锁//writeLock.unlock();//释放读锁//readLock.unlock();}
}

在这里插入图片描述

同时获取读锁:

public class ReadWriteDemo2 {public static void main(String[] args) {//可重入读写锁ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();//读锁ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();//写锁ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();//获取读锁readLock.lock();System.out.println("reading....");new Thread(() -> {//另一线程也获取读锁readLock.lock();System.out.println("reading....");}).start();//释放写锁//writeLock.unlock();//释放读锁//readLock.unlock();}
}

在这里插入图片描述

同时获取写锁:

public class ReadWriteDemo2 {public static void main(String[] args) {//可重入读写锁ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();//读锁ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();//写锁ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();//获取写锁writeLock.lock();System.out.println("writing....");new Thread(() -> {//另一线程也获取读锁writeLock.lock();System.out.println("writing....");}).start();//释放写锁//writeLock.unlock();//释放读锁//readLock.unlock();}
}

在这里插入图片描述

4、锁的演变

无锁 ⇒ 独占锁 ⇒ 读写锁

在这里插入图片描述

5、读写锁的降级

前面提到,不同线程下,读读共享,读写互斥,写写互斥。

而同一线程中,在持有写锁未解锁的情况下,可以获取读锁。按照如下步骤:

在这里插入图片描述

在同一个线程中,写锁就被过渡降级到了读锁,读写锁的降级,其目的是为了解决,持有写锁时,其他线程无法获得读锁,影响性能。

public class ReadWriteDemo {public static void main(String[] args) {//可重入读写锁ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();//读锁ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();//写锁ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();//锁降级//1.获取写锁writeLock.lock();System.out.println("write....");//2.获取读锁readLock.lock();System.out.println("reading....");//3.释放写锁writeLock.unlock();//4.释放读锁readLock.unlock();}
}

如果写锁被释放时,执行读锁的线程非常多,而需要执行写锁的线程非常少,则会导致读锁一直被使用不被释放,从而造成写线程无法获取写锁,造成写线程一直等待获取,造成线程“饥饿”。这个就像某一站地铁,上来100个人,下1个人,结果车一停(类比锁一降级),100个人往进涌(类比其他线程可以获取读锁了),把地铁门堵到发车(好多线程,读锁半天没有全部释放完),导致这一个下车的人也愣是没下去(类比少数其他想获取写锁的线程半天获取不到,因为不同线程,读写互斥)。

补充:

可能会有个疑问,既然释放写锁,干嘛非要手里纂一个读锁后才释放写锁,为何不:

持有写锁 -> 释放写锁 -> 持有读锁 -> 释放读锁

这样的坑在于,你释放完写锁,被另一线程T拿到并写了些数据,等你再拿到读锁时(不是你一释放写锁就一定能给自己拿到读锁,不同线程,读写互斥!),读到的已经是被修改了N手的数据。降级是,你什么时候想要读锁,你就什么时候获取读锁(因为写锁在你手里,你主动的),而如果你先释放写锁,想再获取读锁,那就不是想要就能立马拿到的了。

6、复习:悲观锁和乐观锁

最后,梳理下其他相关的锁。

悲观锁

心态悲观,认为每次操作都会去修改数据,因此,次次操作前都上锁,即共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。如Java中的synchronized、ReentrantLock就属于悲观锁的范畴(独占锁)。

乐观锁

总是假设好的方向,即认为每次操作都不会修改数据,因此也不上锁,只是在更新时会去判断一下有没人在这期间更新过这个数据,这个"判断",可以使用版本号机制或者CAS算法。版本号即多维护个字段:

# version=version+1
# where xx=#{xx} and version=#{version}
update table set x=x+1, version=version+1 where id=#{id} and version=#{version};  

CAS算法,CAS 即compare and swap,比较与交换,是一种无锁算法,实现了不用锁的情况下进行多线程变量同步,也称非阻塞同步。其实现思路是一种自旋的思想,即不断的重试(这同时也是乐观锁的一个缺点,长时间不成功并重试CPU开销变大)。CAS算法的三个数:

  • 需要读写的内存值 V
  • 进行比较的值 A
  • 拟写入的新值 B

当且仅当 V 的值等于 A时,CAS通过原子方式用新值B来更新V的值,否则不会执行任何操作(比较和替换是一个原子操作)。

ABA问题:

如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然是A值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回A,那CAS操作就会误认为它从来没有被修改过。这个问题被称为CAS操作的 "ABA"问题。

根据悲观锁与乐观锁的特点,可以知道:

  • 悲观锁适用于多写少读的场景
  • 乐观锁适用于多读少写的场景

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

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

相关文章

网站分类seo怎么优化(如何调整有利于SEO排名)

网站分类seo怎么优化&#xff08;如何调整有利于SEO排名&#xff09; 这期分享一篇关于网站seo优化中关于网页分类排名优化的文章&#xff0c;看看原文来自 蝙蝠侠IT &#xff0c;原文标题网站分类&#xff0c;没有SEO标题&#xff0c;怎么办的文章介绍。 理想与现实总是充满矛…

计算属性与watch的区别,fetch与axios在vue中的异步请求,单文本组件使用,使用vite创建vue项目,组件的使用方法

7.计算属性 7-1计算属性-有缓存 模板中的表达式虽然很方便,但是只能做简单的逻辑操作,如果在模版中写太多的js逻辑,会使得模板过于臃肿,不利于维护,因此我们推荐使用计算属性来解决复杂的逻辑 <!DOCTYPE html> <html lang"en"> <head><meta …

msvcp140.dll文件的丢失原因以及五个解决办法

在计算机使用过程中&#xff0c;我们常常会遇到一些错误提示&#xff0c;其中之一就是“msvcp140.dll丢失”。这个错误通常会导致某些应用程序无法正常运行。为了解决这个问题&#xff0c;我们需要采取一些措施来修复丢失的msvcp140.dll文件。本文将介绍五个处理办法&#xff0…

正点原子阿尔法Linux开发板——MfgTool烧写工具烧写系统

WINDOWS系统下 前提准备 OTG、TTL的USB接口分别连接电脑 USB_OTG 用于烧写&#xff0c;USB_TTL 用于串口查看烧录进度 拨码拨至USB烧录——0100 0000 烧写 SECURE 软件连接串口CH340的COM&#xff08;软件安装激活&#xff09; 我的开发板是I.MX6ULL(EMMC)——双击"Li…

mysql操作 sql语句中的完整性约束有哪些,主键约束、外键约束、引用完整性约束,主键外键、唯一性

什么是约束&#xff1a;约束&#xff1a;就是约定哪些东西能填、怎么填&#xff1f;哪些东西不能填&#xff1f; 文章目录 前言&#xff1a;建表正文一、实体完整性约束1. 主键约束2. 唯一性约束3. 自增长约束4. 联合主键约束 二、域完整性约束三、引用完整性约束1. 外键约束 讲…

JVM虚拟机-虚拟机性能监控、故障处理工具

1基础故障处理工具 jps&#xff08;JVM Process Status Tool&#xff09;是&#xff1a;虚拟机进程状况工具 作用&#xff1a;可以列出正在运行的虚拟机进程&#xff0c;并显示虚拟机执行主类&#xff08;Main Class&#xff0c;main()函数所在的类&#xff09;名称以及这些进…

亚里士多德的思想统治西方世界,实体论证明人不是猪

苏格拉底、柏拉图、亚里士多德&#xff0c;并称古希腊三贤。 公元前384年&#xff0c;亚里士多德出生在马其顿的奴隶主家庭&#xff0c;父亲是马其顿国王腓力二世的宫廷御医。 公元前367年&#xff0c;亚里士多德迁居到雅典&#xff0c;进入柏拉图学园&#xff0c;跟随柏拉图…

如何找寻需要贷款的精准客户? 运营商大数据获客解决企业获客难题

如今&#xff0c;它在商业和日常生活中的使用越来越普遍。然而&#xff0c;当谈到大数据的使用时&#xff0c;它已经渗透到各个行业。我们如何才能基于大数据准确定位用户&#xff0c;从而达到获取客户的目的。 您可以对上述用户群体的上网行为和通话行为进行建模和分析&#…

【JavaEE初阶】 HTML基础详解

文章目录 &#x1f38b;什么是HTML&#xff1f;&#x1f340;HTML 结构&#x1f6a9;认识标签&#x1f6a9;HTML 文件基本结构&#x1f6a9;快速生成代码框架 &#x1f384;HTML 常见标签&#x1f6a9;注释标签&#x1f6a9;标题标签: h1-h6&#x1f6a9;段落标签: p&#x1f6…

美容院服务预约小程序的作用是什么

随着女性悦己消费增强&#xff0c;以及对自身容貌身材的高要求&#xff0c;美容院近些年迎来大发展&#xff0c;成为不少消费者前往的场所&#xff0c;无论大小&#xff0c;每个城市都汇聚着各式品牌&#xff0c;用户选择度较高&#xff0c;同时我们也观察到美容院经营痛点也很…

UnitTest测试框架详解

从软件架构的⻆度来说&#xff0c;测试最重要的步骤是在软件开发的时候界入比较好&#xff0c;所以在早期测试的界入&#xff0c;从软件经济学的⻆度上来说&#xff0c;发现的问题解决成本低&#xff0c;投入的资源比较少。因此&#xff0c;对一个测试的系统&#xff0c;开始最…

Linux C 编程入门 (GCC 和 Makefile的使用和编写)

Linux C 编程入门 在 Windows 下我们可以使用各种各样的 IDE 进行编程&#xff0c;比如强大的 Visual Studio。Ubuntu 下也有一些可以进行编程的工具&#xff0c;但是大多都只是编辑器&#xff0c;也就是只能进行代码编辑&#xff0c;如果要编译的话就需要用到 GCC 编译器&…