JUC高级十二-ReentrantLock、ReentrantReadWriteLock、StampedLock

news2024/11/14 18:40:04

无锁→独占锁→读写锁→邮戳锁

1. 关于锁的大厂面试题

  • 你知道Java里面有哪些锁?
  • 你说你用过读写锁,锁饥饿问题是什么?
  • 有没有比读写锁更快的锁?
  • StampedLock知道吗?(邮戳锁/票据锁)
  • ReentrantReadWriteLock有锁降级机制策略你知道吗?

2. 读写锁

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

2.1 ReentrantReadWriteLock分析

由ReentrantLock与ReentrantReadWriteLock的对比

  • ReentrantLock实现的是Lock接口
  • ReentrantReadWriteLock实现的是ReadWriteLock接口

image-20230423153136682

ReadWriteLock接口

  • 有一个读锁一个写锁

image-20230423153304047

2. 读写锁演变

无锁无序->加锁->读写锁

image-20230423154226987

  1. 无锁时期一旦多线程,肯定会出现线程安全问题
  2. 加锁时期读读操作也只能单线程操作-----解决了线程安全问题但是性能太低
  3. 读写锁时期:高并发的情况下大部分的请求都是查询请求,即读多写少的情况,如果读读也互斥了那么效率极低,而且读操作不会影响数据一致性可以不互斥—读写锁诞生.
    • 缺点:
      1. 因为读写锁读写互斥,假设100个线程99个都是读线程只有一个写线程,写线程很难抢到机会,就会出现锁饥饿问题
      2. 锁降级:为了让当前线程感知到数据的变化,目的是保证数据可见性,写锁会降级为写锁(写后立刻读)

2.3 『读写锁』意义和特点

  • 『读写锁ReentrantReadWriteLock』并不是真正意义上的读写分离,它只允许读读共存,而读写和写写依然是互斥的,
  • 大多实际场景是“读/读”线程间并不存在互斥关系,只有"读/写"线程或"写/写"线程间的操作需要互斥的。因此引入ReentrantReadWriteLock。
  • 一个ReentrantReadWriteLock同时只能存在一个写锁但是可以存在多个读锁,但不能同时存在写锁和读锁
    • 也即一个资源可以被多个读操作访问或一个写操作访问,但两者不能同时进行。
  • 只有在读多写少情境之下,读写锁才具有较高的性能体现。

2.4 ReentrantLock加锁实例

package site.zhourui.readWriteLockAndStampedLock;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
class MyResource {
    Map<String,String> map = new HashMap<>();
    //=====ReentrantLock 等价于 =====synchronized
    Lock lock = new ReentrantLock();
    //=====ReentrantReadWriteLock 一体两面,读写互斥,读读共享
    ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

