[多线程进阶]CAS与Synchronized基本原理

news2024/9/22 1:10:32


专栏简介: JavaEE从入门到进阶

题目来源: leetcode,牛客,剑指offer.

创作目标: 记录学习JavaEE学习历程

希望在提升自己的同时,帮助他人,,与大家一起共同进步,互相成长.

学历代表过去,能力代表现在,学习能力代表未来! 


目录:

1.CAS

1.1 什么是CAS?

1.2 CAS伪代码

1.3 CAS 是怎么实现的

1.4 CAS 的应用场景

1) 实现原子类

2) 实现自旋锁(伪代码)

1.5 CAS 的 ABA 问题

1.6 ABA问题引发的 BUG

1.7 相关面试题

2. Synchronized 基本原理

2.1 基本特点

2.2 加锁过程

2.3 其他的优化操作


1.CAS

1.1 什么是CAS?

CAS: 全称 Compare and swap , 字面意思是"比较并交换" , 一个 CAS 涉及到以下操作:

假设内存中原数据 V , A B 分别为寄存器中 , 旧的预期值和需要修改的新值. 

  • 1. 比较 A 与 V 是否相等.(比较)
  • 2. 如果比较相等 , 将 B 写入 V. (交换)
  • 3. 返回操作是否成功.

Tips: 上述交换过程中 , 并不关心 B 变量后续的情况 , 更关心的是 V 这个变量的情况(这里的交换可以理解为赋值) , CAS 可以理解成 CPU 的一个特殊指令 , 通过这个指令就可以一定程度的处理线程安全问题.


1.2 CAS伪代码

真实的 CAS 是一个原子硬件指令完成的 , 这个伪代码只是辅助理解 CAS 的工作流程.

boolean CAS(address , expectvalue , swapvalue){
    if(&address == expectedValue){
        &address = swapValue;
        return true;
    }
    return false;
}

两种典型的不是"原子性"的代码

1.check and set (判定然后设定值)[上面的 CAS 伪代码就是这种形式]

2.read and update(i++)

当多个线程对某个资源进行 CAS 操作 , 只有一个线程操作成功 , 但是并不会阻塞其他线程 , 其实线程只会收到操作失败的信号.

CAS 可以视为是一种乐观锁(或者乐观锁是 CAS 的一种实现方式)


1.3 CAS 是怎么实现的

针对不同的操作系统 , JVM 用到了不同的 CAS 实现原理 , 简单来讲:

  • Java 的 CAS 利用的是 unsafe 这个类提供的 CAS操作;
  • unsafe 的 CAS 依赖的是 jvm 针对不同操作系统实现的 Atomic::cmpxchg
  • Atomic::cmpxchg 的实现使用了汇编的 CAS 操作 , 并使用 CPU 硬件提供的 lock 机制保证其原子性.

简而言之 , 就是因为硬件给予了支持 , 软件层面才能做得到.


1.4 CAS 的应用场景

1) 实现原子类

标准库中提供了 java.util.concurrent.atomic 包 , 里面的类都是基于这个方式实现的. 

典型的就是 AtomicInteger 类.

代码示例:

import java.util.concurrent.atomic.AtomicInteger;

public class ThreadDemo14 {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger count = new AtomicInteger();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) {
                count.getAndIncrement();
            }
        });
        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.get());
    }
}

伪代码实现:

