【JavaEE进阶】锁的特性

news2024/9/24 13:22:25

目录

一、乐观锁&悲观锁

二、公平锁&非公平锁

三、可重入锁&非可重入锁

四、读写锁&互斥锁

      互斥锁

    读写锁

      读写锁涉及的类:ReentrantReadWriteLock

       读写锁的优势:

五、轻量级锁&重量级锁

六、CAS

 ①基于CAS实现原子类

下面,来一段CAS的伪代码,来解释一下,为什么上述的过程没有出现线程安全问题:

②基于CAS实现自旋锁(实现一个类:SpainLock/伪代码)。

     属性:需要一个线程的引用,来确保是哪一个线程加锁的

     lock方法:

       Java当中的自旋锁 

  unlock方法

ABA问题

解决ABA问题


一、乐观锁&悲观锁

        判定一个锁是悲观锁还是乐观锁,主要还是站在锁冲突(锁竞争)发生的概率预测上面来进行判断的。

        对于乐观锁:

        乐观锁默认锁竞争的情况不那么激烈

我们熟悉的ConcurrentHashmap,它使用的就是乐观锁的策略。只有当存在线程安全问题的时候,才会采用加锁的方式。

而与它相反的,Hashtable,采用的就是悲观锁的处理方式,无论是否发生线程安全问题,都需要加锁。 


二、公平锁&非公平锁

       给定一个场景:

       此时,三个线程t1,t2,t3同时竞争一把锁

        t1如果最先获取到锁,那么接下来在t1释放锁之前,t2,t3必须要阻塞等待t1释放锁。

        如图:

        

       可以看到,虽然t2,t3线程都没有获取到锁,但是t2比t3先开始阻塞等待。

       那么,当t1释放锁之后,t2和t3线程哪个优先可以获取到锁呢?这个就涉及到公平锁和非公平锁的差别。


       对于公平锁:会优先让t2线程获取到锁,也就是让最开始进行阻塞等待的线程优先获取到锁。

       对于非公平锁:  在t1释放锁之后,其余阻塞等待的线程都会继续重新竞争锁,不存在谁最先获取到锁的情况。

       其中,synchronized是非公平锁,ReentranctLock可以实现公平&非公平两种策略。

       对于公平锁来说,因为它需要确保其他线程都按照顺序再次获取锁。

       因此,公平锁需要一个队列来记录阻塞等待线程的等待顺序。


三、可重入锁&非可重入锁

        也在这篇文章当中介绍了。

        synchronized是可重入的
(1条消息) Java对于synchronized的初步认识_革凡成圣211的博客-CSDN博客https://blog.csdn.net/weixin_56738054/article/details/128062475?spm=1001.2014.3001.5502


四、读写锁&互斥锁

      互斥锁

        典型代表就是synchronized。

        提供了加锁、解锁两个操作。

        当一个线程获取到锁之后,其他的线程如果想加锁,那就会造成"阻塞等待"。无论其他线程对于加锁代码块的操作是否产生线程安全问题,都会产生阻塞等待。

         synchronized是典型的互斥锁

         关于synchronized的介绍,也已经在这一篇文章当中提及了:  
(1条消息) Java对于synchronized的初步认识_革凡成圣211的博客-CSDN博客https://blog.csdn.net/weixin_56738054/article/details/128062475?spm=1001.2014.3001.5502


    读写锁

      有关线程安全问题的分析,我们在上一篇文章当中也提到了。

       再简单回顾一下:如果多个线程同时针对某一变量进行修改操作,那么就会发生线程安全问题。此处的修改可以理解为"写"操作。

       但是,如果在多个线程仅仅只是读取某一个变量的值,不对这个变量进行修改,那么不存在线程安全问题。


      读写锁涉及的类:ReentrantReadWriteLock

        这个类当中,同时又提供了两个静态内部类:

        第一个是:ReentrantReadWriteLock.ReadLock

        这表示一个读锁,提供了lock,unlock两个方法,一个代表加锁,另外一个代表解锁。


        第二个是:ReentrantReadWriteLock.WriteLock writeLock

        这表示一个写锁,也提供了lock,unlock两个方法。


       读写锁的优势:

       而读写锁,恰好就是针对"写"这个操作来作文章。给"写",也就是修改的操作加锁,不给"读"的操作加锁。   

       也就是:

       加锁和加锁之间,不会产生锁冲突;

       加锁和加锁之间,产生锁冲突;

       加锁和加锁之间,产生锁冲突; 

       所以:

        如果代码当中的操作仅仅是读操作,那么只加读锁即可。

        如果代码当中的操作涉及写,那么加写锁即可。

  

        以看到,由于减小了读、读的锁冲突,相比于互斥锁,有效减少了锁的粒度 。


