【Java EE】-多线程编程(九) 锁策略CAS锁优化

news2025/6/15 4:31:27

作者:学Java的冬瓜
博客主页:☀冬瓜的主页🌙
专栏:【JavaEE】
分享
主要内容:乐观锁VS悲观锁、轻量级锁VS重量级锁、自旋锁VS挂起等待锁、互斥锁VS读写锁、公平锁VS非公平锁、可重入锁VS不可重入锁。CAS实现原子类,CAS实现自旋锁、CAS的ABA问题、Synchronized的锁优化:锁升级、锁消除、锁粗化。

文章目录

  • 一、常见的锁策略
    • 1、乐观锁 VS 悲观锁
    • 2、轻量级锁 VS 重量级锁
    • 3、自旋锁 VS 挂起等待锁
    • 4、互斥锁 VS 读写锁
    • 5、公平锁 VS 非公平锁
    • 6、可重入锁 VS 不可重入锁
    • 以synchronized作为示例:
    • 总结:适用场景
  • 二、CAS
    • 1、CAS概念
    • 2、CAS应用场景
      • 1> CAS实现原子类
        • @ AtomicInteger的几个方法
        • @ CAS实现原子类的方法
      • 2> CAS实现自旋锁
    • 3、CAS的ABA问题
  • 三、synchronized的锁优化
    • 1、锁升级
    • 2、锁消除
    • 3、锁粗化

一、常见的锁策略

1、乐观锁 VS 悲观锁

  • 乐观锁:是一种预期乐观的策略,认为锁竞争很少。在操作数据时先不加锁,而是在提交数据时再检查其它线程是否对这个数据进行了修改,如果没有,则提交成功;则需要回滚操作,并重新尝试。
  • 悲观锁:是一种预期悲观的策略,认为并发访问共享资源时,必然会产生冲突。在访问数据时,先加锁,然后再进行操作,确保其它线程无法同时操作该数据。这种锁策略常常会导致性能问题,因为加锁会导致其它线程阻塞等待。
  • 在实际开发中,乐观锁常常被用于高并发的场景,因为它能够提高并发性能。而悲观锁则用于对数据一致性要求较高的场景中,因为它能够保证数据的一致性。

2、轻量级锁 VS 重量级锁

  • 轻量级锁:轻量级锁是一种优化的锁机制,通过CAS(Compare And Swap,下面会讲)操作,来避免线程阻塞和唤醒的开销
    当一个线程尝试获取锁时,如果该锁没有被其它线程占用,它会将锁记录在自己的线程栈帧中,并将锁对象头中的标志位设置成"轻量级锁"状态。如果另一个线程也尝试获取这个锁,它会发现锁对象头中标志位已经被设置成"轻量级锁",就会进入自旋,占用CPU资源,直到该锁被前一个线程释放或者自旋次数达到最大值升级为重量级锁。注意:轻量级锁的自旋次数是有限的,达到一定次数后会变成重量级锁,因次占用资源少,效率高
  • 重量级锁:重量级锁是一种传统的锁机制,它使用操作系统提供的底层线程同步机制来实现(如互斥量、信号量等)
    当一个线程尝试获取锁时,如果该锁已经被其他线程占用,这个线程就会阻塞等待,让出CPU资源,直到该锁被前一个线程释放。重量级锁的缺点是线程的阻塞和唤醒开销比较大。
  • 在实际开发中,轻量级锁适用于短时间内只有一个线程访问共享资源的场景(即锁竞争较少的场景),可以提高效率和性能;重量级锁适用于多个线程竞争同一个锁的场景,可以保证多线程的安全性和准确性。

3、自旋锁 VS 挂起等待锁

  • 自旋锁:自旋锁是一种基于忙等的锁,当一个线程尝试加锁时,如果锁已经被占用,那这个线程就会一直自旋等待锁的释放,直到获取到锁为止。如果锁的持有时间长,则会导致CPU的浪费。
  • 挂起等待锁:挂起等待锁是重量级锁的一种实现方式,和重量级锁的情况类似,当一个线程获取锁,这个锁已经被其他线程占用时,当前线程就被操作系统"挂起",不参与操作系统调度,不占用CPU,直到锁被释放后才能唤醒这个线程并再次尝试获取锁。
  • 总的来说,自旋等待锁适用于锁的持有时间短的情况,挂起等待锁适用于锁的持有时间长的情况。