Class AtomicInteger{
    private int value;
    
    public int getAndIncrement(){
        int oldVaue = value;//相当于load操作
        while((CAS(value , oldvalue , oldvalue+1) != true){
            oldvalue = value;
            }
            return oldvalue;
    }
}

oldervalue 相当于寄存器中的值 , value 相当于内存中的值.

正常情况下 , oldvalue 和 value 是一样的 , 可以直接执行 CAS 操作. 但有可能当oldvalue在内存中读取值后 , 线程发生了切换 , 另一个线程也修改了 value 的值 , 此时等这个线程重新回来 . oldvalue和value已经不相等了.

图示:

假设两个线程同时调用 getAndIncrement.

(1). 两个线程都读取 value 的值到 oldvalue 中.

(2). 线程1先进行 CAS 操作. 由于 oldvalue 和 value的值相同 , 直接对 oldvalue 进行赋值.

Tips: 

  • CAS 是直接写内存的不是操作寄存器的.
  • CAS 读内存 , 比较 , 写内存 是一套原子的硬件指令. 

(3) 线程2再进行 CAS 操作 , 第一次 CAS 的时候  , oldvalue和value不相等 , 不能进行赋值 , 因此需要进入循环. 在循环中重新读取 value 的值赋值给 oldvalue.

(4) 线程2 接下来进行第二次 CAS , 此时 oldvalue 和 value 相同 , 于是直接进行赋值操作.

(5) 线程1 和 线程2 返回各自的 oldvalue即可.

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

本来 check and set 这样的操作在代码角度不是原子的 , 但是在硬件层面上可以让一条指令完成这个操作 , 也就变成原子的了.


2) 实现自旋锁(伪代码)

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

1.5 CAS 的 ABA 问题

ABA的问题:

假设存在两个线程 t1 和 t2 , 有一个共享变量 num , 初始值为 A.

接下来 , 线程 t1 想使用 CAS 把 num 值改成 Z , 那么就需要:

  • 先读取 num 的值 , 记录到 oldNum 变量中.
  • 使用 CAS 判定当前 num 的值是否为 A , 如果是 A , 就修改成 Z.

但是 , 在 t1 执行这两个操作之间 , t2 线程可能把 num 的值从 A 改成 B , 又从 B 改成了 A

线程 t1 的 CAS 期望 num 不变就修改 , 但是 num的值已经被 t2 给改了. 只不过又改成了 A , 此时 t1 是否要将 num 的值更新为 Z 呢?


1.6 ABA问题引发的 BUG

大部分情况下 t2 线程反复横跳对 t1 是否修改 num 是没有影响的 , 但不排除一些特殊情况.

假设小明有 100 存款 , 小明想从 ATM 机中取 50元. 不小心多按了几次 , 取款机创建了两个线程 , 并发的执行 -50 操作.

我们期望一个线程执行 -50 成功 , 另一个线程 -50 失败.

如果 CAS 的实现方式来完成这个扣款过程就会出现问题.

正常的过程:

  • 1. 存款100 , 线程1 获取到当前的存款值为100 , 期望更新为50; 线程2 获取到当前存款值为 100 , 期望更新为50.
  • 2. 线程1 扣款成功 , 存款改为50 , 线程2 阻塞等待中.
  • 3. 轮到线程2 执行 , 发现当前存款为 50 , 和之前读到的 100 不相同 , 执行失败.

异常的过程:

  • 1. 存款100 , 线程1 获取到当前的存款值为100 , 期望更新为50; 线程2 获取到当前存款值为 100 , 期望更新为50.
  • 2. 线程1 扣款成功 , 存款改为50 , 线程2 阻塞等待中.
  • 3. 在线程2 执行之前 , 小明的朋友正好给他转了50 , 账户余额变为100.
  • 4. 轮到线程2 执行了 , 发现当前存款为100 , 和之前读到的100相同 , 再次执行扣款操作.

此时扣款操作执行了两次 , 这就是 ABA 问题引发的 BUG.

解决方案:

给要修改的值 , 引入版本号. 在 CAS 比较当前值和旧值的同时 , 也要比较版本号是否符合预期.

​​​​​真正修改时:

  • 在当前值等于旧值的前提下:
  • 如果当前版本号和之前读到的版本号相同 , 则修改数据 , 并把版本号 + 1.
  • 如果当前版本号高于之前读到的版本号 , 就操作失败(认为数据已经被修改过了).

在 Java 标准库中提供了 AtomicStampedReference<E>类. 这个类可以对某个类进行包装 , 在内部就提供了上述描述的版本管理功能.


1.7 相关面试题

1. 讲解下自己理解的 CAS 机制.

CAS 全称 Compare and Swap , 相当于一个原子操作 , 同时完成"读取内存 比较数据是否相等 修改内存" 这三个步骤. 本质上是一条 CPU 指令.

2. ABA 问题怎么解决?

