JUC-ReentrantLock,ReentrantReadWriteLock,StampedLock

news2024/11/19 19:23:27

1. 概述

前面介绍过了synchronized关键字作用的锁升级过程
无锁->偏向锁->轻量锁->重锁
下面再介绍实现Lock接口的锁的升级过程
无锁->独占锁(ReentrantLock,Synchronized)->读写锁(ReentranReadWriteLock)->邮戳锁(StampedLock)
并准备了一些问题,回顾一下自己对知识的掌握程度。

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

2. ReentrantLock

ReentantLock是可重入的独占锁。默认是非公平锁。
可重入:当一个线程持有锁后,在内部可以继续获取锁。
独占:是一种悲观锁,当一个线程持有锁的时候,其他线程会阻塞。
公平和非公平:在公平的机制下,线程会依次排队,放到等待队列中。排队获取锁。在非公平的机制下,新来的线程通过CAS获取锁,获取不到,才会进入等待队列。

2.1 ReentrantLock使用代码演示

public static void main(String[] args)
    {
        new Thread(() -> {
            lock.lock();
            try
            {
                System.out.println(Thread.currentThread().getName()+"\t ----come in外层调用");
                lock.lock();
                try
                {
                    System.out.println(Thread.currentThread().getName()+"\t ----come in内层调用");
                }finally {
                    lock.unlock();
                }

            }finally {
                // 由于加锁次数和释放次数不一样,第二个线程始终无法获取到锁,导致一直在等待。
                lock.unlock();// 正常情况,加锁几次就要解锁几次
            }
        },"t1").start();

        new Thread(() -> {
            lock.lock();
            try
            {
                System.out.println(Thread.currentThread().getName()+"\t ----come in外层调用");
            }finally {
                lock.unlock();
            }
        },"t2").start();
    }

2.2 ReentrantLock和Synchronized比较

  1. ReentrantLock是对象,synchronzied是关键字
  2. 两者都是独占锁。(悲观锁)
  3. ReentrantLock加锁后需要手动解锁try{//do something}finally{Lock.unlock();}。synchronized关键字超出同步块自动解锁。
  4. ReentrantLock更灵活,可以控制是否是公平锁。synchronized只能是非公平锁。

使用场景的区别:

2.2.1 synchronized

写冲突比较多,线程强冲突的场景。
自旋的概率比较大,会导致浪费CPU性能。

2.2.2 ReentrantLock

synchronized锁升级是不可逆的,进入重量级锁后性能会下降。
ReentrantReadWriteLock(注意不是ReentrantLock)可以使用读写锁,增加性能。

3. ReentrantReadWriteLock

可重入读写锁。上面的可重入锁在两个线程同时读的过程中会竞争。可重入读写锁可以允许多个线程同时读取同一个资源。只允许读读共存,读写,写写之间都是互斥的。适用于读读不互斥的场景。

3.1 具有锁降级的性质

锁降级可以理解为一种操作。具体操作为写锁持有后,在准备释放写锁的之前,当前线程继续持有读锁,然后释放写锁。

    writeLock.lock();
    //do something
    // 锁降级:释放写锁之前先只有读锁。
    readLock.lock(); // A 降级开始
    writeLock.unlock();// 注意执行完这一步,其他阻塞队列的头部的读线程才能进入。
    // 锁降级完成
    ....
    readLock.unlock();

这种方式的好处是:在耗时长的事务中,锁降级能够使让读操作更快进行执行不会被写操作给抢占,且后面的读操作不会被打断。
锁降级的代码演示:


import org.slf4j.Logger;
        import org.slf4j.LoggerFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class LockDownTest {
    private Logger logger = LoggerFactory.getLogger(LockDownTest.class);
    ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);
    ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
    ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
    /**
     *	这里仅仅是想知道锁重入的情况,是不是这个时候加入的锁会到等待队列里面排队。
     */
    public void queryData() {
        try {
            Thread.sleep(500);
            readLock.lock();
            logger.info("主线程通过可重入读锁,查询数据完成.");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            readLock.unlock();
        }
    }
    public void test3() throws Exception {
        // 开始锁降级
        writeLock.lock();
        logger.info("主线程抢到写锁...");
        // 这里的休眠是为了让下面线程能在预想的情况下加入等待队列.
        Thread.sleep(500);
        // 这里就是假设等待队列里面排在前面的是读锁线程
        processReadLock(1); 
        processReadLock(2); 
        Thread.sleep(500);
        processWriteLock(4);
        Thread.sleep(500);
        processReadLock(3); 
        Thread.sleep(500);
        // 开始锁降级
        readLock.lock(); // A 降级开始
        // 锁降级完成
        writeLock.unlock();// 注意必须读锁锁住,写锁释放操作完才降级完成,其他读线程才能进入。
        logger.info("主线程释放写锁(写锁降级为读锁,允许其他读操作进入)");
        logger.info("sleep 10s 验证等待队列中的读操作是否能执行..");
        TimeUnit.SECONDS.sleep(10);// 睡3s验证其他的读操作能进行
        logger.info("sleep 10s 结束");
        queryData();// 还是主线程去获取读锁。验证可重入锁。
        readLock.unlock(); // A 降级结束
        logger.info("主线程读锁释放");
//        logger.info("过程结束..");
    }
    private void processWriteLock(int threadIndex) {
        new Thread(() -> {
            logger.info("线程" + threadIndex + " 写锁开始竞争,阻塞中.");
            writeLock.lock();
            logger.info("线程" + threadIndex + " 写锁执行中..");
            writeLock.unlock();
            logger.info("线程" + threadIndex + " 写锁释放..");
        }).start();
    }
    private void processReadLock(int threadIndex) {
        new Thread(() -> {
            logger.info("线程" + threadIndex + " 读锁开始竞争,阻塞中.");
            readLock.lock();
            logger.info("线程" + threadIndex + " 读锁执行中..");
            readLock.unlock();
            logger.info("线程" + threadIndex + " 读锁释放..");
        }).start();
    }
    public static void main(String[] args) throws Exception {
        LockDownTest readWriteLockTest = new LockDownTest();
        readWriteLockTest.test3();
    }
}