4、互斥锁 VS 读写锁

  • 互斥锁:一个线程加锁时,如果锁被占用了,那这个线程就进入阻塞等待。
  • 读写锁:对读和写单独加锁,其中读和读之间不互斥,而读和写以及写和写之间存在互斥。
    原因是,读和读之间不存在线程安全问题,而读和写以及写和写都可能引发线程安全问题。并且因为读操作比写操作更频繁,因此同时允许多个线程读取,提高并发性,从而提高效率。
    只有读操作的时候加共享锁,有写操作的时候加排它锁。

5、公平锁 VS 非公平锁

  • 公平锁:当多个线程对被占用的刚被释放的锁,加锁时,阻塞等待时间长的先获取到锁。遵循"先来先服务"的原则,避免了线程饥饿现象,但是可能会导致线程频繁切换上下文,降低了效率和性能。
  • 非公平锁:当多个线程对被占用的刚被释放的锁,加锁时,每个线程都有机会,即随机一个线程获取到锁。减少频繁切换上下文,提高效率。但是可能会导致一些线程一直获取不到锁资源,造成线程饥饿。
  • 总的来说,公平锁适用于对线程执行顺序有严格要求的场景,非公平锁适用于对性能要求较高的场景。

6、可重入锁 VS 不可重入锁

  • 可重入锁:同一个线程对一个对象加两次锁,不会产生死锁。
  • 不可重入锁:同一个线程对一个对象加两次锁,第二次加锁时,锁对象认为这个线程不是给它加了锁的线程,就让这个线程阻塞等待,但是其实都是一个线程,因此产生死锁。一般不建议适用。

以synchronized作为示例:

  • synchronized可以是乐观锁,也可以是悲观锁。synchronized默认是乐观锁,但是如果发现当前锁竞争比较激烈就会变成悲观锁。
  • synchronized可以是轻量级锁,也可以是重量级锁。synchronized默认是轻量级锁,但是如果发现当前锁竞争比较激烈就会变成重量级锁。
  • synchronized的轻量级锁基于自旋锁的方式实现;synchronized的重量级锁基于挂起等待锁的方式实现
  • synchronized是互斥锁,不是读写锁。
  • synchronized是非公平锁,不是公平锁。
  • synchronized是可重入锁。

总结:适用场景

  • 乐观锁和悲观锁:是对锁竞争的一种预测。乐观锁适用于高并发场景;悲观锁适用于数据一致性要求高的场景。
  • 轻量级锁和重量级锁:轻量级锁适用于短时间内锁竞争少的情况;重量级锁适用于锁竞争大的情况,可以保证线程安全。
  • 自旋锁和挂起等待锁:自旋锁适用于锁持有时间短的情况,挂起等待锁适用于锁持有时间长的情况。
  • 轻量级锁和自旋锁的自旋:轻量级锁和自旋锁都是使用自旋的方式等待获取锁,但具体情况有所不同:它们都持续占用CPU资源,但在自旋时如果被占用的锁一直获取不到:
    轻量级锁有一个最大自旋次数,达到这个次数后会变成重量级锁,这个线程再尝试获取锁时,就会阻塞等待,不占用CPU资源;
    而自旋锁会死等,一直占用CPU资源自旋,尝试获取锁,直到原来的线程释放这个锁。
  • 这六种锁策略相当于用来描述一把锁是怎么样的一把锁。

二、CAS

1、CAS概念

CAS:全称Compare And Swap,字面上的意思是:“比较并交换”,这里的交换其实也可以理解为赋值。

比如在下列代码中:在内存中有个V,原来的预设值为A(操作CAS前把V加载(load)到寄存器给A),需要修改成的新值为B(也在寄存器上),那么CAS操作就是先比较此时内存中的V和寄存器上的A是否相等,如果相等则令V=B,返回true,否则就返回false。

伪代码:

boolean CAS(V, A, B) {
    if (&V == A) {
   		&V = B;
        return true;
    }
    return false;
}

需要注意的是:CAS操作是原子的,上面三步操作在CPU上仅仅是一个指令。所以为线程安全的方式又提供了一种思路。

2、CAS应用场景

1> CAS实现原子类

标准库中提供了包:java.util.concurrent.atomic
原子类:这个包中的类的操作是原子的,是基于CAS实现,这个包下的类是线程安全的。

// 有如下类
AtomicBoolean、AtomicInteger、AtomicIntegerArray、
AtomicLong、AtomicReference、AtomicStampedReference。

@ AtomicInteger的几个方法

AtomicInteger类的几个方法如下:

AtomicInteger atomicInteger = new AtomicInteger(10);  // initalValue
atomicInteger.getAndIncrement();  // i++ 获取后增加
atomicInteger.getAndDecrement();  // i-- 获取后减少

