[多线程进阶]CAS与Synchronized基本原理
专栏简介: 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 就会把锁粗化 , 频繁的申请释放锁.
相关文章:

[多线程进阶]CAS与Synchronized基本原理
专栏简介: JavaEE从入门到进阶 题目来源: leetcode,牛客,剑指offer. 创作目标: 记录学习JavaEE学习历程 希望在提升自己的同时,帮助他人,,与大家一起共同进步,互相成长. 学历代表过去,能力代表现在,学习能力代表未来! 目录: 1.CAS 1.1 什么是CAS? 1.2 CAS伪代码 1.3 CAS …...

【Linux系统编程】02:文件操作
文件IO 系统调用(不带缓冲的IO操作)库函数(默认带用户缓冲的IO操作) 一、非缓冲IO 系统调用:即为不带缓冲的IO 1.打开文件open 2.读取文件read NAMEread - read from a file descriptorSYNOPSIS#include <unist…...

华为OD机试 - 去除多余空格(Python)| 真题+思路+代码
去除多余空格 题目 去除文本多余空格,但不去除配对单引号之间的多余空格。给出关键词的起始和结束下标,去除多余空格后刷新关键词的起始和结束下标。 条件约束: 不考虑关键词起始和结束位置为空格的场景;单词的的开始和结束下标保证涵盖一个完整的单词,即一个坐标对开…...

百趣代谢组学分享,补充α-酮酸的低蛋白饮食对肾脏具有保护作用
文章标题:Reno-Protective Effect of Low Protein Diet Supplemented With α-Ketoacid Through Gut Microbiota and Fecal Metabolism in 5/6 Nephrectomized Mice 发表期刊:Frontiers in Nutrition 影响因子:6.59 作者单位:…...