给要修改的数据引入一个版本号 , CAS 不仅要比较当前值和旧值是否相等 , 还要比较版本号是否符合预期. 在当前值和旧值相等的前提下 , 如果当前版本号和之前读到的版本号一致 , 就修改数据 , 并让版本号自增. 如果发现当前版本号比之前读的版本号大 , 操作失败.


2. Synchronized 基本原理

2.1 基本特点

结合上述所策略 , 我们可以总结出 Synchronized 具有以下特性(只考虑 jdk 1.8)

  • 1. 开始是乐观锁 , 如果锁冲突频繁 , 就转换为悲观锁.
  • 2. 开始是轻量级锁 , 如果锁持有时间较长 , 就转换为重量级锁.
  • 3. 实现轻量级锁的时候大概率使用自旋锁策略.
  • 4. 是一种不公平锁.
  • 5 . 是一种可重入锁.
  • 6. 不是读写锁.

2.2 加锁过程

JVM 将 synchronized 锁分为: 无锁 , 偏向锁 , 轻量级锁 , 重量级锁 状态. 会根据情况 , 进行依次升级.

1) 偏向锁

第一个加锁的线程 , 优先进入偏向锁状态.

偏向锁不是真的"加锁" , 只是给对象做一个"偏向锁的标记". 记录这个锁属于哪个线程.

如果后续没有其他线程来竞争该锁 , 那么就不用执行加锁操作(由此避免了加锁的开销)

如果后续有线程来竞争该锁 , 那就取消原来偏向锁的状态 , 进入一般的轻量级锁状态.(刚才已在锁对象中记录了当前锁属于哪个线程 , 很容易识别当前申请锁的线程是不是原来的线程)

Tips: 偏向锁本质上相当于 "延迟加锁" , 能不加锁就不加锁 , 尽量避免不必要的加锁开销.

但该做的标记还是得做 , 否则无法区分何时需要真正加锁.

举个例子: 假设小明有个女朋友叫小美 , 但由于没有其他女生对小明感兴趣 , 因此小美有恃无恐 , 一直拖着不和小明结婚. 直到有一天 , 出现一个对小明感兴趣的女生 , 小美慌了 , 立即和小明去领证.


2)轻量级锁

随着其他线程进入竞争 , 偏向锁状态被消除 , 进入轻量级锁状态(自适应的自旋锁)

此处的轻量级锁就是通过 CAS 来实现.

  • 通过 CAS 检查并更新一块内存(比如 null => 该线程引用)
  • 如果更新成功 , 则认为加锁成功
  • 如果更新失败则认为锁被占用 , 继续自旋式的等待(不放弃 CPU)

何为"自适应"?

自选操作会让 CPU 一直空转 , 比较浪费 CPU 资源.

因此此处的自旋不会一直进行 , 达到一定次数或时间后 , 就不在自旋了.也是"自适应"


3) 重量级锁

如果竞争进一步激烈 , 自选不能快速获取到锁状态 , 就会膨胀为重量级锁

此处的重量级锁就是指内核提供的 mutex.

  • 执行加锁操作 , 先进入内核态.
  • 在内核态判定当前锁是否被占用.
  • 如果该锁没有被占用 , 则加锁成功 , 并切换会用户态.
  • 如果该锁被占用了 , 则加锁失败 , 此时线程进入锁的等待队列(挂起) , 等待被操作系统唤醒.
  • 经过漫长的等待 , 该锁被其他线程释放 , 操作系统也想起了这个被挂起的线程 , 于是唤醒这个线程重新尝试获取锁.

2.3 其他的优化操作

锁消除

编译器 + JVM 判断锁是否可以消除 , 如果可以 , 就直接消除.

有些应用程序的代码块 , 在单线程的情况下也用到了synchronized(例如 StringBuffer)

StringBuffer str = new StringBuffer();
str.append("H");
str.append("e");
str.append("l");
str.append("l");
str.append("o");

此时每次调用 append 操作都会涉及到加锁/解锁 , 在单线程情况下是不必要的 , 白白浪费资源开销.


锁粗化

一段操作中如果多次进行加锁操作 , 编译器 + JVM 会自动进行锁的粗化.

锁的力度: 粗和细