注意这是公平锁的情况,结果说明:
在这里插入图片描述

3.2 可重入读写锁缺点(引入邮戳锁)

ReentrantReadWriteLock实现了读写分离。默认是非公平锁,每个线程是随机获取锁的。可能会导致锁饥饿的问题。
使用公平锁策略一定程度上能缓解这个问题,但是公平锁是牺牲系统的吞吐量为代价的。
引入StampedLock类的乐观锁。

4. StampedLock

StampedLock邮戳锁。这种锁是一种乐观锁,允许线程在读过程中进行写操作。让读多写少的时候,写线程有机会获取写锁。减少了线程饥饿的问题。吞吐量(单位时间系统能处理的请求量)大大提高。
在读线程操作临界资源的时候,允许写操作进行资源修改,那么读取到的数据是错误的怎么办?
为了保证读线程读取数据的正确性。读取的时候是乐观读,乐观读tryOptimisticRead不能保证读取的数据是正确性的,所以将数据读取到局部变量中,再通过lock.validate(stamp)校验是否被写线程修改过,若修改过则需要上悲观锁,重新读取数据到局部变量。

4.1 代码示例

使用代码示例:

 //乐观读,读的过程中也允许获取写锁介入
    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 = 0; 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);
            }finally {
                stampedLock.unlockRead(stamp);
            }
        }
        System.out.println(Thread.currentThread().getName()+"\t"+" finally value: "+result);
    }

4.2 使用场景和注意事项

StampedLock适用于读多写少的高并发场景。通过乐观读很好的解决了写线程饥饿的问题。
值得注意的是:
StampedLock不是可重入锁

5. 无锁-独占锁-读写锁-邮戳锁总结

在这里插入图片描述

  1. 从无锁到独占锁:无锁状态下数据在多线程环境下不安全因此需要锁
  2. 独占锁到可重入读写锁:独占锁在「读读」的时候线程存在竞争关系,实际很多场景中是允许多个线程同时读的。
  3. 可重入读写锁到邮戳锁:可重入读写锁会导致读多写少情况下的线程饥饿问题。引入了邮戳锁,允许读的过程中进行写。但是要采取乐观读的方式,进行数据的校验。如果数据校验失败,从乐观读变为悲观读。(乐观读的过程中允许写,悲观读的过程中不允许写操作)

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

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