atomicInteger.incrementAndGet();  // ++i 增加后获取
atomicInteger.decrementAndGet();  // --i 减少后获取

atomicInteger.addAndGet(10); // i+=delta  增加delta后获取

@ CAS实现原子类的方法

以AtomicInteger类的getAndIncrement()方法使用CSA分析为例

AtomicInteger atomicInteger = new AtomicInteger(0);
atomicInteger.getAndIncrement();      // 相当于 i++

getAndIncrement方法伪代码:

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

分析上面的伪代码:
       一般情况下,value一定是在内存(为了保证原子性),oldvalue和oldvalue+1是在寄存器上(这两个也可以在内存,只是在寄存器上会更快)。第4行中因为是把内存上的值value读到寄存器上得到oldvalue,因此应当是相等的,那么CAS返回值是true,循环结束,此时是i++操作,因此先使用再++,所以返回oldvalue;
       但是,如果把内存中的value的值从内存读取到寄存器后(第4行执行完后,当前线程被切出CPU),当前线程被切出CPU,另外有一个线程进行了CAS操作修改value的值,那么当原来的线程调度回来后,value和oldvalue就不相等了,返回false,因此进入循环,此时就重新把内存中的value的值拷贝到寄存器上得到新的oldvalue,再去进行CAS判定,这样就可以保证原子性。

总的来说:原子类的实现:每次修改之前,看看value是否已经被修改,不修改则直接把oldvalue+1给value;若修改了,则先把value重新读到oldvalue中,再重新CAS

通过形如上述代码就可以通过CAS实现一个原子类.。不需要使用重量级锁,,就可以高效的完成多线程的自增操作。

2> 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;
   }
}

定义一个锁的变量owner,用来标记当前锁被哪个线程拥有。当有一个线程执行lock操作时:

       如果当前锁未被其他线程获取,进入CAS时,owner == null,那么就把当前的线程标记为这个锁的拥有者,因为循环条件的前面还有个非,此时CAS返回true,所以退出循环,这个线程就已经获取到锁。
       如果当前锁已经被占用,那么返回false,取非后就是true,再进入循环,再CAS操作,直到锁被原来的线程释放,这个线程才能获取到锁,这就是CAS实现自旋锁。

形如上面的代码,就可以利用CAS实现一个自旋锁。
然而,虽然CAS也能解决线程安全问题,且高效,但是呢,CAS只能某些特定的情况,因此,大部分情况下还是使用加锁操作。

3、CAS的ABA问题

ABA问题:CAS的原理是:比较value和oldvalue是否相等,如果相等,就视为value没有中途被修改过,所以进行下一步的修改没问题。但是呢有没有可能value被修改过,然后又还原回来了呢?是有可能的!!!

在这里插入图片描述

  • 正常情况下:本来应该是t1线程CAS操作后,t2线程操作时,余额=1000
    那么value!=oldvalue,返回false。那就只完成t1线程的1000元扣款,是合理的

  • 若刚好ATM机卡的时候我点了两下,且这时我妈往我卡里充钱和我取钱一样的金额:
    也就是说:t3线程在t2load后,CAS前给我的账户充值了1000元,那么本来变回1000的value,现在又回到了2000此时,在t2看来,余额没变,满足CAS的条件,所以又扣款,最终value变成1000。
    我妈给我充了1000,我本来还有2000,最终却只剩下了1000,所以就出现了在t1和t2线程中重复扣款的现象

怎么解决?上面的情况出现的原因就是我的value虽然修改了,但又恢复回来了,因此解决的办法就是不让它恢复回去。
方法:使用版本号,以版本号为基准(value),这个版本号从0开始,不管充值还是取钱,每次操作加1,这样,如果版本号没改变,那就确保是还没修改的,就解决的CAS的ABA问题。

三、synchronized的锁优化

1、锁升级

锁升级又叫锁膨胀。
无锁 => 偏向锁 => 轻量级锁 => 重量级锁
当代码执行synchronized(this){ ... }刚进入代码块中时,就会从无锁变成偏向锁。

偏向锁的原则:非必要不加锁,即当前线程获取到锁时,先加上偏向锁,具体是先加一个标记,如果整个过程中没有锁竞争,在synchronized执行完后,取消偏向锁即可;
但是,如果在使用的过程中,如果有其它线程尝试获取这个锁,那么在这些线程获取到锁之前,迅速把偏向锁升级为真正的加锁状态,轻量级锁。

轻量级锁:此时,synchronized通过自旋的方式加锁,直到其它线程释放当前锁或者自旋次数达到最大值变成重量级锁。

