【JavaEE初阶】第一节.多线程(进阶篇 ) 常见的锁策略、CAS及它的ABA问题

news2024/11/29 23:53:46

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档

文章目录

  • 前言
  • 一、常见的锁策略
  •       1.1 乐观锁 vs 悲观锁
  •       1.2 普通的互斥锁 vs 读写锁
  •       1.3 重量级锁 vs 轻量级锁
  •       1.4 自旋锁 vs 挂起等待锁
  •       1.5 公平锁 vs 非公平锁
  •       1.6 可重入锁 vs 不可重入锁
  • 二、CAS
  •       2.1 CAS典型应用场景
  •              2.1.1 使用CAS实现原子类
  •              2.1.2 使用CAS实现自旋锁
  •       2.2 CAS中的ABA问题(小概率bug)
  •             2.2.1 什么是ABA问题
  •             2.2.2 ABA问题引发的bug
  •             2.2.3 解决ABA问题的办法
  • 总结


前言

前几节内容我们介绍了有关多线程基础的有关内容,今天开始我们将进入到多线程的进阶的学习当中;接下来的内容会有很大的难度;希望各位能够认真学习,争取能够很好的掌握今天将要学习的内容;

进阶篇中的很多知识,不再是工作中常用的,但是却是在面试中常考的(俗称:面试造核弹,工作拧螺丝)


提示:以下是本篇文章正文内容,下面案例可供参考

一、常见的锁策略

简单通俗的来说,锁策略就是 加锁的时候是咋加的

1.1 乐观锁 vs 悲观锁

乐观锁:预测接下来锁冲突的概率不大,就会少做一点工作,成本更小;

悲观锁:预测接下来锁冲突的概率很大,就会多做一点工作,成本更大;

举例说明:

比如说,就前段时间,西安那边又有确诊的了;

有居民就比较紧张,就在想是不是要在家里屯点菜啥的(疫情会引起封城,封城会影响买菜),提前屯点菜以备不时之需

这个就可以看作是 悲观锁,花费所需成本较大(买菜、运菜、放在地上......);

当时有居民却认为,由于之前已经有过几次确诊的经历,所以说 已经有了不少经验了,所以封城的概率比较小,不需要提前屯菜(屯了吃不完大概率会坏)

这个就可以看作是 乐观锁,话费所需成本更小;

synchronized

就既是一个悲观锁,也是一个乐观锁,准确的来说 它是一个自适应锁;

如果当前锁冲突概率不大,就以乐观锁的方式运行,往往是纯用户态执行的;

一旦发现锁冲突概率大了,就以悲观锁的方式运行,往往要进入内核,对当前线程进行挂起等待;

1.2 普通的互斥锁 vs 读写锁

synchronized 就属于普通的互斥锁,两个加锁操作之间会发生竞争;

读写锁,把加锁操作细化了 "加读锁" "加写锁";


情况一:

线程A 尝试加写锁,线程B 尝试加写锁,此时 A和B 产生竞争,和普通的锁没有区别;


情况二:

线程A 尝试加读锁,线程B 尝试加读锁,此时 A和B 不产生竞争,和没有加锁一样(多线程读,不涉及修改,是线程安全的)

这种情况是相当普遍的;


情况三:

线程A 尝试加读锁,线程B 尝试加写锁,此时 A和B 不产生竞争(一个读一个写所以不存在竞争),和普通的锁没有区别;

1.3 重量级锁 vs 轻量级锁
 

重量级锁:锁开销比较大,做的工作比较多;

轻量级锁:锁开销比较小,做的工作比较小;

重量级锁、轻量级锁 与之前所介绍的 乐观锁、悲观锁 差不多(内容上不是完全的区分开),但是最终的着力点还是不一样的

其中,在大部分情况下(不绝对),悲观锁 经常会是重量级锁,乐观锁 经常会是轻量级锁