    public void write(String key,String value)
    {
        lock.lock();
        try
        {
            System.out.println(Thread.currentThread().getName()+"\t"+"---正在写入");
            map.put(key,value);
            //暂停毫秒
            try { TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println(Thread.currentThread().getName()+"\t"+"---完成写入");
        }finally {
            lock.unlock();
        }
    }
    public void read(String key)
    {
        lock.lock();
        try
        {
            System.out.println(Thread.currentThread().getName()+"\t"+"---正在读取");
            String result = map.get(key);
            //后续开启注释修改为2000,演示一体两面,读写互斥,读读共享,读没有完成时候写锁无法获得
            try { TimeUnit.MILLISECONDS.sleep(200); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println(Thread.currentThread().getName()+"\t"+"---完成读取result:"+result);
        }finally {
            lock.unlock();
        }
    }

}

public class ReadWriteLockDemo {

    public static void main(String[] args)
    {
        MyResource myResource = new MyResource();

        for (int i = 1; i <=10; i++) {
            int finalI = i;
            new Thread(() -> {
                myResource.write(finalI +"", finalI +"");
            },String.valueOf(i)).start();
        }

        for (int i = 1; i <=10; i++) {
            int finalI = i;
            new Thread(() -> {
                myResource.read(finalI +"");
            },String.valueOf(i)).start();
        }

    }
}

执行结果:

不光读写互斥,读读也是互斥的

image-20230423161721493

2.5 ReentrantReadWriteLock实例实现读读共享

只需要把ReentrantLock换成对应的读写锁

image-20230423162057055

执行结果:

读写仍然互斥,但是读读不会互斥了

image-20230423162216884

2.5.1 读锁占用时写锁是无法获取的

将读锁sleep参数修改为2000

image-20230423162814562

在执行10个读线程后再次写线程

image-20230423162915561

执行结果:

在读锁没有释放的时,写锁是不能获取的

image-20230423162948429

2.6 读写锁的锁降级

image-20230423163152485

从写锁→读锁,ReentrantReadWriteLock可以降级

锁降级:将写入锁降级为读锁(类似Linux文件读写权限理解,就像写权限要高于读权限一样)

2.6.1 读写锁降级规则

  • 锁降级:遵循获取写锁→再获取读锁→再释放写锁的次序,写锁能够降级成为读锁。
  • 如果一个线程占有了写锁,在不释放写锁的情况下,它还能占有读锁,即写锁降级为读锁。

image-20230423163423459

2.6.2 读写锁降级的作用

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

2.6.3 代码实例

package site.zhourui.readWriteLockAndStampedLock;

import java.util.concurrent.locks.ReentrantReadWriteLock;

public class LockDownGradingDemo {
    public static void main(String[] args)
    {
        ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();

        ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
        ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();


        writeLock.lock();
        System.out.println("-------正在写入");


        readLock.lock();
        System.out.println("-------正在读取");

        writeLock.unlock();

    }

}

执行结果:

当,当前线程没有释放写锁时,仍然能够该线程仍然能够获取写锁:锁降级

注意之前的读写互斥是说的多线程之间,这里是一个线程的情况获取写锁还能获取读锁这叫锁降级

image-20230423163734702

2.7 锁不可升级

如果有线程在读,那么写线程是无法获取写锁的,是悲观锁的策略

image-20230423165025359

2.8 总结

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

因此,
分析读写锁ReentrantReadWriteLock,会发现它有个潜在的问题:读锁全完,写锁有望;写锁独占,读写全堵;
如果有线程正在读,写线程需要等待读线程释放锁后才能获取写锁,见前面Case《code演示LockDownGradingDemo》即ReadWriteLock读的过程中不允许写,只有等待线程都释放了读锁,当前线程才能获取写锁,也就是写入必须等待,这是一种悲观的读锁,o(╥﹏╥)o,人家还在读着那,你先别去写,省的数据乱。

2.9 Oracle公司ReentrantWriteReadLock源码总结

锁降级 下面的示例代码摘自ReentrantWriteReadLock源码中:
ReentrantWriteReadLock支持锁降级,遵循按照获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁,不支持锁升级。
解读在最下面:

image-20230423165504760

image-20230423165534559

3. 邮戳锁StampedLock 也叫票据锁

  • StampedLock是JDK1.8中新增的一个读写锁,也是对JDK1.5中的读写锁ReentrantReadWriteLock的优化
  • stamp(戳记,long类型)
    • 代表了锁的状态。当stamp返回零时,表示线程获取锁失败。并且,当释放锁或者转换锁的时候,都要传入最初获取的stamp值
  • 它是由锁饥饿问题引出

3.1 锁饥饿问题

ReentrantReadWriteLock实现了读写分离,但是一旦读操作比较多的时候,想要获取写锁就变得比较困难了,假如当前1000个线程,999个读,1个写,有可能999个读取线程长时间抢到了锁,那1个写线程就悲剧了 因为当前有可能会一直存在读锁,而无法获得写锁,根本没机会写

3.1.1 如何缓解锁饥饿问题?

  • 使用“公平”策略可以一定程度上缓解这个问题
    • 但是“公平”策略是以牺牲系统吞吐量为代价的
  • 邮戳锁
    • 乐观读来解决问题

3.2 StampedLock 和ReentrantReadWriteLock的区别

  • ReentrantReadWriteLock

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

  • StampedLock

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

3.3 StampedLock的特点

  • 所有获取锁的方法,都返回一个邮戳(Stamp),Stamp为零表示获取失败,其余都表示成功;
  • 所有释放锁的方法,都需要一个邮戳(Stamp),这个Stamp必须是和成功获取锁时得到的Stamp一致;
  • StampedLock是不可重入的,危险(如果一个线程已经持有了写锁,再去获取写锁的话就会造成死锁)
  • StampedLock有三种访问模式
    • ①Reading(读模式):功能和ReentrantReadWriteLock的读锁类似
    • ②Writing(写模式):功能和ReentrantReadWriteLock的写锁类似
    • ③Optimistic reading(乐观读模式):无锁机制,类似于数据库中的乐观锁,支持读写并发,很乐观认为读取时没人修改,假如被修改再实现升级为悲观读模式

3.4 示例

3.4.1 传统读写功能

package site.zhourui.readWriteLockAndStampedLock;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.StampedLock;

public class StampedLockDemo {
    static int number = 37;
    static StampedLock stampedLock = new StampedLock();

    public void write()
    {
        long stamp = stampedLock.writeLock();
        System.out.println(Thread.currentThread().getName()+"\t"+"=====写线程准备修改");
        try
        {
            number = number + 13;
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            stampedLock.unlockWrite(stamp);
        }
        System.out.println(Thread.currentThread().getName()+"\t"+"=====写线程结束修改");
    }

    //悲观读
    public void read()
    {
        long stamp = stampedLock.readLock();
        System.out.println(Thread.currentThread().getName()+"\t come in readlock block,4 seconds continue...");
        //暂停几秒钟线程
        for (int i = 0; i <4 ; i++) {
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println(Thread.currentThread().getName()+"\t 正在读取中......");
        }
        try
        {
            int result = number;
            System.out.println(Thread.currentThread().getName()+"\t"+" 获得成员变量值result:" + result);
            System.out.println("写线程没有修改值,因为 stampedLock.readLock()读的时候,不可以写,读写互斥");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            stampedLock.unlockRead(stamp);
        }
    }
    public static void main(String[] args)
    {
        StampedLockDemo resource = new StampedLockDemo();

        new Thread(() -> {
            resource.read();
        },"readThread").start();

        new Thread(() -> {
            resource.write();
        },"writeThread").start();
    }

}

执行结果:

读完再写,实现了读(悲观读)写锁的功能

image-20230423181558430

3.4.2 乐观读写功能

package site.zhourui.readWriteLockAndStampedLock;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.StampedLock;

public class StampedLockDemo {
    static int number = 37;
    static StampedLock stampedLock = new StampedLock();

    public void write()
    {
        long stamp = stampedLock.writeLock();
        System.out.println(Thread.currentThread().getName()+"\t"+"=====写线程准备修改");
        try
        {
            number = number + 13;
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            stampedLock.unlockWrite(stamp);
        }
        System.out.println(Thread.currentThread().getName()+"\t"+"=====写线程结束修改");
    }

    //悲观读
    public void read()
    {
        long stamp = stampedLock.readLock();
        System.out.println(Thread.currentThread().getName()+"\t come in readlock block,4 seconds continue...");
        //暂停几秒钟线程
        for (int i = 0; i <4 ; i++) {
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println(Thread.currentThread().getName()+"\t 正在读取中......");
        }
        try
        {
            int result = number;
            System.out.println(Thread.currentThread().getName()+"\t"+" 获得成员变量值result:" + result);
            System.out.println("写线程没有修改值,因为 stampedLock.readLock()读的时候,不可以写,读写互斥");
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            stampedLock.unlockRead(stamp);
        }
    }

    //乐观读
    public void tryOptimisticRead()
    {
        long stamp = stampedLock.tryOptimisticRead();
        int result = number;
        //间隔4秒钟,我们很乐观的认为没有其他线程修改过number值,实际靠判断。
        System.out.println("4秒前stampedLock.validate值(true无修改,false有修改)"+"\t"+stampedLock.validate(stamp));
        for (int i = 1; i <=4 ; i++) {
            try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
            System.out.println(Thread.currentThread().getName()+"\t 正在读取中......"+i+
                    "秒后stampedLock.validate值(true无修改,false有修改)"+"\t"
                    +stampedLock.validate(stamp));
        }
        if(!stampedLock.validate(stamp)) {
            System.out.println("有人动过--------存在写操作!");
            stamp = stampedLock.readLock();
            try {
                System.out.println("从乐观读 升级为 悲观读");
                result = number;
                System.out.println("重新悲观读锁通过获取到的成员变量值result:" + result);
            }catch (Exception e){
                e.printStackTrace();
            }finally {
                stampedLock.unlockRead(stamp);
            }
        }
        System.out.println(Thread.currentThread().getName()+"\t finally value: "+result);
    }

    public static void main(String[] args)
    {
        StampedLockDemo resource = new StampedLockDemo();

        new Thread(() -> {
//            resource.read();
            resource.tryOptimisticRead();
        },"readThread").start();

        // 2秒钟时乐观读失败,6秒钟乐观读取成功resource.tryOptimisticRead();,修改切换演示
        try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { e.printStackTrace(); }

        new Thread(() -> {
            resource.write();
        },"writeThread").start();
    }
}

执行结果:

在读线程执行时,写线程仍然能抢到锁

image-20230423182103657

3.4.2.1 乐观读未升级情况

image-20230423182545533

执行结果:

将写的在读后6秒才开启,读操作早在写操作执行之前就执行完成了,所以不会出现重新悲观读

image-20230423182731408

3.5 缺点

  • StampedLock 不支持重入,没有Re开头
  • StampedLock 的悲观读锁和写锁都不支持条件变量(Condition),这个也需要注意。
  • 使用 StampedLock一定不要调用中断操作,即不要调用interrupt() 方法

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

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

相关文章

Spring的创建与Bean对象的存取

文章目录&#xff1a;一.Spring项目的创建1.先创建maven项目 2.添加国内源 3.添加spring依赖 4.创建spring配置文件 5.创建启动类 二.Bean对象的创建和读取1.Bean对象的创建与存储方式&#xff08;1&#xff09;类注解 &#xff08;2&#xff09;方法注解 &#xff08;3&#x…

Java中的容器大杂烩-集合

Java中的容器大杂烩-集合 一、 集合引入二、集合框架体系三 、Collection 接口四 、List集合4.1 ArrayList类4.2 LinkedList类4.3 Vector类4.4 ArrayList 、 LinkedList 和 Vector区别 五 、Set集合5.1 HashSet类5.2 TreeSet5.3 LinkedHashSet类 六、List和Set区别七 、Map集合…

制造业巨头遭黑客勒索400万美元,企业如何防范勒索病毒攻击?

4月7日&#xff0c;台湾电脑制造商微星&#xff08;简称MSI&#xff09;发布公开声明&#xff0c;证实其部分网络信息系统遭受了勒索病毒攻击。一个名为“Money Message”的新网络黑客团伙称其从微星的网络系统中窃取了1.5TB的数据&#xff0c;其中包括微星数据库的屏幕截图、源…

YAML /Excel /CSV?自动化测试测试数据管理应用,测试老鸟总结...

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 自动化测试无论是…

webhub123 前端技术社区和技术交流学习网站导航

整理了学习前端技术可以参考学习和技术交流的一些网站集合&#xff0c;全部收录到 webhub123 前端技术社区和技术交流学习网站导航http://​www.webhub123.com/#/home/detail?projectHashid30929575&ownerUserid22053727 整理后的效果如下&#xff0c;我们已经按照不同类…

React Props

state 和 props 主要的区别在于 props 是不可变的&#xff0c;而 state 可以根据与用户交互来改变。 所以&#xff0c;有些容器组件需要定义 state 来更新和修改数据。 而子组件只能通过 props 来传递数据。 props 使用 Demo.js &#xff1a; import React from reactfunct…

智能学习 | MATLAB实现ACO-BP多变量时间序列预测(蚁群算法优化BP神经网络)

智能学习 | MATLAB实现ACO-BP多变量时间序列预测(蚁群算法优化BP神经网络) 目录 智能学习 | MATLAB实现ACO-BP多变量时间序列预测(蚁群算法优化BP神经网络)预测效果基本介绍程序设计参考资料预测效果 基本介绍 MATLAB实现ACO-BP多变量时间序列预测(蚁群算法优化BP神经网络…

数据集合注入

集合注入 前面我们已经能完成引入数据类型和简单数据类型的注入&#xff0c;但是还有一种数据类型集合&#xff0c;集合中既可 以装简单数据类型也可以装引用数据类型&#xff0c;对于集合&#xff0c;在Spring中该如何注入呢? 先来回顾下&#xff0c;常见的集合类型有哪些…

Vue电商项目--应用开发详解

vue-cli脚手架初始化项目 首先&#xff0c;页面上新建一个文件夹。然后打开命令端口 vue create app 选择Default ([Vue 2] babel, eslint) 然后把项目拖拽到vscode中。项目目录看一下 脚手架项目的目录 node_modules:放置项目依赖的地方 public:一般放置一些共用的静态资源&a…

数据采集方式有哪些,都有什么特点?

随着中国社会的进一步发展&#xff0c;各行各业都得到了一定程度的进步。进入21世纪以来&#xff0c;大数据、人工智能等行业的飞速发展&#xff0c;极大的带动全社会进步。但是&#xff0c;在一些传统行业内部&#xff0c;还存在这落后的东西&#xff0c;例如数据采集还是沿用…

【机器学习】P24 随机森林算法(1) 实现 “鸢尾花” 预测

随机森林算法 Random Forest Algorithm 随机森林算法随机森林算法实现分类鸢尾花 随机森林算法 随机森林&#xff08;Random Forest&#xff09;算法 是一种 集成学习&#xff08;Ensemble Learning&#xff09;方法&#xff0c;它由多个决策树组成&#xff0c;是一种分类、回…

OS实战笔记(8)-- 设置基本OS基本工作环境

本笔记会搭建OS实战所需的虚拟机环境&#xff0c;主要是创建好虚拟机&#xff0c;设置虚拟机启动硬盘&#xff0c;在启动盘上安装Grub。 由于本专题是个人在业余时间除了Unity学习之外做的&#xff0c;没有时间和精力去解答具体的问题。本笔记中的实验个人在做的过程中除了遇到…

17.集合

集合 集合类是Java数据结构的实现。Java的集合类是java.util包中的重要内容&#xff0c;它允许以各种方式将元素分组&#xff0c;并定义了各种使这些元素更容易操作的方法。Java集合类是Java将一些基本的和使用频率极高的基础类进行封装和增强后再以一个类的形式提供。集合类是…

整理现有的wiki私服项目

文章目录 核心功能现有项目wikijsBookStackmediawikiTiddlyWikigollumdokuwikixwiki 总结参考 核心功能 查找编辑 在线/离线内链【核心】代码高亮图表、表达式生成多媒体&#xff08;图片、音频、视频&#xff09;管理 协作&#xff08;用户管理模式/github模式&#xff09; 修…

JVM 关键点详解

一&#xff0c;JVM 的主要组成部分及其作用 JVM包含两个子系统和两个组件&#xff0c;两个子系统为Class loader(类装载)、Execution engine(执行引擎); 两个组件为Runtime data area(运行时数据区)、Native Interface(本地接口)。 Class loader(类装载): 根据给定的全限定名类…

【Linux网络】部署YUM仓库及NFS服务

部署YUM仓库及NSF服务 一、YUM仓库1.1、YUM仓库概述1.2准备安装来源1.3在软件仓库加载非官方RPM包组1.4yum与apt 二、配置yam源与制作索引表2.1配置FTP源2.2配置国内在线yum源2.3在线源与本地源同时使用2.4建立软件包索引关系表的三种方法 三、nfs共享存储服务3.1安装软件&…

LVS负载均衡群集——NAT模式实操

1.1 群集的的定义及意义 群集的定义 Cluster&#xff0c;集群&#xff08;也称群集&#xff09;由多台主机构成&#xff0c;但对外只表现为一一个整体&#xff0c;只提供一-个访问入口(域名或IP地址)&#xff0c; 相当于一台大型计算机。 群集的作用 对于企业服务的的性能提升…

数学知识四

容斥原理 S表示面积&#xff0c;下面公式可求出不相交的面积 2个圆的公式是这样 4个圆的面积是 总面积-所有俩俩相交的面积所有三三相交的面积-四四相交的面积&#xff0c;公式里加和减互相出现。 从n个集合里面挑一个一直到从n个集合里面挑n个 1-10中&#xff0c;能被2&#x…

【KingSCADA】如何创建新应用

大家好&#xff0c;我是雷工! 今天学习使用KingSCADA3.8创建一个新的应用&#xff0c;以下为学习过程和操作笔记。 一、前言 KingSCADA3.8集成开发环境是基于工程的应用管理模式&#xff0c;实现了对多个应用的集中开发和管理的功能&#xff0c;一个工程可以同时管理多个应用…

【WinForm】Android手机群控工具-桌面程序开发实现

如何将手下多个Android手机统一管理起来呢&#xff0c;这里是用通过终端输入adb命令来实现控制多个手机的&#xff0c;具体怎么做&#xff0c;接下来给讲一讲。 使用adb工具包 首先&#xff0c;需要准备一套工具&#xff0c;以下是adb工具套件&#xff0c;是在Android SDK开发…