json对象和formData相互转换
前言 大家都知道,前端在和后台进行交互联调时,肯定避免不了要传递参数,一般情况下,params 在 get 请求中使用,而 post 请求下,我们有两种常见的传参方式: JSON 对象格式和 formData 格式&#x…...
【c++面试问答】常量指针和指针常量的区别
问题 常量指针和指针常量有什么区别? const的优点 在C中,关键字const用来只读一个变量或对象,它有以下几个优点: 便于类型检查,如函数的函数 func(const int a) 中a的值不允许变,这样便于保护实参。功能…...
Ubuntu18下编译android的ffmpeg经验
虽然按照网上的一些资料(如:最简单的基于FFmpeg的移动端例子:Android HelloWorld_雷霄骅的博客-CSDN博客_android ffmpeg 例子,,编译FFmpeg4.1.3并移植到Android app中使用(最详细的FFmpeg-Android编译教程…...

Spring Security in Action 第十三章 实现OAuth2的认证端
本专栏将从基础开始,循序渐进,以实战为线索,逐步深入SpringSecurity相关知识相关知识,打造完整的SpringSecurity学习步骤,提升工程化编码能力和思维能力,写出高质量代码。希望大家都能够从中有所收获&#…...
本文章提供中国国界、国界十段线原始数据以及加载方法
本文章提供中国国界九段线原始数据和加载方法 1、中国国界 完整数据 包括十段线 中国国界线(完整版 包括十段线) 2、原始数据 中国国界十段线topojson格式数据.rar 中国国界线topjson数据 中国国界十段线svg格式数据.rar 中国国界线svg数据 中国国界十段线shp格式数据…...

一文带你搞懂,Python语言运算符
Python语言支持很多种运算符,我们先用一个表格为大家列出这些运算符,然后选择一些马上就会用到的运算符为大家进行讲解。 说明:上面这个表格实际上是按照运算符的优先级从上到下列出了各种运算符。所谓优先级就是在一个运算的表达式中&#x…...

JAVA集合专题4 —— Map
目录Map接口实现类的特点Map接口的常见方法Map六大遍历方式Map练习1code编程练习2code编程练习3思路codeMap接口实现类的特点 Map与Collection并列存在,是Map集合体系的顶级接口Map的有些子实现存储数据是有序的(LinkedHashMap),有些子实现存储数据是无…...

二叉树进阶--二叉搜索树
目录 1.二叉搜索树 1.1 二叉搜索树概念 1.2 二叉搜索树操作 1.3 二叉搜索树的实现 1.4 二叉搜索树的应用 1.5 二叉搜索树的性能分析 2.二叉树进阶经典题: 1.二叉搜索树 1.1 二叉搜索树概念 二叉搜索树又称二叉排序树,它或者是一棵空树,…...

牛客网Python篇数据分析习题(三)
1.现有一个Nowcoder.csv文件,它记录了牛客网的部分用户数据,包含如下字段(字段与字段之间以逗号间隔): Nowcoder_ID:用户ID Level:等级 Achievement_value:成就值 Num_of_exercise&a…...
Java开发常见关键词集绵
一、关键词1: (1)RPC:远程过程调用(Remote Procedure Call)的缩写形式。远程调用的时候让人们觉得是本地调用。 (2)HTTP:超文本传输协议(Hyper Text Transfer…...

解决idea出现的java.lang.OutOfMemoryError: Java heap space的问题
文章目录1. 复现问题2. 分析问题3. 解决问题4. 补充解决java.lang.OutOfMemoryError: PermGen space问题1. 复现问题 今天使用idea开发时,突然报出如下错误: Exception in thread "main" java.lang.OutOfMemoryError: Java heap spaceat org.…...

为什么子进程要继承处理器亲缘性?
请先考虑一个典型的程序为什么需要启动一个子进程。(当然资源管理器不算一个典型的程序) 这是因为手头的任务被分解为子任务,无论出于何种原因,这些子任务都被放入子流程中。例如,在实现多次遍历型编译器/链接器时,其中每次遍历都…...

【算法】高精度
作者:指针不指南吗 专栏:算法篇 🐾不能只会思路,必须落实到代码上🐾 文章目录前言一、高精度加法二、高精度减法三、高精度乘法四、高精度除法前言 高精度即很大很大的数,超过了 long long 的范围&…...

计算机网络-基本概念
目录 计算机网络-基本概念 互联网 Java的跨平台原理 编辑 C\C的跨平台原理 解释性语言的跨平台原理(python,js等) 客户端 vs 服务器 什么是协议? 网络互连模型 请求过程 计算机之间的通信基础 计算机之间的连接方式-网线直连(需要用交叉线,而…...

你评论,我赠书~【哈士奇赠书 - 13期】-〖Python程序设计-编程基础、Web开发及数据分析〗参与评论,即可有机获得
大家好,我是 哈士奇 ,一位工作了十年的"技术混子", 致力于为开发者赋能的UP主, 目前正在运营着 TFS_CLUB社区。 💬 人生格言:优于别人,并不高贵,真正的高贵应该是优于过去的自己。💬 ὎…...

【设计模式】我终于读懂了代理模式。。。
👦代理模式的基本介绍 1)代理模式:为一个对象提供一个替身,以控制对这个对象的访问。即通过代理对象访问目标对象,这样做的好处是:可以在目标对象实现的基础上,增强额外的功能操作,即扩展目标对象的功能。 2)被代理的对象可以是远程对象、创建…...

Swift 协议扩展精进之路:解决 CoreData 托管实体子类的类型不匹配问题(下)
概述 在 Swift 开发语言中,各位秃头小码农们可以充分利用语法本身所带来的便利去劈荆斩棘。我们还可以恣意利用泛型、协议关联类型和协议扩展来进一步简化和优化我们复杂的代码需求。 不过,在涉及到多个子类派生于基类进行多态模拟的场景下,…...
Java多线程实现之Callable接口深度解析
Java多线程实现之Callable接口深度解析 一、Callable接口概述1.1 接口定义1.2 与Runnable接口的对比1.3 Future接口与FutureTask类 二、Callable接口的基本使用方法2.1 传统方式实现Callable接口2.2 使用Lambda表达式简化Callable实现2.3 使用FutureTask类执行Callable任务 三、…...

DIY|Mac 搭建 ESP-IDF 开发环境及编译小智 AI
前一阵子在百度 AI 开发者大会上,看到基于小智 AI DIY 玩具的演示,感觉有点意思,想着自己也来试试。 如果只是想烧录现成的固件,乐鑫官方除了提供了 Windows 版本的 Flash 下载工具 之外,还提供了基于网页版的 ESP LA…...
A2A JS SDK 完整教程:快速入门指南
目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库ÿ…...

Razor编程中@Html的方法使用大全
文章目录 1. 基础HTML辅助方法1.1 Html.ActionLink()1.2 Html.RouteLink()1.3 Html.Display() / Html.DisplayFor()1.4 Html.Editor() / Html.EditorFor()1.5 Html.Label() / Html.LabelFor()1.6 Html.TextBox() / Html.TextBoxFor() 2. 表单相关辅助方法2.1 Html.BeginForm() …...

android13 app的触摸问题定位分析流程
一、知识点 一般来说,触摸问题都是app层面出问题,我们可以在ViewRootImpl.java添加log的方式定位;如果是touchableRegion的计算问题,就会相对比较麻烦了,需要通过adb shell dumpsys input > input.log指令,且通过打印堆栈的方式,逐步定位问题,并找到修改方案。 问题…...

Chrome 浏览器前端与客户端双向通信实战
Chrome 前端(即页面 JS / Web UI)与客户端(C 后端)的交互机制,是 Chromium 架构中非常核心的一环。下面我将按常见场景,从通道、流程、技术栈几个角度做一套完整的分析,特别适合你这种在分析和改…...
vue3 daterange正则踩坑
<el-form-item label"空置时间" prop"vacantTime"> <el-date-picker v-model"form.vacantTime" type"daterange" start-placeholder"开始日期" end-placeholder"结束日期" clearable :editable"fal…...

从零开始了解数据采集(二十八)——制造业数字孪生
近年来,我国的工业领域正经历一场前所未有的数字化变革,从“双碳目标”到工业互联网平台的推广,国家政策和市场需求共同推动了制造业的升级。在这场变革中,数字孪生技术成为备受关注的关键工具,它不仅让企业“看见”设…...

【1】跨越技术栈鸿沟:字节跳动开源TRAE AI编程IDE的实战体验
2024年初,人工智能编程工具领域发生了一次静默的变革。当字节跳动宣布退出其TRAE项目(一款融合大型语言模型能力的云端AI编程IDE)时,技术社区曾短暂叹息。然而这一退场并非终点——通过开源社区的接力,TRAE在WayToAGI等…...