多线程锁相关

news2024/11/19 16:27:25

日升时奋斗,日落时自省 

目录

1、常见的锁策略

1.1、悲观锁vs乐观锁

1.2、轻量级锁vs重量级锁

1.3、自旋锁vs挂起等待锁

1.4、互斥锁vs读写锁

1.5、公平锁vs非公平锁

1.6可重入锁vs不可重入锁

2、CAS

2.1、CAS解析

 2.2、CAS的应用场景

2.2.1、实现原子类

2.2.2、实现自旋锁

2.2.3、CAS相关ABA问题

  3、Synchronized原理

3.1、锁升级/锁膨胀

3.2、锁消除

3.3、锁粗化

1、常见的锁策略

谈到锁不分语言,因为多线程操作中,锁是一种很通用的方法保证线程安全,锁也不局限于Java,C++,Python,数据库,操作系统等,如果涉及到锁,基本都是可以使用下列策略锁的

1.1、悲观锁vs乐观锁

这里不是具体的锁,应该叫做“两类锁” (两种锁的类型)

乐观锁:预测锁竞争不是很激烈(所以做一些相对更少的工作)

如:数据改变的时候 一般不会发生冲突,所以提交更新数据后才会检测是否发生冲突,如果冲突交给用户决定如何做(也就是后知后觉)

悲观锁:预测锁竞争是很激烈的(工作可能会比较多)

如:悲观锁相反而已,你要操作数据,就先加锁,谁也拿不走别人修改不了,保证安全

总结:两类锁的背后工作是截然不同的,这里也不是绝对的,判定依据,主要就是看预测锁竞争激烈程度

1.2、轻量级锁vs重量级锁

轻量级锁:加锁解锁开销都比较小,效率更高(多数情况下,乐观锁,也是一个轻量级锁,但是不完全保证) 

重量级锁:加锁解锁开销都比较大,效率更低(多数情况下,悲观锁,也是一个重量级锁,但是不完全保证)

1.3、自旋锁vs挂起等待锁

自旋锁:是一种典型的轻量级锁

事例解释:一个庙里的主持,,有多个小徒弟,有一个小徒弟每天都问老主持,我能不能担任咱们庙里的主持(老和尚就对当前主持这个位置加锁),(这个小和尚就是自旋锁,老和尚辞去主持位置也就是解锁,自己就可以上位了)

优点:没有放弃CPU,不涉及线程阻塞和调度,一旦锁被释放,就能第一时间获取到锁

缺点:如果锁被其他线程持有的时间比较久,那么就会持续的消耗CPU资源(此时挂起等待就不会消耗CPU的资源)

挂起等待锁:是一种典型的重量级锁

事例解释:还是小和尚们和老和尚主持,但是老和尚虽然对主持加锁了,但是没有小和尚主动去争取主持位置,想等老和尚主持主动辞职,让这个小和尚担任主持(小和尚就是挂起等待),或许老和尚主持辞去让别的小和尚担任主持,他就只是等待自动轮到他

总述:以上三种锁任何一个需要锁的场景,其实都涉及到这样的一些类似的策略情况

1.4、互斥锁vs读写锁

互斥锁:就是咱们前面用过的像synchronized这样的锁,提供加锁 和 解锁 两个操作,如果一个线程加锁了,另一个线程也尝试加锁,就会阻塞等待

读写锁:提供了三种操作 针对读加锁、针对写加锁、解锁(适合频繁读,不频繁写的场景)

多线程并发读是不存在线程安全问题的,也不需要加锁控制

(1)读锁和读锁之间,没有互斥

(2)写锁和读锁之间,存在互斥

(3)写锁和写锁之间,存在互斥 

只有一组操作有读也有写,才会产生竞争(解释为多线程操作情况下)

注:只要涉及到“互斥”,就会产生线程的挂起等待,一旦线程挂起,再次被唤醒就不知道隔了多久了。

因此尽可能减少“互斥”的机会,就是提高效率的重要途径

在我们所做的开发中一般情况下,读操作比写操作更加高频

1.5、公平锁vs非公平锁

公平锁:就是所有的线程都在等待,但是等待时间肯定是有差异的,公平起见就让等的时间最长的那个来执行吧。(遵循先来后到的规则)

非公平锁:还是那线程来说,所有线程都在等,有的线程等的时间久,有的只等了一会,但是现在选择线程不会有任何条件,随机来选,选到谁来执行就谁来,对于等的时间长的线程来说就不公平了。(不遵循先来后到的规则)