重量级锁:基于操作系统原生的API进行加锁,如果此时有一个线程要加锁,但是锁对象被占用了,这个线程就进入阻塞等待。在操作系统中,这个线程对应的PCB就会被放入到阻塞队列中,暂时不占用CPU资源了。

2、锁消除

编译器察觉到我们的代码中加锁的部分其实不需要加锁,它就给我们做了一个优化,就把这个锁消除了。

3、锁粗化

锁的粒度:synchronized包含的代码越多,粒度就越粗,反之则越细。一般来说,锁的粒度细一点会更好,因为可以更好的并发执行。

但是有的情况下,粒度粗一点还反而更好。比如:频繁加锁解锁,并且前一次的解锁和后一次的加锁之间,间隙非常小,那反而粒度粗一点,一次的加锁和解锁搞定更好,因为多次加锁解锁的操作也是有资源消耗的!(比如你打电话时要讲三件事,你应该打一次电话告诉对方三件事情,而不是打三个电话,每次告诉对方一件事)
在这里插入图片描述

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

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

相关文章

docker 执行springboot 报数据源找不到

本地运行springboot项目完全正常&#xff0c;在docker中开启容器&#xff0c;报错&#xff0c;如下&#xff1a; 解决方案&#xff1a;特别简单&#xff08;经过摸爬滚打得出来的结论&#xff09; <resources><resource><directory>src/main/resources</d…

Hum Brain Mapp | 双语者语言控制的遗传基础:一项EEG研究

双语者似乎具有一种独特的能力&#xff0c;在使用一种语言时暂时“忽略”另一种语言&#xff0c;避免无关语言不必要的干扰。这种排除干扰的过程被称为语言控制。部分研究认为双语者的语言控制是通过调用一系列执行功能&#xff0c;如抑制、注意控制、更新、冲突监测和工作记忆…

Linux 云服务器好用吗?(解读Linux云服务器的特点优势)

​  如今&#xff0c;云计算越来越受欢迎&#xff0c;许多公司正在将业务转移到那里。企业向云过渡的主要原因是它提供的众多服务&#xff0c;包括安全和充足的存储、数据库、服务器和其他关键元素。 作为相对前|沿的技术之一&#xff0c;云建立在虚拟服务器上。Linux 服务器…

2.30、守护进程(1)

2.30、守护进程&#xff08;1&#xff09;1.终端是什么2.进程组是什么3.会话是什么4.进程组、会话、控制终端之间的关系5.进程组、会话操作有哪些函数①pid_t getpgrp(void);②pid_t getpgid(pid_t pid);③int setpgid(pid_t pid, pid_t pgid);④pid_t getsid(pid_t pid);⑥pid…

【Java8】新的日期和时间API

【Java8】新的日期和时间API前言为什么要设计新的API&#xff1f;使用LocalDate、LocalTime、LocalDateTimeLocalDateLocalTimeLocalDateTime 合并日期和时间改变日期打印输出及解析日期-时间对象Date和LocalDate、LocalDateTime转换《Java8实战》读书笔记 前言 为什么要设计新…

十二载征程犹未止,看今朝星光尽闪耀丨万字长文回顾2023数据技术嘉年华

4月8日下午&#xff0c;为期两天的第十二届数据技术嘉年华&#xff08;DTC 2023&#xff09;在北京新云南皇冠假日酒店圆满落下帷幕。大会得到了工业和信息化部电子五所的支持和指导&#xff0c;围绕“开源融合数字化——引领数据技术发展&#xff0c;释放数据要素价值”这一主…

基于Spring Boot和Vue3打造一个属于自己的博客平台CodeInsight

CodeInsight是一个基于Spring Boot和Vue3技术栈的博客平台&#xff0c;为开发者和技术爱好者提供了一个专注于现代编程技术分享与学习的高质量平台。在本文中&#xff0c;我们将详细介绍CodeInsight的特点、功能模块以及付费专栏内容。 目录: 《用户体验与交互设计实践》 用户模…

AI新宠:Prompt Learning,用提示学习调教大模型

“提示学习”对于很多人来说都是新名词&#xff0c;Prompt Learning 和 Prompting 这两者之间有什么区别和联系呢&#xff1f;现在的一些大模型如何利用“提示语言”呢&#xff1f;本期直播课为大家做了详细介绍&#xff0c;从 AI 范式的一个变迁&#xff0c;到提示学习&#x…

LVGL V9.0基于VS2022仿真搭建(2)