重量级锁 主要依赖了操作系统提供的锁,使用这种锁,容易产生阻塞等待;

轻量级锁 主要尽量的避免使用操作系统提供的锁,尽量在用户态完成功能,尽量避免用户态和内核态的切换,尽量避免挂起等待;

同时,synchronized 是一个自适应锁,既是轻量级锁,也是重量级锁;

锁冲突不高:轻量级;

锁冲突很高:重量级;

1.4 自旋锁 vs 挂起等待锁

自旋锁 是轻量级锁的具体实现,挂起等待锁 是重量级锁的具体实现;

自旋锁:当发生锁冲突的时候,不会挂起等待,会迅速来尝试看这个锁能不能获取到(更轻量,乐观锁)

特点:

  1. 一旦锁被释放,就可以第一时间获取到;
  2. 如果锁一直不释放,就会消耗大量的;

可以看作是一个 不断的循环,可以用一个伪代码来表示:

//自旋锁伪代码,不停的循环
while(抢锁(lock) == 失败) {
 
}

挂起等待锁:发现锁冲突,就挂起等待(更重量,悲观锁)

特点:

  1. 一旦锁被释放,不能第一时间获取到;
  2. 在锁被其他线程占用的时候,会放弃CPU资源;

总结:

synchronized 作为轻量级锁的时候,内部是 自旋锁作为重量级锁的时候,内部是 挂起等待锁

1.5 公平锁 vs 非公平锁

啥样的情况才算是公平?

一般认为,符合 "先来后到" 这样的规则,就是公平!!!

公平锁:多个线程等待一把锁的时候,谁先来尝试拿着一把锁,这把锁就是谁的;

非公平锁:多个线程等待一把锁的时候,就和哪个线程先来后到没有关系,每个线程拿到锁的概率是均等的;

synchronized 是非公平锁;

1.6 可重入锁 vs 不可重入锁

一个线程连续加锁两次,不会造成死锁,那么这个锁就叫做 可重入锁

一个线程连续加锁两次,会造成死锁,那么这个锁就叫做 不可重入锁

 代码示例:

  private  static void func() {
        //......进行一些多线程操作
        //第一次加锁
        synchronized (Demo27.class) {
            //第二次加锁
            synchronized (Demo27.class) {
 
            }
        }
    }

如上述代码,第一次加锁能够成功,Demo27.class 处于被加锁的状态;但是 第二次加锁,由于 Demo27.class 已经是被加锁的状态了,所以就会呈现出 阻塞状态;

要等待第一次加锁释放掉,第二次加锁才能够成功;但是 要想第一次加锁释放,那么 又必须要到第二次加锁成功之后,代码往下执行 .

这样就构成了一个死循环,就叫做 死锁!!!

 synchronized 属于可重入锁;

 

二、CAS

CAS 是操作系统硬件 给JVM提供的另外一种更轻量的原子操作的机制

准确来说,CAS是CPU提供的一条特殊的指令 —— compare and swap(比较和交换);

CAS 是一个原子指令;

比较:是比较内存和寄存器的值;

如果相等,则把寄存器和另一个值进行交换;如果不相等,就不进行操作;

代码实现:

//CAS 的伪代码来理解它的工作流程
//其中,address表示内存地址,expextValue表示一个寄存器中 用来比较的值,
//expextValue表示另一个寄存器中 用来交换的值
boolean CAS(address,expextValue,swapValue) {
    if(&address == expextValue) {
        &address = swapValue;
        return true;
  }
  return false;
}
//上面一系列操作都是由一个CPU指令来完成的

2.1 CAS典型应用场景

2.1.1 使用CAS实现原子类

原子类:这是标准库中提供的一组类,可以让原子的进行 ++、-- 等运算 ;

代码实现:

package thread;
 
public class Demo28 {
    public static int count = 0;
 
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count++;
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count = " + count);
    }
}

在之前,我们已经介绍过,最终的结果不是 10_0000 ;