操作系统和java synchronized原生都是“非公平锁”

操作系统这里的针对加锁的控制,本身就是依赖于线程调度顺序,这个调度顺序是随机的,不会考虑线程锁等了多久,如果不做任何额外的限制,锁就是非公平锁;如果要是实现公平锁,就需要依赖额外的数据结构,来记录线程们的先后顺序

该两种锁没有好坏之分,看程序的不同情况

1.6可重入锁vs不可重入锁

可重入锁:一个线程对一把锁,连续加锁多次都不会出现死锁(这里多次加的是同一把锁,有点像递归一样锁里套锁,可重入锁也可以叫做“递归锁”)

不可重入锁:一个线程针对一把锁 ,连续加锁两次,出现死锁

针对java:Java里只要以Reentrant开头命名的锁都是可重入锁,而且JDK提供的所有现成的Lock实现类,包括synchronized关键字锁都是可重入的

针对java提供的锁synchronized判别

(1)synchronized 即是一个悲观锁,也是乐观锁

注:synchronized默认是乐观锁,竞争变激烈后就会变成悲观锁

(2)synchronized即是轻量级锁,也是一个重量级锁

注:synchronized默认是轻量级锁,如果当前锁竞争比较激烈,就会转换成重量级锁

(3)synchronized这里轻量级锁,是基于自旋锁的方式实现的。

synchronized这里的重量级锁,是基于挂起等待锁的方式实现的

(4)synchronized不是读写锁

(5)synchronized不是公平锁

(6)synchronized是可重入锁

2、CAS

2.1、CAS解析

CAS:全称Compare and swap ,字面意思:比较并且交换

翻译有点直白,不过确实是这样的

解释CAS:原数据为 V   预期值A    需要修改值B

(1)比较A 与 V 是否相等 (比较)

(2)如果比较相等 ,将B 写入 V(交换),如果不等的话就是直接到(3)

(3)返回操作成功 

这里文字解释还是有点模糊不清

来写我们这里拿图来解释  寄存器 与 内存的关系

 此处最特别的地方,上述这个CAS的过程,并非是通过一段代码实现的,而是通过一条CPU指令实现的,也就是说该操作是一个“原子”(CAS是原子)

原子就可以在一定程度上回避线程安全问题,咱们解决线程安全问题除了加锁之外又有了一个新的路子

总述:CAS可以理解为CPU给我们提供了一个特殊指令,通过这个指令,就可以一定程度的解决线程安全问题

(这里写一个伪代码来解释CAS)

伪代码:就是假的,不能运行编译的,用来帮助我们来断思路的

 2.2、CAS的应用场景

2.2.1、实现原子类

java标准库里提供的类AtomicInteger

AtomicInteger count=new AtomicInteger(0);

不要感觉陌生,这里就和创建一个包装类一样,直接赋值,后面的传参就是赋值

说了CAS是一个“原子” 那如何证明呢,之前的博客中有提及到锁synchronized可以解决多线程安全问题,这里也用代码尝试一下

public class Test {
    public static void main(String[] args) throws InterruptedException {
        //这些原子类 就是基于 CAS实现了 自增 自减等操作, 此时进行这类操作,也是线程安全的
        AtomicInteger count=new AtomicInteger(0);
        //使用原子类 来解决线程安全问题
        Thread t1=new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                //java中不支持运算重载 ,所以只能使用普通的方法表示自增自减
                count.getAndIncrement();   //解释 count++
               /* count.incrementAndGet();   //解释 ++count
                count.getAndDecrement();  //解释 count--
                count.decrementAndGet();   //解释 --count*/
            }
        });
        Thread t2=new Thread(()->{
            for (int i = 0; i < 5000; i++) {
                count.getAndIncrement();
            }
        });
        t1.start();
        t2.start();
        t1.join();  //等待
        t2.join();
        System.out.println(count);  //直接解决线程安全问题 不会影响多线程加的结果
    }
}

我们知道如果两个线程都执行++操作,在线程不安全的情况下不会得到一个准确的数值,当前可以。

之所以有这个代码出现,不仅仅是证明CAS可以保证线程安全,还有就是java中不支持运算重载,所以中用普通的方法来表示加加或者减减(前或者后)

伪代码解释一下CAS的精妙之处

 上图的(3)没有截上,这里补充一下

 到这里是不是友友想问既然一条CPU指令就实现了,精妙还安全为啥还要锁来维护线程安全