实际开发过程中使用细粒度锁 , 是希望释放锁的时候其他线程能使用锁.

但如果实际上并没有那么多的线程抢占锁 , 这种情况下 JVM 就会把锁粗化 , 频繁的申请释放锁.


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

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

相关文章

【C++初阶】vector的使用

大家好我是沐曦希&#x1f495; 文章目录一.vector介绍二、构造函数三、遍历1.[]2.迭代器3.范围for四、容量操作1.扩容机制五、增删查改六、迭代器失效问题一.vector介绍 vector是表示可变大小数组的序列容器。就像数组一样&#xff0c;vector也采用的连续存储空间来存储元素。…

【Git】如何修改本地仓库的用户名和邮箱

最近我修改了我gitee和github的用户名还有邮箱&#xff0c;所以需要对本地仓库配置的用户名和邮箱进行更改 本文首发于 慕雪的寒舍 1.命令 刚开始我使用的是如下命令 git config --global user.email "邮箱" git config --global user.name "用户名"但是…

机器学习基础总结

一&#xff0c;机器学习系统分类 机器学习系统分为三个类别&#xff0c;如下图所示: 二&#xff0c;如何处理数据中的缺失值 可以分为以下 2 种情况&#xff1a; 缺失值较多&#xff1a;直接舍弃该列特征&#xff0c;否则可能会带来较大噪声&#xff0c;从而对结果造成不良影…

【云原生】promehtheus整合grafana实现可视化监控实战

文章目录前言一. 实验环境二. 安装grafana2.1 grafana的介绍2.2 为什么选择grafana&#xff1f;2.3 grafana下载及安装三. 网页端配置grafana3.1 浏览器访问grafana网页3.2 使用grafana 获取prometheus的数据源3.3 grafana导入prometheus模板总结前言 大家好&#xff0c;又见面…

新出海品牌必看!Colorkey如何构建海外第二增长曲线 ?

根据中商产业研究院数据&#xff0c;2022年1-6月中国美容化妆品及洗护用品出口量484138吨&#xff0c;同比增长8.6%&#xff0c;并且在2022年下半年依然保持强劲的增长。国货美妆品牌出海成为大趋势&#xff0c;各大品牌都纷纷开始出海&#xff0c;寻找新的增长点。Colorkey珂拉…

第二部分:并列句

想要表达一件事&#xff0c;一个简单句即可&#xff0c;一主一谓&#xff0c;n. v. 那&#xff0c;想要表达两件事&#xff0c;就写两个简单句呗&#xff0c;以此类推&#xff0c;想要描述几件事&#xff0c;就写几个简单句就行 英语是形合语言&#xff0c;形式上需要加上连接…

tomcat:设计模式用的好,下班就能早

tomcat作为一款经典的轻量级应用服务器&#xff0c;自然也使用了很多优雅的设计模式。 今天给大家简单介绍一下tomcat在初始化组件时使用的几种设计模式。 组合模式 在tomcat中&#xff0c;把不同的功能设计为了不同的组件&#xff0c;比如connector、engine、host、context等…

推荐五款实用的良心软件,无广告无弹窗

分享是一种神奇的东西,它使快乐增大,它使悲伤减小。 1.拼音输入法——手心输入法 如果你曾被输入法软件的弹屏骚扰&#xff0c;如果你仅需纯粹输入法不需要冗余功能&#xff0c;手心输入法将是你最好的选择&#xff0c;界面清爽简洁&#xff0c;无广告&#xff0c;精准的预测输…

CSI Tool 安装及配置记录

一、Ubuntu安装 1.下载Ubuntu 首先安装Ubuntu 14.04 LTS 64位下载地址&#xff08;页面中第一个链接&#xff09; 2.制作启动盘&#xff08;注意备份&#xff09; 可以使用官方的工具Rufus&#xff0c;下载地址&#xff1a;https://rufus.ie/ 打开Rufus&#xff0c;先备份…

wav转mp3,wav转换成mp3教程

很多使用音频文件的小伙伴&#xff0c;总会接触到不同类型的音频格式&#xff0c;根据需求不同需要做相关的处理。比如有人接触到了wav格式的音频&#xff0c;这是windows系统研发的一种标准数字音频文件&#xff0c;是一种占用磁盘体积超级大的音频格式&#xff0c;通常用于录…