五、轻量级锁&重量级锁

     对于synchronized,它的轻量级锁,是基于自旋锁的方式实现的。

     对于synchronized,它的重量级锁,是基于挂起等待锁的方式实现的。

     下一篇文章,将详细介绍。


六、CAS

        含义:cas的全称为:compare and swap

       是并发编程当中一种常用的算法,它包含了下面三个参数:V,A,B。

        V表示要读写的内存位置,A表示旧的预期值(可以理解为原来的值),B表示一个新值。


        给定一个场景:

        如图,CPU寄存器当中有两个变量A=10,B=20,内存当中有一个地址V,里面保存了一个10。


    当内存当中的值(&V)旧的值(A)相同的时候,把新的值(B)写入到内存当中。

    伪代码: 其中address代表V的地址,A代表预期值(expectVal),也就是改动之前的值。

     B代表新的(swapvalue)值。

boolean CAS(address, expectVal,swapvalue){

  if(&address==expectedvalue){
     
   &address=swapvalue;
   return true;

}
   return false;

}

        以上看似简单的几行代码,也有几个比较特殊的地方:

        第一个特殊的地方:CAS这一系列代码是一条CPU指令。 

        那么,也就意味着:CAS操作是原子的。也就意味着:比较和交换的整个过程是原子的。

        也就意味着,一个线程在CAS的时候,另外的线程无法进入CAS的代码块。


 ①基于CAS实现原子类

      还是之前的文章当中提到的场景,让两个线程,分别对一个变量count各自自增50W次的场景。  

       根据之前的知识,可以判定,上述代码是存在线程安全问题的,因为count++这个操作不是原子性的。

       但是,如果把上述代码改成这样,引入了原子类(AtomicInteger)呢?     

      运行结果:可以看到这个时候,没有发生线程安全问题。

      


下面,来一段CAS的伪代码,来解释一下,为什么上述的过程没有出现线程安全问题:

class AtomicInteger{

    /**
     * val可以理解为V,也就是原来的内存当中的值
     */
     private int val;

     public int getAndIncrement(){
         //读取旧的值,也就是原来内存当中的值
         int oldVal=val;
         //oldValue为一个预期的值(A)
         //oldValue+1为目标值(B)
         //比较,如果oldValue的值和原来内存当中的值相同,那么就需要更改内存当中
         //val的值,然后返回true
         while (CAS(val,oldVal,oldVal+1)!=true){
             oldVal=val;
         }
         return oldVal;
     }
}

       第一步:使用一个oldValue变量,来保存原来的内存当中的值value。此处的oldValue可以理解为寄存器当中的值,线程工作内存当中的值,也就是预期值。


       第二步:已经进入到CAS方法当中了:

           判断内存当中的值val是否和寄存器当中的值相同,也就是valoldValue的值是否相同。

           这里有可能有个疑问:oldValue不是刚刚由val赋值过去的吗?但是为什么二者还会出现不相同的情况呢?

       正常情况下面,oldValue的值应该是和内存当中的val相同的。如果相同的话,那么就把oldValue的值+1,然后赋值给val(下划线部分的内容,都是在CAS方法内部完成的)

       然后,CAS方法返回true,不进入while循环

    

        但是,完全有可能在一个线程(thread1)进入了getAndIncrement()方法当中之后,被切出了CPU内核   

          此时,另外一个线程(thread2)修改了val的值,也就是原来内存当中的值。

          然后,当thread1重新回到cpu内核上面的时候,发现,val已经不再是自己的工作内存当中的oldValue了。    

       这个时候,由于val和oldValue的值不相同,因此CAS方法返回false,进入while循环当中,也就是CAS不成功,继续进行load。

       下面,模拟一下两个线程被cpu进行调度的过程。

时间轴thread1thread2
t1load:oldValue=val
t2load:oldValue=val
t3

CAS(val,oldValue,oldVal+1)

返回true,实现increment

t4返回自增过后的value
t5

CAS(val,oldValue,oldValue+1)

因为val和oldValue不相同,因此返回false,进入while循环,重新比较,直到CAS的返回值为true

t6CAS自增成功之后,返回true。

       可以看到,上述的过程当中,没有涉及到任何阻塞等待的情况。

       但是,出现了反复比较的情况。但是,只要不CAS成功,那么线程就会一直CAS,因此这种实现线程安全的方式也存在效率的问题。 


②基于CAS实现自旋锁(实现一个类:SpainLock/伪代码)。

     自旋锁不涉及任何的阻塞等待的操作。

     属性:需要一个线程的引用,来确保是哪一个线程加锁的

/**
  * 记录哪一个线程尝试获取这个自旋锁
 */