CAS属于“特殊方法” ->只能特定场景使用,没有那么通用(共享访问一个量可以,例如解决++在线程的安全问题)。

synchronized 属于“通用方法” ->各种场景,都能使用。

2.2.2、实现自旋锁

伪代码解释:

 2.2.3、CAS相关ABA问题

CAS在运行中的核心,检查value 和 oldValue 是否一致,如果一致就任务value没有被修改过,进行交换操作是没有问题的。(这个真的,下面解释的问题和这个有点点冲突)

要知道线程操作是很快的

value值 一致 有两种可能

value=A; 

一种是真没有修改过  value =A;

 第二种就是改了但是又改回来了。 value=A ->  value=B -> value=A

ABA问题 家人看见的你出远门的时候好好的 ,中途可能生病了,但是回家的时候好了但是在你不说的情况下,家人认为你一直是好的。(基本就是这样一个情况)

但是ABA仍然算是个缺陷,极端情况下,也可能会造成影响(实际中概率很低)

以取钱为例,去取款机上取钱这种

许某账户余额 有1000块,准备取500元

当按下取款机一瞬间,机器卡了许某就想着多按几下,能不能快点出来,这时候就会产bug重复扣款,考虑使用CAS方式扣款(图解)

 当然能看出来,上面这种情况概率还是很低的。

主要是要满足两个条件(1)恰好许某按了很多下,产生扣款操作(2)刚刚好非常极限的时间有人转账一样的金额

概率很低,但是不代表不会有,还是要考虑的,因为此时只是一种现实的情况,但是此情况一旦出现,不好解决吧,提前准备是最好的。

解决当前问题的就是 加一个版本号(就是标记),每次修改时,版本号都会更新+1,然后CAS不是以金额为基准,以版本号为基准,版本号就是无限加(版本号没有改变就代表什么都没有发生)

  3、Synchronized原理

使用原理前面提及过 : 保证线程执行安全,如果两个线程加锁同一个对象,就会产生阻塞。

synchronized不仅仅是我们看到的,内部还有很多操作。

3.1、锁升级/锁膨胀

锁要经历的4个过程 无锁、偏向锁、轻量级锁、重量级锁

(1)无锁

(2)偏向锁

加锁的时候,首先会进入到偏向锁状态,偏向锁,并不是真正的加锁,而只是占个位置,有需要了在真加锁,没有需要就算了。(其实看的出还是很优质的,毕竟加锁是要有开销的)

举个例子解释偏向锁:该两个小孩买了一个夜光球,小孩A先看见就在旁边看(尚未加锁),过了一会小孩B来了,孩子都喜欢独一份的,小孩A立马将夜光球抱到怀里(加锁),这就是偏向锁(有偏向性,只是没有遇到竞争者,一旦遇到立刻加锁)

偏向锁又是怎么做到:为了降低冲突,减少开销。

这里synchronized等待也不是凭空等待,有偏向性,做个标记 (这个过程很轻量)

<1> 如果整个使用锁的过程中没有出现锁竞争synchronized执行完后,取消偏向锁即可(清楚标记)

<2>   如果另一个线程也尝试加锁,在它加锁之前,迅速就标记的偏向锁升级为真正的加锁状态

(3)轻量级锁

synchronized发生锁竞争的时候,会从偏向锁,升级到轻量锁,此时,synchronized通过自选的方式来加锁,也是自旋锁(与刚才CAS伪代码一样思路)

(4)重量级锁

自旋锁不能一直自旋不是嘛,CPU是资源开销的,自旋次数多了就不划算了,自旋到一定程度会自己停的,再次升级为重量级锁(挂起等待锁)

重量级锁就不像前面的锁一样了,很保险,是基于操作系统的原生API来进行加锁,linux原生提供了mutex一组API,操作系统内核提供加锁功能,这个锁会影响到线程的调度,如果此时线程进行到了重量级锁,发生锁竞争就只能等了,该线程也就被放阻塞队列中,直到锁被释放,线程才有机会被调度,并且有机会获取锁,(注:是有机会,不是直接就获取)线程一旦被切出CPU就会变的低效

关于锁升级就是以上内容,有升级是不是就降级,当前还没有,到了重量级锁就在这个位置了,JVM只有锁升级;要想降级也只有等该线程执行完,锁释放了,从新有一个对象获取锁,开始重复刚刚加锁的过程(偏向锁,轻量级锁,重量级锁)

3.2、锁消除

编译器智能判定,看当前的代码是否是真的要加锁

