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

作者:学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.mfbz.cn/a/10277.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Python数据结构与算法-树

一、树的概念详情见 https://blog.csdn.net/little_limin/article/details/129845592 Python数据结构与算法-堆排序(NB组)—— 一、树的基础知识二、树的实例:模拟文件系统1、树的存储树结构也是链式存储的,与链表的结构相似&…

类ChatGPT代码级解读:如何从零起步实现Transformer、llama/ChatGLM

前言 最近一直在做类ChatGPT项目的部署 微调,关注比较多的是两个:一个LLaMA,一个ChatGLM,会发现有不少模型是基于这两个模型去做微调的,说到微调,那具体怎么微调呢,因此又详细了解了一下微调代…

Vulnhub_Pylington

目录 一、信息收集 (一)端口服务探测 (二)目录扫描 二、漏洞挖掘 (一)robots敏感信息泄露 (二)python IDE沙箱绕过RCE 1. python敏感函数沙盒绕过 2. exec(__import_…

【ES】搜索结果处理RestClient查询文档

【ES】搜索结果处理&RestClient查询文档2.搜索结果处理2.1.排序2.1.1.普通字段排序2.1.2.地理坐标排序2.2.分页2.2.1.基本的分页2.2.2.深度分页问题2.2.3.小结2.3.高亮2.3.1.高亮原理2.3.2.实现高亮2.4.总结3.RestClient查询文档3.1.快速入门3.1.1.发起查询请求3.1.2.解析响…

Python做个猫狗识别系统,给人美心善的邻居

嗨害大家好鸭!我是爱摸鱼的芝士❤ 宠物真的看着好治愈 谁不想有一只属于自己的乖乖宠物捏~ 这篇文章中我放弃了以往的model.fit()训练方法, 改用model.train_on_batch方法。 两种方法的比较: model.fit():用起来十分简单&#…

Kubernetes 部署 StarRocks 集群

文章目录StarRocks简介系统架构图安装部署StarRocks手动部署通过 Docker部署使用 StarGo 部署管理通过 StarRocks Manager部署管理通过 Kubernetes部署工作原理逻辑图部署 StarRocks Operator部署 StarRocks 集群访问 StarRocks 集群集群内访问 StarRocks 集群集群外访问 StarR…

【案例实践】R语言多元数据统计分析在生态环境中的实践应用

查看原文>>>R语言生物群落分析绘图、多元统计分析、CMIP6、遥感碳储量、GEE林业、InVEST等 生态环境领域研究中常常面对众多的不同类型的数据或变量,当要同时分析多个因变量(y)时需要用到多元统计分析(multivariate sta…

Spark----DataFrame和DataSet

Spark之DataFrame和DataSet 文章目录Spark之DataFrame和DataSetDataFrameDSL 语法创建DataFrame查看DataFrame的Schema信息只查看列数据的6种方式按照“age”分区,查看数据条数增加列withColumn修改列名withColumnRenamedRDD 转换为 DataFrameDataFrame 转换为 RDD转…

音质蓝牙耳机哪款好用?2023公认音质好的四款蓝牙耳机推荐

现如今,蓝牙耳机越来越受欢迎,不少人在听歌、追剧、甚至是玩游戏的时候都会戴着它。最近看到很多人问,音质蓝牙耳机哪款好用?针对这个问题,我来给大家推荐四款公认音质好的蓝牙耳机,一起来看看吧。 一、南…

算法笔记:Frechet距离度量

曲线之间相似性的度量,它考虑了沿曲线的点的位置和顺序 1 概念 1.1 直观理解 主人走路径A,狗走路径B,他们有不同的配速方案主人和狗各自走完这两条路径过程中所需要的最短狗绳长度 (在某一种配速下需要的狗绳长度)&a…

考研复试确认神操作!

终于进行到了研究生考试的尾声,但让考生感到无力吐槽的事情,却还在继续上演,比如苏科大,再比如中地大、苏大,三所学校的神操作,着实让无数考生忍不住调侃:原来考研不仅拼实力,还得拼…

你的APP内存还在暴增吗?试着用Bitmap管理下内存~

作者:layz4android 相信伙伴们在日常的开发中,一定对图片加载有所涉猎,而且对于图片加载现有的第三方库也很多,例如Glide、coil等,使用这些三方库我们好像就没有啥担忧的,他们内部的内存管理和缓存策略做的…

如何实现Chatgpt写文章(附chatgpt3.5免费接口)

申明:本次只是说一下实现思路,官方的接口以及如何实现方式,本文没有提及,这次只是一个思路,若想代替人工完成质量还差的很远,请审核大大放行 今天再次优化了代码,修复了一些bug,考虑…

VUE 学习笔记(一)开发环境搭建

1、Visual Studio Code安装及使用 下载地址官网:https://code.visualstudio.com/ 直接点击下载按钮即可,会根据系统自动下载合适的版本,无需自行选择。 2、VSCode 上安装:JavaScript Debugger 目前 Debugger for Chrome 已经处…

使用向量机(SVM)算法的推荐系统部署实现

包括3个模块:数据预处理、模型训练及保存、模型测试,下面分别给出各模块的功能介绍及相关代码。 数据集下载链接为https://www.aitechclub.com/data-detail? data_id29,停用词典下载链接为http://www.datasoldier.net/archives/636。 1.数…

232:vue+openlayers选择左右两部分的地图,不重复,横向卷帘

第232个 点击查看专栏目录 本示例的目的是介绍演示如何在vue+openlayers项目中自定义js实现横向卷帘。这个示例中从左右两个选择框中来选择不同的地图,做了不重复的处理,即同一个数组,两部分根据选择后的状态做disabled处理,避免重复选择。 直接复制下面的 vue+openlayers…

c语言—指针进阶

创作不易&#xff0c;本篇文章如果帮助到了你&#xff0c;还请点赞支持一下♡>&#x16966;<)!! 主页专栏有更多知识&#xff0c;如有疑问欢迎大家指正讨论&#xff0c;共同进步&#xff01; 给大家跳段街舞感谢支持&#xff01;ጿ ኈ ቼ ዽ ጿ ኈ ቼ ዽ ጿ ኈ ቼ ዽ ጿ…

第13届蓝桥杯省赛真题剖析-2022年4月17日Scratch编程初中级组

[导读]&#xff1a;超平老师的《Scratch蓝桥杯真题解析100讲》已经全部完成&#xff0c;后续会不定期解读蓝桥杯真题&#xff0c;这是Scratch蓝桥杯真题解析第122讲。 第13届蓝桥杯省赛举办了两次&#xff0c;这是2022年4月17日举行的第一次省赛&#xff0c;比赛仍然采取线上形…

ChatGPT技术原理、研究框架,应用实践及发展趋势(附166份报告)

​ 一、AI框架重要性日益突显&#xff0c;框架技术发展进入繁荣期&#xff0c;国内AI框架技术加速发展&#xff1a; 1、AI框架作为衔接数据和模型的重要桥梁&#xff0c;发展进入繁荣期&#xff0c;国内外框架功能及性能加速迭代&#xff1b; 2、Pytorch、Tensorflow占据AI框…

因果推断14--DRNet论文和代码学习

目录 论文介绍 代码实现 DRNet ReadMe 因果森林 论文介绍 因果推断3--DRNet&#xff08;个人笔记&#xff09;_万三豹的博客-CSDN博客 摘要&#xff1a;估计个体在不同程度的治疗暴露下的潜在反应&#xff0c;对于医疗保健、经济学和公共政策等几个重要领域具有很高的实…
最新文章