完整Demo&#xff0c;lvgl,lvgl_drivers相关资料下载 链接&#xff1a;https://pan.baidu.com/s/1h3OKCIBQRX0Hn7KjZsynqg 提取码&#xff1a;sc2l 下载的lv_drivers中的win32drv.c及win32drv.h文件是做了修改的&#xff0c;官网下载的lv_drivers编译会报错&#xff0c;因为l…

数据类型。

数据类型分为简单数据类型&#xff08;值类型&#xff09;和复杂数据类型&#xff08;引用类型&#xff09;值类型&#xff1a;在存储时变量中存储的是值本身&#xff08;string、number、boolean、undefined、null&#xff08;null特殊&#xff0c;返回一个空的对象 object&am…

2023年第十三届MathorCup高校数学建模挑战赛|A题|量子计算机在信用评分卡组合优化中的应用

目录 题目详情 最终收入 贷款利息收入 &#xff0d; 坏账损失 赛题说明 1&#xff1a;流程简化及示例 赛题说明 2&#xff1a;QUBO 模型简介 赛题说明 3&#xff1a;赛题数据 问题​ 题目详情 在银行信用卡或相关的贷款等业务中&#xff0c;对客户授信之前&#xff0c;需…

mycat2安装配置,分库分表,一库多表

1、官网下载&#xff08;官网下载地址&#xff09; 官网下载地址 Index of /2.0/ 下载模板 下载jdk包 下载好后吧jdk包房到mycat的lib目录下 2、配置启动 配置结构 mycat配置文件夹 clusters- prototype.cluster.json //无集群的时候自动创建- c0.cluster.json- c1.cluster…

jupyter notebook笔记 visualpython

通过界面拖拽生成Python代码 0 安装 pip install visualpythonvisualpy install 出现橙色按钮即为成功 1 主界面 2 logic 一些简单的逻辑运算 以class 为例&#xff1a; 3 Data Analysis 3.1 import 3.2 File&#xff08;数据集&#xff09; 3.2.1 读本地文件 3.2.2 写…

【两阶段鲁棒优化】利用列-约束生成方法求解两阶段鲁棒优化问题(Python代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

基于深度学习的农作物叶片病害检测系统(UI界面+YOLOv5+训练数据集)

摘要&#xff1a;农作物叶片病害检测系统用于智能检测常见农作物叶片病害情况&#xff0c;自动化标注、记录和保存病害位置和类型&#xff0c;辅助作物病害防治以增加产值。本文详细介绍基于YOLOv5深度学习模型的农作物叶片病害检测系统&#xff0c;在介绍算法原理的同时&#…

能够翻译文档的免费软件-免费翻译整个文档的软件

chatgpt怎么实现批量翻译 ChatGPT是一种基于人工智能技术的自然语言处理软件&#xff0c;可以实现快速、准确的批量翻译操作&#xff0c;同时也支持多种语言翻译。下面是 ChatGPT 的批量翻译操作流程&#xff1a; 步骤 1: 确定翻译语言和翻译文本 首先需要确定要翻译的原文本…

套接字通信基础

套接字通信基础概述套接字通信基础套接字类型socket模型创建流程分析套接字创建socket()套接字绑定bind()套接字监听listen()套接字接受连接请求accept套接字连接套接字数据传输套接字通信实例服务器端代码客户端代码应用场景总结概述 套接字通信是在Linux系统中实现进程间通信…

WinForms 网格控件 - iGrid.NET 10.1.22 Crack

WinForms 网格控件 - iGrid.NET WinForms 的 10Tec 网格介绍 iGrid.NET 是适用于 Windows Forms 平台的多功能WinForms 网格控件&#xff0c;它是 Microsoft .NET Framework 和 .NET Core 的一部分。软件开发人员使用 iGrid for WinForms 来构建高度可调整的表格界面。它速度…

【MATLAB图像处理实用案例详解(10)】——基于Kalman滤波的目标跟踪预测红色小球位置

目录一、Kalman滤波二、Kalman滤波源程序三、Kalman滤波预测红色小球位置程序实现一、Kalman滤波 Kalman 滤波算法是一个最优化自回归数据处理算法&#xff0c;对于很多问题的解決&#xff0c;它是最优、效率最高甚至是最有用的。Kalman 滤波的广泛应用己经超过30年&#xff0…

LeetCode-120. 三角形最小路径和

目录题目思路动态规划(由上到下)动态规划(由下到上)题目来源 120. 三角形最小路径和 题目思路 由上往下 动态规划(由上到下) 1.确定dp数组以及下标的含义 dp[i][j] 表示从点 (i,j)) 到底边的最小路径和。 2.确定递推公式 常规&#xff1a; triangle[i][j]一定会经过tri…