判定当前场景不需要加锁,程序员也加了,就自动把锁给卸掉

在学习StringBuffer的时候说个这个类是标准库提供的,具有线程安全的涉及线程安全会在标准库里面已经加了synchronized,如果我们针对StringBuffer就锁的话,就会被编译器直接卸掉,判定安全不需要加锁

3.3、锁粗化

锁的粒度:synchronized包含的代码越多,粒度就越粗,包含代码越少,粒度就越细,很符合生活中的场景,留个印象就行

通常认为锁的粒度细一点比较好,为什么这么理解?我们知道多线程随机调度并发执行的,但是加锁部分的代码,是不能进行并发执行的,会降低多线程的执行速度,锁的粒度越细,synchronized内代码越少,有更多的代码能够进行并发执行。

也不是说粒度越细就一定越好,如果加锁比较频繁的情况下,大部分还是都加锁,这时候就不划算了,加锁也是有开销的,此时锁与锁之间的间隙就很小,不如一次给整个阶段都加上锁,减少加锁的开销,直接只加一次。(用图举个例子)

 

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

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

相关文章

JavaSE学习day2_03, 算数运算符

5. 算术运算符 5.1 分类&#xff1a; - * / % 5.2 细节&#xff1a; 1.运算规则。跟小学数学是一模一样的 2.如果是整数跟整数进行计算&#xff0c;结果只能是整数。直接去掉小数部分&#xff0c;保留整数。如5/22,而不是2.5. 3.如果想要结果中有小数&#xff0c;需要有小…

(二)K8S Namespace 和 Label

Namespace 1、K8S集群中默认的Namespace为default&#xff0c;通过Namespace可以实现Pod之间的相互隔离&#xff08;如测试环境、生成环境的隔离&#xff09; 2、通过K8S的资源配置机制限定不同的Namespace对CPU、内存的使用&#xff0c;再通过K8S授权机制将不同的Namespace交…

启动ruoyi框架(初学)

启动ruoyi框架&#xff08;初学&#xff09;&#xff08;RuoYi&#xff09; Ruo Yi启动ruoyi框架&#xff08;初学&#xff09;&#xff08;[RuoYi](http://doc.ruoyi.vip/)&#xff09;一、简介二、项目启动1.前端模组&#xff1a;RUOYI-UI2.后端模组&#xff1a;3.环境要求&a…

Java版阿里云/腾讯云域名动态映射DDNS到动态IPv4或IPv6

1 介绍 利用业余时间&#xff0c;整合了一下阿里云和腾讯云动态域名映射&#xff0c;并将其使用spring boot做了一下封装&#xff0c;正好可以把家里的闲置电脑和IP v6利用起来&#xff0c;具体使用方法如下&#xff0c;或者参考对应项目的readme文档 2 地址 aliyun-ddns&am…

数据分析-深度学习 Day3

youtube课程地址&#xff1a;&#xff08;实时更新&#xff09;https://www.youtube.com/playlist?listPLJV_el3uVTsMhtt7_Y6sgTHGHp1Vb2P2Jwww.youtube.com/playlist?listPLJV_el3uVTsMhtt7_Y6sgTHGHp1Vb2P2JB站课程地址&#xff1a;李宏毅2021机器学习【week3】&#xff1a…

基于tensorflow框架bert的自然灾害描述文本分类详细教程

概述: 使用tensorflow框架: 预测给定的推文是否与真正的灾难有关。如果是这样,预测一个1。如果不是,预测一个0 数据展示:

Java之String概述、对象创建原理和常见面试题、String类常用API、案例

目录String、ArrayList前言String简单介绍Arraylist简单介绍String 概述String类的特点详解总结String类创建对象的两种方式两种方式有什么区别吗&#xff1f;总结String常见面试题String类常用API-字符串内容比较总结String类常用API-遍历、替换、截取、分割操作String案例验证…

内存管理(C/C++)

内存管理&#xff08;C/C&#xff09;C/C内存分布C语言中的动态内存管理方式C中动态内存管理new / delete 操作内置类型new 和 delete 操作自定义类型operator new 和 operator delete 函数operator new 和 operator delete 函数new 和 delete 的实现原理内置类型自定义类型定位…

【人工智能原理自学】激活函数:给机器注入灵魂

&#x1f60a;你好&#xff0c;我是小航&#xff0c;一个正在变秃、变强的文艺倾年。 &#x1f514;笔记来自B站UP主Ele实验室的《小白也能听懂的人工智能原理》。 &#x1f514;本文讲解激活函数&#xff1a;给机器注入灵魂&#xff0c;一起卷起来叭&#xff01; 目录一、“分…