运行结果:


我们可以使用加锁来解决这个问题,也可以使用原子类来解决这个问题: 

package thread;
 
import java.util.concurrent.atomic.AtomicInteger;
 
public class Demo28 {
    //public static int count = 0;
    public static AtomicInteger count = new AtomicInteger(0);
 
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                //count++;
                //这个方法相当于count++
                count.getAndIncrement();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                //count++;
                count.getAndIncrement();
            }
        });
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println("count = " + count);
    }
}
//和之前的不同的代码已注释,这是使用 原子类来解决问题的,没有使用加锁操作,也实现了线程安全

运行结果:


在Java标准库 里面提供了基于CAS所实现的 "原子类",是线程安全的;

这些 "原子类" 通常以 Atomic 开头,对常用的 int、long等等 进行了封装,如:

2.1.2 使用CAS实现自旋锁 

代码实现:

//自旋锁伪代码
public class SpinLock {
    private Thread owner = null;
    public void lock() {
        //当前的owner是否为空,为空即为当前没有加锁,于是就进行交换,
        //把当前要给加锁的线程的值赋予owner
        //非空就不去进行交换,就循环继续进行,呈现自旋的状态
        while(!CAS(this.owner,null,Thread.currentThread())) {
    }
  }
}

当 owner 为 null 的时候 CAS 才能成功,循环才能结束;

当 owner 为非null,这说明当前的锁已经被其他线程给占用了,因此 就需要继续循环(自旋);

2.2 CAS中的ABA问题(小概率bug) 

2.2.1 什么是ABA问题

ABA问题可以单纯的这样理解:如果你去买一个手机,那么你无法区分 它是一个新机,还是一个翻新机(二手的、外面包装和新机一样);

类似的,在CAS里面,也无法区分,数据始终就是A;还是数据从 A 变成 B,之后又变回了 A ;

如果是前者,那么一点问题都没有;但是如果是后者,那么 CAS 就会有一定的概率引发 bug(极端情况下的小概率事件) 

图示示例:

 2.2.2 ABA问题引发的bug

这里结合一个具体的例子,来介绍ABA问题引发的bug;

举例说明;

假设滑稽老铁有 1000 存款,此时想要从 ATM机 上取走 500(ATM机 按照CAS的方式来进行操作)

取钱的时候,按下取款按钮,就会触发一个 "取钱的线程",但是 滑稽老铁手一滑,连续按了两下(即 产生了两个线程)


但是,怕就怕在这期间 突然又来了一个线程(比如说 滑稽老铁的一个朋友,此时正好向滑稽老铁转了500)

这时候,就扣除了两次钱了,这个就是典型的ABA问题(极端情况下的小概率问题);

此时,线程2不知道 当前的1000,始终是1000;还是 1000 -> 500 -> 1000 ;

2.2.3 解决ABA问题的办法

正经的解决ABA问题的办法,是想办法获取到中间过程 —— 引入一个 "版本号" 来解决 ;

在上述的例子当中,CAS是比较的是 余额,余额相同,就可以进行修改(余额是可以变大和变小,所以就会出现ABA问题)

但是,如果换成 "版本号",并且规定 "版本号" 只能增不能减,那么就不会出现ABA问题;

当然,解决ABA问题的办法肯定不止这一种,这里只是列举了一种非常典型的办法 ;

 总结

好了,这篇博客到这里就已经结束了

本篇博客主要介绍的是 各种常见的锁策略,以及CAS、CAS中的小概率bug —— ABA问题,并且介绍了ABA问题的解决方案 ;

 

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

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

相关文章

Mac - Spotlight(聚焦)

文章目录一、Mac 中 Spotlight 的使用1、调用/打开 Spotlight2、执行搜索3、Spotlight 设置二、Mac 上的 Spotlight 开发1、关于 Spotlight2、使用 NSMetadataQuery 搜索示例三、mds 和 fsevents四、命令行访问 Spotlight五、Core Spotlight Framework六、Spotlight 插件相关资…