private Thread threadOwner=null;

     lock方法:

    /**
     * 加锁
     */
    public void lock(){
       while (CAS(this.threadOwner,null,Thread.currentThread())!=true){
           
       }
    }

       以上CAS的工作过程,是这样的:

       首先,判断threadOwner是否为null。如果为null,那么说明此时自旋锁还没有被占用。

       可以把当前线程,也就是Thread.currentThread()赋值给threadOwner。这个时候,threadOwner就已经被赋值了。

       当其他线程再次CAS的时候,就会因为threadOwner!=null,因此就不会产生赋值,因此返回false,重新进入while循环进行CAS判断

时间轴线程
t1进入lock方法
t2

进行CAS操作:threadOwner,null,Thread.currentThread()

说明:

如果threadOwner为null,说明这把锁没有被占有,当前线程(t1)可以占用这把锁,因此把当前线程的引用赋值给threadOwner

此时,如果其他线程(t2)过来,那么就会因为threadOwner不为null而进入循环,反复比较,直到二者相同。

       Java当中的自旋锁 

        ReentrantLock就是一个典型的自旋锁。从上述的代码也可以看出来,线程没有发生阻塞等待的情况,也就是自旋锁不会造成阻塞等待的情况。


  unlock方法

     让threadOwner重新变为null。

     这样,其他线程再次CAS的时候,threadOwner就不再是null了,不用进入CAS。

 public void unlock(){
        threadOwner=null;
    }

ABA问题

 之前提到了,对于CAS指令:

       可以看到,在if语句当中,对于&address和expectedvalue两个值,也就是原来内存当中的值期待的值,当这两个值相同的时候,才会把内存当中的值切换为swapvalue,也就是目标值。


        但是,对于判断相等这个操作,很有可能是这样的一种场景:

        当线程thread把value加载到自己的工作内存之后,被CPU调度离开了操作系统内核。    

        调度离开之后,其他线程(thread1),有可能对于原来内存当中的值(val),也进行了一次修改(变为val1),然后又改回了(val)。这个时候,线程thread再次读取ovalue的值,读取到的值虽然是和线程工作内存当中的值:value一样。但是,已经有其他线程对于原来的value更改过了。


        总结一下:ABA:

        A:原来线程内存当中的值

        B:其他线程更改过后的值

        A:另外的线程又把B改变回来的值--A


解决ABA问题

对于原来内存当中的val值,同时添加一个版本号来修饰。

初始的时候,版本号为1,当有线程对于这个值进行修改的时候,让版本号+1。

 后续判断&address的expectValue是否相等的操作改变为判断版本号是否相同

 这样,就让A和B的版本号不一样,B和该回去的A的版本号又不一样,也就避免了ABA问题。  

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

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

相关文章

举个栗子~Tableau 技巧(250):创建 KPI 指标突出显示表