已解决pandas正确创建DataFrame对象的四种方法(通过list列表、dict字典、ndarray、Series对象创建)

已解决&#xff08;pandas创建DataFrame对象失败&#xff09;ValueError: 4 columns passed, passed data had 2 columns 文章目录报错代码报错翻译报错原因解决方法创建DataFrame对象的四种方法1. list列表构建DataFrame2. dict字典构建DataFrame3. ndarray创建DataFrame4. Se…

【MySQL】十一,存储引擎

查看存储引擎 查看mysql提供什么存储引擎 设置系统默认的存储引擎 查看默认的存储引擎 show variables like %storage_engine%; SELECT default_storage_engine;修改默认的存储引擎 如果在创建表的语句中没有显式指定表的存储引擎的话&#xff0c;那就会默认使用 InnoDB 作…

08、ThingsBoard使用msa构建镜像并上传到Harbor

1、概述 今天讲解如何使用thingsboard源码中的msa模块来构建镜像,首先我先说一下这个模块的作用,很多人都不明白msa是如何构建镜像的,msa下面创建了很多模块,每个模块都是一个应用,就像我们平时微服务一样可以独自启动,thingsboard是使用这些模块去其它模块拉取代码过来…

卷积神经网络 Convolutional Neural Network (CNN)

CNNObservation 1Observation 2Benefit of Convolutional LayerMultiple Convolutional LayersComparision of Two StoriesObservation 3Convolutional Layers PoolingApplication: Playing GoTo learn more ...仅供个人学习&#xff0c;不具参考价值&#xff01; Image Class…

python代码运行速度有点慢 ? 教你使用多线程速度飞升

前言 嗨喽&#xff0c;大家好呀~这里是爱看美女的茜茜呐 又到了学Python时刻~ 在我们爬取数据的时候,有时候它运行速度会非常慢 那么怎么解决呢? 这里给大家带来一个多线程的方法 我们用采集二手车来举例 环境使用: Python 3.8 Pycharm 模块使用: requests 数据请求模…

JVM类加载机制-让你明明白白的了解类的执行流程

一、类加载运行过程1.1 类加载到jvm的流程当我们使用java命令运行某个类的main函数启动程序时&#xff0c;首先需要通过类加载器把主类加载到jvm里。1.2 loadClass的类加载过程其中loadClass的类加载过程有如下几步&#xff1a;加载 >> 验证 >> 准备 >> 解析…

关于Visual Studio C++项目属性设置的说明

链接器—常规—输出文件 这里的输出文件指的是最终生成的exe文件 C/C–常规—附加包含目录 这里指的是需要include的一些头文件所在的 目录&#xff0c;可以提前在项目文件中建立好include文件&#xff0c;然后在这里设置&#xff0c;以后将一些自定义的include头文件放在这个…

Python之父强烈推荐,爬虫入门经典《python3网络爬虫开发实战》

实操性很强的一本工具书&#xff0c;由浅入深&#xff0c;深入浅出。前面从环境配置到爬虫原理、基本库和解析库的使用再到数据存储&#xff0c;一步步把基础打好&#xff0c;然后再分门别类介绍了Ajax、动态页、验证码、App的爬取、识别等&#xff0c;还讲解了代理使用、模拟登…

【Java】Spring中Aware接口的作用

Spring的几个aware接口的用法 ApplicationContextAware public interface ApplicationContextAware extends Aware {void setApplicationContext(ApplicationContext applicationContext) throws BeansException; }该接口仅有一个方法&#xff0c;用于设置Spring容器上下文。…

浅谈Springboot自动配置

目录 1.自动配置原理 2.自动配置案例 3.总结 1.自动配置原理 在一个Springboot程序中&#xff0c;我们只需要在main程序中使用springBootApplication注解即可标记为一个一个Springboot程序&#xff0c;然后使用 SpringApplication.run(TestMain.class,args) 代码即可创建…

多把锁,死锁,活锁,饥饿

目录 多把锁 多把锁的优缺点 活跃性 死锁 手写死锁 死锁的四个必要条件 定位死锁 jconsole运行命令 jps 定位进程 id&#xff0c;再用 jstack 定位死锁 死锁的三种场景 一个线程一把锁 两个线程两把锁 多个线程多把锁 解决死锁 活锁 饥饿 多把锁 现在有一个场…