拼多多订单查询

下载地址webcrawl最新版本下载、安装、运行教程使用场景场景一我有很多个拼多多的买家号&#xff0c;想通过一个订单编号&#xff0c;查询该订单的各种信息&#xff0c;如订单状态&#xff0c;收件信息&#xff0c;物流信息&#xff0c;售后信息&#xff0c;联系商家场景二有很…

unix高级编程-fork和execve

fork和vfork vfork是老的实现方法又很多问题 vfork #include <sys/types.h> #include <unistd.h> #include <stdlib.h> #include <stdio.h> #include <string.h> #include <signal.h> #include <errno.h> #include <sys/stat.…

AQS-ReentrantLock

一、AQS 在 Lock 中&#xff0c;用到了一个同步队列 AQS&#xff0c;全称 AbstractQueuedSynchronizer&#xff0c;它是一个同步工具&#xff0c;也是 Lock 用来实现线程同步的核心组件。 1.AQS 的两种功能 独占和共享。 独占锁&#xff1a;每次只能有一个线程持有锁&#x…

Git 分支操作

1&#xff1a;什么是分支几乎所有的版本控制系统都以某种形式支持分支。 使用分支意味着你可以把你的工作从开发主线上分离 开来进行重大的Bug修改、开发新的功能&#xff0c;以免影响开发主线。 几乎所有的版本控制系统都以某种形式支持分支。 使用分支意味着你可以把你的工作…

2023“Java基础-中级-高级”面试集结,已奉上我的膝盖

Java基础&#xff08;对象线程字符接口变量异常方法&#xff09; 面向对象和面向过程的区别&#xff1f; Java 语言有哪些特点&#xff1f; 关于 JVM JDK 和 JRE 最详细通俗的解答 Oracle JDK 和 OpenJDK 的对比 Java 和 C的区别&#xff1f; 什么是 Java 程序的主类&…

GLOG如何控制输出的小数点位数

1 问题 在小白的蹩脚翻译演绎型博文《GLOG从入门到入门》中&#xff0c;有位热心读者提问说&#xff1a;在保存日志时&#xff0c;浮点型变量的小数位数如何设置&#xff1f; 首先感谢这位“嘻嘻哈哈的地球人”赏光阅读了小白这不太通顺的博客文章&#xff0c;并提出了一个很…

红旗语音助手HMI设计流程之调研篇

红旗智能语音助手是基于红旗4.0智能化平台打造的场景设计研究成果。本篇文章&#xff0c;将会以红旗语音助手为例&#xff0c;带领小伙伴们了解一下HMI设计中的调研工作。在项目中&#xff0c;我们需要要通过多模态的调研手段&#xff0c;去分辨用户的哪些需求是真需求&#xf…

【C++】string类的基本使用

层楼终究误少年&#xff0c;自由早晚乱余生。你我山前没相见&#xff0c;山后别相逢… 文章目录一、编码&#xff08;ascll、unicode字符集、常用的utf-8编码规则、GBK&#xff09;1.详谈各种编码规则2.汉字在不同的编码规则中所占字节数二、string类的基本使用1.string类的本质…

Hive---Hive语法(一)

Hive语法&#xff08;一&#xff09; 文章目录Hive语法&#xff08;一&#xff09;Hive数据类型基本数据类型&#xff08;与SQL类似&#xff09;集合数据类型Hive数据结构数据库操作创建库使用库删除库表操作创建表指定分隔符默认分隔符&#xff08;可省略 row format&#xff…

逆向工具之 unidbg 执行 so

1、unidbg 入门 unidbg 是一款基于 unicorn 和 dynarmic 的逆向工具&#xff0c; 可以直接调用 Android 和 IOS 的 so 文件&#xff0c;无论是黑盒调用 so 层算法&#xff0c;还是白盒 trace 输出 so 层寄存器值变化都是一把利器&#xff5e; 尤其是动态 trace 方面堪比 ida tr…