上一个栗子发出后,有数据粉反馈:有什么办法可以让全年的销售数据分层显示哇?业绩表现好、一般和差的分别使用不同的底色。 这个需求,如果考核的是名次,可以使用 🌰 用颜色突出显示前N项(TopN)和后N项(Bott…

samba设置文件共享

前提说明本人使用的系统如下共享文件的系统:ubuntu 版本:18.04.6目标系统:windows11安装sambasudo apt-get install samba修改配置文件sudo vim /etc/samba/smb.conf文件末尾添加以下内容[share] # 共享的名称,可以自行定义 c…

完整数据分析流程:Python中的Pandas如何解决业务问题

开篇 作为万金油式的胶水语言,Python几乎无所不能,在数据科学领域的作用更是不可取代。数据分析硬实力中,Python是一个非常值得投入学习的工具。 这其中,数据分析师用得最多的模块非Pandas莫属,如果你已经在接触它了…

c#入门-异步方法

异步方法 如果一个操作会返回Task,那么用这个操作续接后续操作,也会得到Task。 也就是说Task具有传染性,最终拼凑出来的Task非常复杂。 使用异步方法,可以简化Task的拼凑。 async修饰 异步方法需要添加async修饰符。并且通常方…

【前端】Vue项目:旅游App-(15)home:网络请求house数据、动态并组件化展示house列表信息

文章目录目标过程与代码content组件请求数据:houseListrequeststore控制台输出动态加载更多列表数据house-item组件阶段1:数据传送阶段2:对着目标写样式house-item-v9house-item-v9:debughouse-item-v3阶段3:总体效果效…

Android ANR触发机制(二)

上一篇文章看了Service的ANR触发流程,现在看一下其他三种ANR触发流程。 1.BroadcastReceiver触发ANR BroadcastReceiver超时是位于ActivityManager线程中的BroadcastQueue.BroadcastHandler收到BROADCAST_TIMEOUT_MSG消息时触发。 广播队列分为foreground队列和b…

基于Java+SpringBoot+Vue前后端分离学生管理系统设计与实现

博主介绍:✌全网粉丝3W,全栈开发工程师,从事多年软件开发,在大厂呆过。持有软件中级、六级等证书。可提供微服务项目搭建与毕业项目实战✌ 博主作品:《微服务实战》专栏是本人的实战经验总结,《Spring家族及…

2023年二月份图形化二级打卡试题

活动时间 从2023年 2月1日至1月21日,每天一道编程题。 本次打卡的规则如下: (1)小朋友每天利用10~15分钟做一道编程题,遇到问题就来群内讨论,我来给大家答疑。 (2)小朋友做完题目后&…

数组中和为0的三个数

给你一个整数数组 nums ,判断是否存在三元组 [nums[i], nums[j], nums[k]] 满足 i ! j、i ! k 且 j ! k ,同时还满足 nums[i] nums[j] nums[k] 0 。请你返回所有和为 0 且不重复的三元组。 注意: 答案中不可以包含重复的三元组。 示例 1: 输入: num…

了解SLI、SLO和SLA

了解SLI、SLO和SLA 概念解释 服务水平指标(SLI) SLI代表目前服务的状态,例如可以是最基本的接口成功率、p99响应时间,也可以是一些业务指标,例如用户投诉率之类的。是可量化,是可确定的。 服务水平目标(SLO) SLO是目标&#x…

【树】哈夫曼树和哈夫曼编码

哈夫曼(Huffman)树,又称最优树,是一类带权路径长度最短的树。最优二叉树(哈夫曼树)路径:从树中一个结点到另一个结点之间的分支构成这两个结点之间的路。路径长度:路径上的分支数目;…

mysql分组排序取组内第一的数据行获取分组后,组内排名第一或最后的数据行。

前言: group by函数后取到的是分组中的第一条数据,但是我们有时候需要取出各分组的最新一条,该怎么实现呢? 本文提供两种实现方式。 一、准备数据 DROP TABLE IF EXISTS tb_dept; CREATE TABLE tb_dept (id bigint(20) UNSIG…

chat聊天系统消息消费时遇到的问题及优化思路

前言 之前有段工作经历涉及到了chat相关,而消息的发送 -> 存储 -> 消费是由不同的团队负责的,因此消息如何再多个团队之间流通、以及通过什么介质传递都是需要考虑的问题。 之前我负责过一些消息消费的相关工作,消息发送团队将消息推…

【Linux】简介磁盘|inode|动静态库

目录一.简介磁盘1.磁盘的物理结构:2.磁盘存储方式:3.磁盘的逻辑抽象:二.inode&&文件系统1.inode文件属性(inode)内容(data block)为什么删除一个文件相比于写一个文件要快得多&#xff…

若依配置教程(二)集成积木报表JimuReport

积木报表配置官网 在搭建好若依环境成功运行以后,我们先在这个系统中加一个小功能:JimuReport积木报表,以下步骤,我们按照官网教程,详细配置一下: 1.在ruoyi-admin文件夹下的pom.xml加入jar包依赖&#x…

MLP多层感知机理解

目录 .1简介 .2例子 2.1模型 2.2 实例 2.2.1 问题描述 2.2.2 数学过程 .3 代码 3.1 问题描述 3.2 代码 references: .1简介 多层感知机是全连接的 可以把低维的向量映射到高维度 MLP整个模型就是这样子的,上面说的这个三层的MLP用公式总结起来…

C 语言零基础入门教程(二十)

C 预处理器 C 预处理器不是编译器的组成部分,但是它是编译过程中一个单独的步骤。简言之,C 预处理器只不过是一个文本替换工具而已,它们会指示编译器在实际编译之前完成所需的预处理。我们将把 C 预处理器(C Preprocessor&#x…

练手好福利!20个Python实战项目含源代码【2023最新】

高效学习源代码的步骤:1.运行程序,观察表现2.运行源码,断点调试,从头跟一边源码的执行流程,注意函数堆栈3.画类图、流程图,先把遇到的重要类记录下来,表明各个类的关系4.记录问题,把…

Unity XR

一、几个Unity XR Interaction Toolkit学习地址 1.B站视频 https://www.bilibili.com/video/BV11q4y1b74z/?spm_id_from333.999.0.0&vd_source8125d294022d2e63a58dfd228a7fcf63 https://www.bilibili.com/video/BV13b4y177J4/?spm_id_from333.999.0.0&vd_source8…

【对象的比较】java代码实现,详解对象的比较,Comparable接口和Comparator比较器

前言: 大家好,我是良辰丫,💞💞💞今天的我们要学习的知识点是java对象的比较,不是大家现实生活中对象的比较,是java中new一个对象的那个对象,对象的比较到底是什么意思呢&…