相关文章

app逆向-apktool逆向工具安装使用

文章目录 一、前言二、安装三、执行 一、前言 apktool是一个能够解析和分析安卓应用文件&#xff08;APK&#xff09;的工具&#xff0c;帮助了解应用程序的内部结构和功能。 二、安装 window版本 下载地址&#xff1a;版本2.9.3&#xff0c;并将下载的 jar 重命名为apktoo…

【缓存周总结】Redis缓存的使用以及数据安全的处理

前言 Redis非关系型数据库已经是很常见的工具了&#xff0c;项目中用到的也很多&#xff0c;这篇文章系统的分析下使用过程中可能会遇到的问题 一、缓存 缓存是数据交换的缓冲区&#xff0c;是存贮数据的临时地方&#xff0c;一般读写性能较高。 我们项目中引用的Redis目的就是…

SPI:JDK 与 SpringBoot

浅谈Java SPI原理与其在JDBC、Flink中的应用 API&#xff1a;由被调方提供的实现了某个完整功能的接口&#xff0c;主调方直接调用该接口来享用该功能&#xff0c;而无需关注该接口的具体实现。比如使用 JDK 的 InputStream#read 从文件系统中读取数据。 SPI&#xff1a;被调方…

DOM 型 XSS 攻击演示(附链接)

一、介绍 DOM&#xff08;Document Object Model&#xff09;型 XSS&#xff08;Cross-Site Scripting&#xff09;攻击是一种 Web 应用程序中的安全漏洞&#xff0c;其特点是攻击者成功地注入了恶意脚本&#xff0c;这些脚本在用户的浏览器中执行&#xff0c;从而导致恶意行为…

DETR解读,将Transformer带入CV

论文出处 [2005.12872] End-to-End Object Detection with Transformers (arxiv.org) 一个前置知识 匈牙利算法&#xff1a;来源于二部图匹配&#xff0c;计算最小或最大匹配 算法操作&#xff1a;在n*n的矩阵中 减去行列最小值&#xff0c;更新矩阵&#xff08;此时行或者…

C# RichTextBox常用属性、方法学习1

1 字体 Font font1 new Font("宋体", 18); richTextBox1.Font font1; Font font2 new Font("宋体", 10, FontStyle.Underline); richTextBox1.SelectionFont font2; 定义字体&#xff0c;可以带2个参数&#…

代码随想录刷题笔记-Day13

1. 二叉树的层序遍历 102. 二叉树的层序遍历https://leetcode.cn/problems/binary-tree-level-order-traversal/层次遍历依靠队列的先进先出特点实现。 解题思路 层序遍历的本质就是对每一个pop出来的处理节点&#xff0c;处理后把他的左右节点放进去。 对于每一层来说&…

菱形打印和十进制ip转二进制

1.菱形打印 用for循环 #!/bin/bashread -p "请输入菱形的大小&#xff1a;" num #打印向上的等腰三角形 for ((i1;i<num;i)) dofor ((jnum-1;j>i;j--))doecho -n " " #打印的是前面的空格donefor ((k1;k<2*i-1;k))doecho -n "*" #打印…

【stm32】hal库学习笔记-FSMC连接TFT_LCD

【stm32】hal库学习笔记-FSMC连接TFT LCD 触摸屏结构与原理 LCD模块接口原理图 LCD 接口连接在 FSMC 总线上面&#xff0c;图中的 T_MISO/T_MOSI/T_PEN/T_SCK/T_CS 连接在 MCU 的 PB2/PF11/PB1/PB0/PC13 上&#xff0c;这些信号用来实现对液晶触摸屏的控制&#xff08;支持电阻…

QT tcp与udp网络通信以及定时器的使用 (7)

QT tcp与udp网络通信以及定时器的使用 文章目录 QT tcp与udp网络通信以及定时器的使用1、QT网络与通信简单介绍2、QT TCP通信1、 服务器的流程2、 客户端的流程3、服务器的编写4、客户端的编写 3、QT UDP通信1、客户端流程2、客户端编写3、UDP广播4、UDP组播 4、定时器的用法1、…

leetcode刷题日志-146LRU缓存