零基础机器学习做游戏辅助第十四课--原神自动钓鱼(四)yolov5目标检测

一、yolo介绍 目标检测有两种实现,一种是one-stage,另一种是two-stage,它们的区别如名称所体现的,two-stage有一个region proposal过程,可以理解为网络会先生成目标候选区域,然后把所有的区域放进分类器分类,而one-stage会先把图片分割成一个个的image patch,然后每个im…

关于SqlServer高并发死锁现象的分析排查

问题描述 通过定期对生产环境SqlServer日志的梳理&#xff0c;发现经常会出现类似事务与另一个进程被死锁在资源上&#xff0c;并且已被选作死锁牺牲品&#xff0c;请重新运行该事务的异常&#xff0c;简单分析一下原因&#xff1a;在高并发场境下&#xff0c;多个事务同时对某…

Ubuntu 使用Nohup 部署/启动/关闭程序

目录 一、什么是nohup&#xff1f; 二、nohup能做什么&#xff1f; 三、nohup如何使用&#xff1f; 四、怎么查看/关闭使用nohup运行的程序&#xff1f; 命令 实例 一、什么是nohup&#xff1f; nohup 命令运行由 Command参数和任何相关的 Arg参数指定的命令&#xff0c…

【微信小程序】--WXML WXSS JS 逻辑交互介绍(四)

&#x1f48c; 所属专栏&#xff1a;【微信小程序开发教程】 &#x1f600; 作  者&#xff1a;我是夜阑的狗&#x1f436; &#x1f680; 个人简介&#xff1a;一个正在努力学技术的CV工程师&#xff0c;专注基础和实战分享 &#xff0c;欢迎咨询&#xff01; &#…

Kotlin 34. recyclerView 案例:显示列表

Kotlin 案例1. recyclerView&#xff1a;显示列表 这里&#xff0c;我们将通过几个案例来介绍如何使用recyclerView。RecyclerView 是 ListView 的高级版本。 当我们有很长的项目列表需要显示的时候&#xff0c;我们就可以使用 RecyclerView。 它具有重用其视图的能力。 在 Re…

【C语言】-程序编译的环境和预处理详解-让你轻松理解程序是怎么运行的!!

作者&#xff1a;小树苗渴望变成参天大树 作者宣言&#xff1a;认真写好每一篇博客 作者gitee:gitee 如 果 你 喜 欢 作 者 的 文 章 &#xff0c;就 给 作 者 点 点 关 注 吧&#xff01; 程序的编译前言一、 程序的翻译环境和执行环境二、 详解翻译环境2.1编译环境2.1.1预编…

代码随想录算法训练营第七天 | 454.四数相加II 、 383. 赎金信、15. 三数之和、18. 四数之和 、总结

打卡第七天&#xff0c;还是哈希表。 今日任务 454.四数相加II383.赎金信15.三数之和18.四数之和总结 454.四数相加II 代码随想录 class Solution { public:int fourSumCount(vector<int>& nums1, vector<int>& nums2, vector<int>& nums3, ve…

单元测试面试秘籍分享

1. 什么是单元测试 “在计算机编程中&#xff0c;单元测试又称为模块测试&#xff0c;是针对程序模块来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中&#xff0c;一个单元就是单个程序、函数、过程等&#xff1b;对于面向对象编程&#xff0c;最…

j-vxe-table 下拉搜索选择框数据加载过多导致前端崩溃问题

Jeeg-boot j-vxe-table 下拉搜索选择框数据加载过多导致前端崩溃问题 最近用到了Jeeg-boot j-vxe-table的组件&#xff0c;这组件时真J8难用&#xff0c;还好多BUG&#xff0c;想用个slot插槽也用不了&#xff0c;好像官方写了个基础就没怎么管了。&#x1f611; 问题&#xf…