常见锁策略,CAS,synchronized原理

news2024/11/17 10:59:57

1.常见锁策略

锁策略不仅仅局限于java,任何与"锁"相关的话题(操作系统,数据库...),都会涉及到锁策略,这些策略是给锁的实现者用来参考的

1.1乐观锁vs悲观锁

这个不是两把具体的锁.而是两类锁,是在锁冲突的概率上进行区分的

乐观锁指的是预测锁竞争不是很激烈(做的工作相对少一些),悲观锁预测锁竞争会很激烈(这里做的工作会多一些).

1.2轻量级锁vs重量级锁

是从锁开销的角度区分的

轻量级锁加锁解锁开销比较小,效率更高.重量级锁加锁解锁开销比较大,效率更低.

多数情况下,乐观锁也是一个轻量级锁,悲观锁也是一个重量级锁

1.3自旋锁vs挂起等待锁

自旋锁是典型的轻量级锁

挂起等待锁是典型的重量级锁]

自旋锁

自旋锁伪代码:

while (抢锁(lock) ==失败) {}

自旋锁如果获取锁失败,立即再尝试获取锁,无限循环..一旦锁被其他线程释放,就能第一时间获取到锁

自旋锁的优点:
没有放弃cpu,不涉及线程阻塞和调度,一旦锁被释放,就饿能第一时间获取到锁
缺点:
如果锁被其它线程持有的时间较长,那么就会持续的消耗cpu资源(挂起等待是不需要消耗资源的)

挂起等待锁

挂起等待锁:如果一个锁被另外的线程持有,挂起等待锁会一直等待,不会主动去获取锁

这种做法不会消耗大量cpu资源,就可以做别的工作了.

1.4互斥锁vs读写锁

互斥锁

提供加锁和解锁操作,就像我们使用过的synchronized这样的锁.如果一个线程加锁了,另一个线程也尝试获取锁,就会阻塞等待

读写锁

提供了三种操作

1.针对读加锁
2.针对写加锁
多线程针对同一个变量并发读是没有线程安全问题的.也不需要加锁.
读锁和读锁之间没有互斥
写锁和写锁之间是互斥的
写锁和读锁之间存在互斥
假设一组线程并发读同一个变量,这时线程之间是没有锁竞争的,也没有线程安全问题!假设一组线程有读又有写,才会产生锁竞争..实际开发中,读操作非常高频

3.解锁

1.5公平锁vs非公平锁

公平锁

把公平锁定义为"先来后到"

B比C先来获取锁然后阻塞等待的,当A释放锁之后,B就能先于C获取到锁

非公平锁

不遵守"先来后到"

不管BC谁先来的,当A释放锁之后,BC都有可能获取到锁,synchronized就是非公平锁!

操作系统内部的线程调度就是随机的,如果不做额外的限制,锁就是非公平锁,如果要实现公平锁,就需要额外的数据结构来保存先后顺序
公平锁和非公平锁没有优劣,要看适用的场景

1.6可重入锁vs不可重入锁

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

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

1.7使用锁策略描述synchronized

上述种锁策略,就像是锁的形容词.任何一个锁,都能用上述锁策略来描述,形容,我们看synchronized是怎样的

1.synchronized既是一个悲观锁,又是个乐观锁
synchronized默认是乐观锁,但是如果发现锁竞争比较激烈,就会变成悲观锁!!
2.synchronized既是轻量级锁,又是一个重量级锁
synchronized默认是轻量级锁,当锁冲突剧烈后,就变成重量级锁!
3.synchronized这里的轻量级锁是基于自旋锁的方式实现的
synchronized这里的重量级锁是基于挂起等待锁的方式实现的
4.synchronized不是读写锁
5.synchronized是非公平锁
6.synchronized是可重入锁

2.CAS(Compare And Swap)

一个CAS涉及到以下操作:

我们假设内存中的原数据V,旧的预期值A,需要修改的新值B
1.比较A与V是否相等
2.如果相等,将B写入V
3.返回操作是否成功

上述交换过程中,大多数不关心B后续的情况了,更关心的是V这个变量的情况.近似可以理解成赋值了