思路&#xff1a;使用hashmap储存key&#xff0c;vaule&#xff0c;使用双向链表以快速查到尾结点&#xff08;待逐出的节点&#xff09;&#xff0c;链表的题一定要在纸上画一下&#xff0c;不然连着连着就不知道连在哪里去了 class LRUCache {public class ListNode {int ke…

【js基础】日期对象的使用,查找、增加、克隆、删除DOM节点,M端事件

文章目录 前言一、日期对象日期对象的作用1.1 实例化1.2 日期对象的方法1.3 时间的格式化1.4 时间戳的使用时间戳是什么js的时间戳 二、DOM的增删改查什么叫做DOM节点2.1 DOM的查找2.2 增加节点2.3 克隆节点和删除节点 三、M端事件3.1 M端是什么&#xff1f; 总结 前言 在 Jav…

你的MiniFilter安全吗?

简介 筛选器管理器 (FltMgr.sys)是Windows系统提供的内核模式驱动程序, 用于实现和公开文件系统筛选器驱动程序中通常所需的功能; 第三方文件系统筛选器开发人员可以使用FltMgr的功能可以更加简单的编写文件过滤驱动, 这种驱动我们通常称为MiniFilter, 下面是MiniFilter的基本…

STM32CubeMX教程31 USB_DEVICE - HID外设_模拟键盘或鼠标

目录 1、准备材料 2、实验目标 3、模拟鼠标实验流程 3.0、前提知识 3.1、CubeMX相关配置 3.1.0、工程基本配置 3.1.1、时钟树配置 3.1.2、外设参数配置 3.1.3、外设中断配置 3.2、生成代码 3.2.0、配置Project Manager页面 3.2.1、设初始化调用流程 3.2.2、外设中…

优选6款前端动画特效分享(附在线演示)

优选6款前端动画特效 其中有CSS动画、canvas动画、js小游戏等等 下方效果图可能不是特别的生动 那么你可以点击在线预览进行查看相应的动画特效 同时也是可以下载该资源的 翻页时钟特效 基于react实现的一款翻页时钟特效 切换时间特效跟比赛切换分数牌相似 以下效果图只能体现…

Gitlab7.14 中文版安装教程

Gitlab7.14 中文版安装教程 注&#xff1a; 本教程由羞涩梦整理同步发布&#xff0c;本人技术分享站点&#xff1a;blog.hukanfa.com转发本文请备注原文链接&#xff0c;本文内容整理日期&#xff1a;2024-01-28csdn 博客名称&#xff1a;五维空间-影子&#xff0c;欢迎关注 …

T05垃圾收集算法与垃圾收集器ParNew CMS

垃圾收集算法与垃圾收集器ParNew & CMS 垃圾收集算法 #### f 分代收集理论 当前虚拟机的垃圾收集都采用分代收集算法。根据对象存活周期不同将内存分为几块&#xff0c;一般将java堆分为新生代和老年代&#xff0c;然后根据各个年代的特点选择不同的垃圾收集算法。 在新…

MySQL-窗口函数 简单易懂

窗口函数 考查知识点&#xff1a; • 如何用窗口函数解决排名问题、Top N问题、前百分之N问题、累计问题、每组内比较问题、连续问题。 什么是窗口函数 窗口函数也叫作OLAP&#xff08;Online Analytical Processing&#xff0c;联机分析处理&#xff09;函数&#xff0c;可…

ESP8266 控制之 : 使用 RingBuffer USART1 和 USART3互传

简介 使用Buffer来避免数据的丢失, 或许你自己在使用串口进行收发时会丢失数据, 现在我们就来简单使用一下RingBuffer创建Rx、Tx的Buffer来避免发送接收丢包或数据丢失问题。 扩展知识 RingBuffer的介绍, 看完大概也就知道了&#xff0c;实在不知道就看看下面的代码 线路连接…

微信小程序开发学习笔记《14》上拉触底事件案例

微信小程序开发学习笔记《14》上拉触底事件案例 博主正在学习微信小程序开发&#xff0c;希望记录自己学习过程同时与广大网友共同学习讨论。建议仔细阅读对应官方文档 一、最终实现效果 实现在加载完这一页之后&#xff0c;刷新之后展示 “数据加载中”&#xff0c;加载完后…