超级好用的json格式化工具

理想的json格式化工具应该具备什么&#xff1f;你心中的json格式化工具是什么&#xff1f; Json.cn? No No No, 这个已经老掉牙了理想的json格式化工具应该支持搜索、定位、非法json容错&#xff0c;若实在无法格式化则应该给出具体的错误位置&#xff0c;并且可视区要大&…

【C++设计模式】学习笔记(3):策略模式 Strategy

目录 简介动机(Motivation)模式定义结构(Structure)要点总结笔记结语简介 Hello! 非常感谢您阅读海轰的文章,倘若文中有错误的地方,欢迎您指出~ ଘ(੭ˊᵕˋ)੭ 昵称:海轰 标签:程序猿|C++选手|学生 简介:因C语言结识编程,随后转入计算机专业,获得过国家奖学金…

数组的操作

1.splice 1.splice 是数组的一个方法&#xff0c;使用这个方法会改变原来的数组结构&#xff0c;splice&#xff08;index &#xff0c;howmany &#xff0c; itemX&#xff09;&#xff1b;这个方法接受三个参数&#xff0c;我们在使用的时候可根据自己的情况传递一个参数&…

ChatGPT原理简明笔记

学习笔记&#xff0c;以李宏毅的视频讲解为主&#xff0c;chatGPT的官方博客作为补充。 自己在上古时期接触过人工智能相关技术&#xff0c;现在作为一个乐子来玩&#xff0c;错漏之处在所难免。 若有错误&#xff0c;欢迎各位神仙批评指正。 chatGPT的训练分为四个阶段&#x…

大数据技术原理与应用

一、大数据概述 1.1大数据时代 三次信息化浪潮 1.2大数据的概念和影响 大数据的4v特征 volume大量化、velocity快速化、variety多样化、value价值化 数据量大数据类型繁多 – 大数据是由结构化和非结构化数据组成的处理速度快价值密度低&#xff0c;商业价值高 – 连续不间…

二十种题型带你复习《概率论与数理统计》得高分(高数叔)

题型一 事件及概率的运算 知识点 注意&#xff1a; 1 互斥与对立事件 2 事件的差 注意&#xff1a; 1 德摩根律注意&#xff1a; 1 加法公式 2 减法公式(事件的差)题目 注意&#xff1a; 1 填空题注意&#xff1a; 1 德摩根律 2 三个事件的和的公式 3 两个事件的积事件为…

数据库关系模型

关系模型简述 形象地说&#xff0c;一个关系就是一个table。 关系模型就是处理table的&#xff0c;它由三个部分组成&#xff1a; 描述DB各种数据的基本结构形式&#xff1b;描述table与table之间所可能发生的各种操作&#xff1b;描述这些操作所应遵循的约束条件&#xff1…

你是真的“C”——详解指针知识

你是真的“C”——详解指针知识&#x1f60e;前言&#x1f64c;1、 指针是什么&#xff1f;&#x1f64c;2、指针和指针类型&#x1f64c;2 、1指针-整数2 、 2指针的解引用3、 野指针&#x1f64c;3、 1野指针成因3、 2如何规避野指针4、指针运算&#x1f64c;4、1 指针-整数4…

Flutter WebView 性能优化,让 h5 像原生页面一样优秀

大家好&#xff0c;我是 17。 WebView 的文章分两篇 在 Flutter 中使用 webview_flutter 4.0 | js 交互Flutter WebView 性能优化&#xff0c;让 h5 像原生页面一样优秀 本篇和大家一起讨论下性能优化的问题。 WebView 页面的体验上之所以不如原生页面&#xff0c;主要是因…

c#数据结构-有序列表和有序字典

有序列表和有序字典 有序列表和有序字典都是是一个键值对容器&#xff0c;像字典一样。 从习惯和描述推测&#xff0c; 列表控制一个数组有序列表使用比有序字典更少的内存如果一次性添加一堆数据&#xff0c;且这堆数据有序。那么有序列表比有序字典更快 有序列表大概长这样 …