如果AV不同,则没有其他操作

我们看一下CAS的伪代码:

boolean CAS(V,A,B){
    if(A == V){
        V = B;
        return true;
    }
    return false;
}
但是CAS的过程并非是通过代码实现的!!而是通过一条CPU指令完成的!CAS操作是原子的,因此它是线程安全的.那么解决线程安全问题除了加锁,就又有个新的思路了.
CAS是CPU提供的一个特殊指令,通过这个指令,就可以一定程度的处理线程安全问题!

2.1CAS应用场景

实现原子类

Java标准库中提供的有原子类,之前我们学习线程安全时,写过一个问题,两个线程对同一个变量进行自增操作后,这个变量没有达到预期的结果,我们是通过加锁解决线程安全问题的.这里我们直接使用原子类,就不会出现线程安全问题

        AtomicInteger count = new AtomicInteger();

AtomicInteger是原子类,基于CAS实现了自增,自减等操作,此时进行自增等操作不需要加锁,也线程安全的

public class Test {
    public static void main(String[] args) throws InterruptedException {
        //使用原子类解决线程安全问题
        AtomicInteger count = new AtomicInteger();
        Thread t1 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count.getAndIncrement();
            }
        });
        Thread t2 = new Thread(()->{
            for (int i = 0; i < 50000; i++) {
                count.getAndIncrement();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(count);
    }
}

结果:

我们看一下伪代码实现的原子类

class AtomicInteger {
    private int value;
    public int getAndIncrement() {
        int oldValue = value;
        while ( CAS(value, oldValue, oldValue+1) != true) {
            oldValue = value;
       }
        return oldValue;
   }
}

这里的oldValue可以理解为是寄存器中的值,相当于先把内存中的值读到寄存器里

正常情况下,oldValue应该是和value的值是相同的,然后这里发生CAS,把old Value+1写到value中

但是也可能会有:执行完读取value到寄存器中后,线程切换了,另外一个线程也修改了内存中value的值,此时这个线程如果继续执行进行CAS判定,就会认为value和oldValue不相等了

value和oldValue不相等,然后重新读取oldValue

我们画图解释一下这个过程:

按照这个时间执行两个线程

t1,t2都进行加载

然后t2开始CAS

比较oldValue和value的值,发现相等,oldValue+1赋给value

t2线程执行完毕,切换回t1线程,t1线程开始CAS,发现oldValue和value的值不相等,返回false,不进行任何交换...然后进入循环,循环内部重新读取value的值到oldValue 中,此时再次比较,发现相等了,进行CAS操作,并返回true,循环结束

原子类这里的实现,每次修改之前都会再确认一下这个值是否符合要求

CAS是属于特殊方法,特定场景能使用,加锁操作是通用方式,各种场景都能使用,打击面很广!

实现自旋锁

我们看一下自旋锁的伪代码

public class SpinLock {
    private Thread owner = null;
    public void lock(){
        // 通过 CAS 看当前锁是否被某个线程持有.
        // 如果这个锁已经被别的线程持有, 那么就自旋等待.
        // 如果这个锁没有被别的线程持有, 那么就把 owner 设为当前尝试加锁的线程.
        while(!CAS(this.owner, null, Thread.currentThread())){
       }
   }
    public void unlock (){
        this.owner = null;
   }
}
Thread owner是记录当前锁是谁加的
this.owner是检测当前的owner是否是null,如果是null的,就进行交换,也就是把当前的线程的引用赋值给owner.如果赋值成功,此时循环结束,加锁完成!
如果当前锁已经被别的线程占用了,那么owner就不是null的,那么CAS就不会产生赋值,同时返回false,循环继续执行,进行下次判断,这就完成了自旋过程!!

在Java中,并不是直接提供了一个方法CAS.此处伪代码是便于理解

2.2CAS的ABA问题

CAS在运行中的核心是检查oldValue和value是否一致,如果一致,就认为value中途没有被修改过.所以进行下一步操作是没问题的

但是还有可能是中途被修改过,然后又还原回来了.把value值设为A,CAS判定value为A,此时value确实可能始终是A,也有可能本来是A,然后被修改为B,最后又还原成了A!这就是ABA问题

ABA情况大部分是不会对代码/逻辑产生太大影响的,当然也有极端情况,我们看下面这个情景:

如果ATM取钱使用的是CAS来扣款,假设A的账户余额1000,要取500.当按下取款按键时,机器卡顿了,A没忍住多按了几下,此时就会产生bug,可能出现重复扣款的现象

正常情况下,机器卡顿多按两次,t1线程的CAS发现余额是1000,然后就交换成500.扣款成功,然后t2线程加载时余额也是1000,CAS发现余额不是1000,就不扣款.正确的逻辑

下面这种情况,当t2执行CAS的时候,正好有人给A转入了500.那么余额就变成1000了, 执行CAS操作,又扣了500,出现了bug!!

当然这种情况出现的概率是很低的,但是还是可能出现,针对这种情况,采取的解决方案就是加入一个版本号,初始版本号是1,每次修改版本号都加1,然后进行CAS的时候,不是以金额多少为准了,是以版本号为准,此时如果版本号没变,就一定没有发生改变

3.synchronized原理

两个线程针对同一个变量加锁,就会阻塞等待.除了上述基本原理,synchronized还有一些内部的优化机制,存在的目的就是为了让锁更高效,好用.

3.1锁升级/锁膨胀

当执行到加锁的代码块儿时,加锁过程就可能经历下面几个升级阶段

无锁

无锁状态,还没开始加锁

偏向锁

进行加锁的时候,首先会进入偏向锁状态

偏向锁,并不是真正的加锁,而只是先占个位置,如果有需要就加锁,没需要就不加锁了

相当于"懒汉模式"提到的懒加载一样,非必要,不加锁

synchronized加锁的时候,并不是真正的加锁,而是先进入偏向锁状态,就相当于做一个标记,如果一直没有别的线程来获取这个锁,那么就不会升级,仅仅只做个标记,因为这个变量本来就只有这个线程要使用,过程也没有出现锁竞争,执行完synchronized{}代码块后,再取消掉标记(偏向锁)即可
但是如果出现了锁竞争,再另一个线程加锁之前,偏向锁会迅速升级为真正的加锁状态!!另一个线程阻塞等待...

轻量级锁

当synchronized发生锁竞争的时候,就会从偏向锁升级为轻量级锁(自旋锁)

此时,synchronized是通过自旋的方式来进行加锁的(就和刚刚伪代码一样的逻辑)

但是,如果很快就释放锁了,自旋是值得的,可以立即获取被释放的锁,反之,迟迟不被释放,那么久迟迟拿不到锁,自旋就不划算了..这时候就需要再次升级了!

重量级锁

一直自旋但是又拿不到锁,synchronized也不会无止境的自旋,此时升级为重量级锁(挂起等待锁)

重量级锁(挂起等待锁)则是基于操作系统原生的API来进行加锁了

linux原生提供了mutex一组API,操作系统北河提供的加锁功能,这个锁是会影响到线程的调度的

此时,如果线程进行了重量级锁的加锁,并且发生了锁竞争,此时线程就会被放入阻塞队列中,暂时不参加CPU的调度了,直到锁被释放了,这个线程才有机会被调度到并有机会获取到锁

锁升级了就不能降级了

3.2锁消除

这是编译器的智能判定,看当前代码是否真的需要加锁,如果这个场景不用加锁,就会自动把加的锁销毁

就像StringBuffer中的关键的方法都是带有synchronized修饰的,就不需要程序员再加锁,加了编译器也会自动销毁!

3.3锁粗化

锁的粒度:synchronized包含的代码越多,粒度就越粗.包含的代码越少,粒度就越细.

通常情况下,粒度细一点比较好,加锁的代码是不能并发执行的,锁的粒度越细,能并发的代码就越多,粒度越粗,能并发的越少.

有些情况,粒度粗反而更好

这种情况下,两次加锁解锁之间的间隙非常小,反反复复加锁解锁效率低开销大,可以直接加一个大锁,将间隙也包括,效率反而高些,毕竟间隙很小,这块儿代码能不能并发执行影响不大!

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

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

相关文章

Ambari2.7.5安装Flink1.14

文章目录下载Flink配置安装源下载ambari-flink-service服务修改配置文件创建用户和组重启Ambari登录Ambari安装Flink提交Flink任务Flink 直接单独提交到 On Yarn指定Flink在Yarn跑的容器运行Flink异常异常1异常2异常3下载Flink配置安装源 wget https://archive.apache.org/dis…

Goby+AWVS 联动

系列文章 AWVS安装与激活 AWVS扫描Web应用程序 AWVS扫描报告分析 GobyAWVS 联动 1.Goby简介 Goby是针对目标企业梳理最全面的工具&#xff0c;同构goby可以清晰的扫描出ip地址开放的端口&#xff0c;以及端口对应的服务&#xff0c;于此同事会根据开放的端口及应用进行实战…

分享116个ASP源码,总有一款适合您

ASP源码 分享116个ASP源码&#xff0c;总有一款适合您 116个ASP源码下载链接&#xff1a;https://pan.baidu.com/s/1LEs24-feWjvhac6vzyVecg?pwdnosx 提取码&#xff1a;nosx 下面是文件的名字&#xff0c;我放了一些图片&#xff0c;文章里不是所有的图主要是放不下...&am…

2.线性表

##线性结构 基本特点&#xff1a;结构中各元素之间满足线性关系。 线性关系&#xff1a;数据元素之间存在一对一的关系 1.存在唯一的开始元素 2.存在唯一的终止元素 3.除了开始元素和终止元素&#xff0c;其他元素均有且仅有一个直接前驱元素和一个直接后驱元素。 所有元素可排…

Sass进阶指南 -- 写出更优雅的样式表

我以为已经会了&#xff0c;之前在公司写项目基本都是用sass写样式&#xff0c;十分顺手。直到有段时间&#xff0c;我准备参考Element Plus来设计自己组件库的工程结构&#xff0c;看到Element Plus那些优雅的sass用法时&#xff0c;我开始为我的浅薄和无知感到羞愧。这便开始…

Python学习笔记——集合

集合&#xff08;set&#xff09;是一个无序的不重复元素序列。可以使用大括号 { } 或者 set() 函数创建集合&#xff0c;注意&#xff1a;创建一个空集合必须用 set() 而不是 { }&#xff0c;因为 { } 是用来创建一个空字典。创建格式&#xff1a;parame {value01,value02,..…

LeetCode[313]超级丑数

难度&#xff1a;中等题目&#xff1a;超级丑数 是一个正整数&#xff0c;并满足其所有质因数都出现在质数数组 primes中。给你一个整数 n和一个整数数组 primes&#xff0c;返回第 n个 超级丑数 。题目数据保证第 n个 超级丑数 在 32-bit 带符号整数范围内。示例 1&#xff1a…

C++ 类和对象(三)

类和对象&#xff08;一&#xff09; 类和对象&#xff08;二&#xff09; 日期类 目录 一. 再谈构造函数 1.构造函数体赋值 2.初始化列表 3.隐式类型转换与explicit关键字 4.C11优化 二. static成员 1.概念 2.特性 三. 友元 1.友元函数 2.友元类 四. 内部…

Java 如何设计一款小游戏详细讲解(二)

继续上面(一)&#xff0c;Java 如何设计一款小游戏详细讲解(一)步骤步骤一接下来我们主要完成GameJFrame&#xff0c;就是拼图游戏的界面及代码。这里接下来完成菜单的设置&#xff0c;将主界面的代码放到一个方法中&#xff0c;像下面这样&#xff0c;接下来我们书写菜单的代码…

基于Python实现种差值方法(完整代码详细教程)

三种插值方法都是使用Python自己实现的。1.1 最近邻插值寻找每个中心点周围的八个点中有无未丢失的点&#xff0c;如果有的话就赋值为第一个找到的点&#xff0c;如果没有就扩大范围再次寻找&#xff0c;在最大范围内都找不到的话就跳过。1.2 双线性插值使用解方程的方法求解&a…

(十八)Threads异步和多线程(Thread、Threadpool、Task)-语言进阶2

Threads异步和多线程-语言进阶2一、Thread1. 线程启动2. 线程等待3.前台线程/后台线程4.扩展thread封装回调二、Threadpool1.线程池2.线程池使用3.ManualResetEvent 线程池等待三、Task1. Task启动方式2.waitall 、waitany1.waitall2.waitany3.WaitAll、waitany场景4. 应用&…

Spring为什么这么火 之 Spring的创建及存储、获取Bean对象

目录 1、创建Spring项目 1.1、创建一个Maven项目 1.2、添加Spring框架支持 1.3、添加启动类 2、存储Bean对象 2.1、创建Bean对象 2.2、将Bean对象注册到Spring容器中 3、获取、使用Bean对象 3.1、得到Spring上下文对象 使用ApplicationContext作为Spring的上下文 【更…

服务器相关命令(docker相关)

一:安装docker 之前安装过旧版本&#xff0c;使用以下命令可以卸载(整个copy过去): yum remove docker \docker-client \docker-client-latest \docker-common \docker-latest \docker-latest-logrotate \docker-logrotate \docker-selinux \docker-engine-selinux \docker-e…

AOP面向切面编程

AOP 面向切面编程 AOP是什么 AOP 为 Aspect Oriented Programming 的缩写&#xff0c;意为&#xff1a;面向切面编程&#xff0c;通过预编译方式和运行期间动态代理实现程序功能的统一维护的一种技术。 AOP 是 OOP 的延续&#xff0c;是软件开发中的一个热点&#xff0c;是 j…

【Linux】-- 环境变量

目录 环境变量 常见环境变量 HOME 和环境变量相关的命令 通过代码如何获取环境变量 环境变量参数 通过第三方变量environ获取 通过getenv函数来特定访问获取 通过setenv函数来特定访问获取 环境变量 命令&#xff1a;which ls 将命令ls的完整路径写入到标准输出。 #in…

用R语言理解洛必达法则

文章目录5 洛必达法则极限的种类洛必达法则作用于幂函数5 洛必达法则 极限的种类 令NNN为常数&#xff0c;则常规的极限运算大致有以下几种 ∞N∞∞⋇N∞(N̸0)N∔∞∞N−∞−∞N/∞0N/0∞N∞∞(N̸1)∞N∞(N̸0)\begin{matrix} &\infty\pm N\infty\quad&\infty\divi…

MySQL版本由5.7.37更新到5.7.39

一、前景 由于mysql5.7.37存在漏洞&#xff0c;影响系统安全&#xff0c;所以需要将mysql版本升级到5.7的最新版本5.7.39。 二、步骤 1、下载5.7.39的安装包&#xff1a; 下载链接如下&#xff1a; https://downloads.mysql.com/archives/get/p/23/file/mysql-5.7.39-1.el…

node基础知识

node基础知识 node在真实项目中的应用 webpack基于node环境 用nodejs的语法合并压缩打包 js放到客户端浏览器中执行 放到服务器端运行&#xff1a;java因为jdk php因为tomcat c#因为有.net framework 项目架构1&#xff1a;中小型项目-基于nodejs构建全栈 项目架构2&#xff1a…

mysql转DM达梦数据库+springboot兼容DM数据库+springboot兼容activity5.22.0

由于现在做的项目中需要针对数据库进行国产化操作&#xff0c;最终完成从mysql到达梦的迁移&#xff0c;记录整合迁移记录如下&#xff1a;安装初始化达梦数据库&#xff08;傻瓜式安装即可&#xff09;安装达梦数据库&#xff08;windows、linux&#xff09;初始化数据库实例关…

代码质量与安全 | 新时代:2023年商业软件开发的五大关键目标

进入2023年&#xff0c;技术趋势仍然聚焦于人工智能、边缘智能和气候变化领域&#xff0c;但供应链增速放缓和日益增长的消费者需求阻碍着创新的步伐。为了在行业中保持竞争力&#xff0c;并实现软件工程预定目标&#xff0c;软件开发领导者需要主动制定预算和时间管理计划&…