[多线程进阶]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)被代理的对象可以是远程对象、创建…...
每天10个前端小知识 【Day 2】
👩 个人主页:不爱吃糖的程序媛 🙋♂️ 作者简介:前端领域新星创作者、CSDN内容合伙人,专注于前端各领域技术,成长的路上共同学习共同进步,一起加油呀! ✨系列专栏:前端…...
帮助中心在线制作工具推荐这4款,很不错哟!
根据用户咨询问题是否解决的情景,分为三个部分,首先帮助中心恰好有用户需要咨询的问题,用户可以通过点击相关问题即可解决自己的问题,其次,用户第一眼没有在帮助中心解决问题,有个搜索框,用户的…...
rabbitMQ相关文章汇总
RabbitMQ五种工作模式: https://blog.csdn.net/weixin_41882200/article/details/117128590?ops_request_misc%257B%2522request%255Fid%2522%253A%2522167625223516800182771874%2522%252C%2522scm%2522%253A%252220140713.130102334…%2522%257D&request_id1…...
【C++】异常
🌈欢迎来到C专栏~~异常 (꒪ꇴ꒪(꒪ꇴ꒪ )🐣,我是Scort目前状态:大三非科班啃C中🌍博客主页:张小姐的猫~江湖背景快上车🚘,握好方向盘跟我有一起打天下嘞!送给自己的一句鸡汤…...
@Validated注解不生效问题汇总
Validated注解不生效问题汇总 文章目录Validated注解不生效问题汇总背景:一:可能原因原因1:原因2:原因3:原因4:二:补充全局异常对validation的处理背景: 项目框架应用的是validatio…...
华科万维C++章节练习2_4
题目:编写程序,从键盘输入一个字符,然后在屏幕上输出该字符开头的连续3个字符以及对应ASCII码。 输出格式请参看: 请输入一个字符>>A 字符 ASCII码 A 65 B 66 C 67 请按任意键继续. . . 请直接…...
17万字数字化医院信息化建设大数据平台建设方案WORD
【版权声明】本资料来源网络,知识分享,仅供个人学习,请勿商用。【侵删致歉】如有侵权请联系小编,将在收到信息后第一时间删除!完整资料领取见文末,部分资料内容: 目录 第1章 医院信息化概述 1.…...
Android 11系统签名修改
Android OS 映像在两个地方使用加密签名:映像中的所有 .apk 文件都必须经过签名。Android 软件包管理器通过下列两种方式使用 .apk 签名:更换应用时,必须使用与旧应用相同的密钥对其签名,才能存取旧应用的数据。无论是通过覆盖 .a…...
亚马逊、沃尔玛卖家自养号退款经验和测评技术
今天给大家介绍下在做亚马逊、沃尔玛退款自养号中的经验,众所周知,自养号最重要的是养号的环境,包括系统的纯净度,下单的信用卡以及其他的一些细节。 环境系统市面上有很多,鱼龙混杂,比如什么lumi…...
Spring Security in Action 第十一章 SpringSecurity前后端分离实战
本专栏将从基础开始,循序渐进,以实战为线索,逐步深入SpringSecurity相关知识相关知识,打造完整的SpringSecurity学习步骤,提升工程化编码能力和思维能力,写出高质量代码。希望大家都能够从中有所收获&#…...
wordpress建设下载网站/seo关键词排名优化联系方式
原文:http://my.oschina.net/leejun2005/blog/343353 目录[-] 1、问题:mysql 遇到某些中文插入异常2、原因:此 utf8 非彼 utf83、解决方案3.1 升级 mysql 版本,并将utf8字符集升级到utf8mb43.1.1 直接修改表结构3.1.2 修改数据库默…...
阿里百秀wordpress/港港网app下载最新版
转载于:https://www.cnblogs.com/supper-Ho/p/6264023.html...
网站无域名注册人id/加盟培训机构
1、次数分配数列是()。(2 分) A.按数量标志分组形成的数列 B.按品质标志分组形成的数列 C.按统计指标分组所形成的数列 D.按数量标志和品质标志分组所形成的数列 2、下列指标中属于时…...
常州模板网站建设咨询/成都最新热门事件
//js 库代码: //ZAJ.js库代码 (function (){ //注册命名空间 AZJ 到window对象上 window[AZJ] {} //getElementsByClassName包含两个参数:类名,标签名 function getEleme…...
上海电子商务网站开发/周口seo
##题目描述 输入一棵二叉树,判断该二叉树是否是平衡二叉树。 ##思路 如果每个节点的左右子树的深度相差都不超过1,即最大深度差为1,则可判定为平衡二叉树。 两种思路: 第一种是递归判断每一个根节点,都要求出它的左右子…...
做网站的抬头标语怎么/网络营销软件
偶然间在微信公众号奇舞周刊上看到了这篇文章《CSS Painting API》,算是对 conic-gradient的初次见面。 后来有空的时候,百度搜了一下,看了这篇文章《CSS神奇的conic-gradient圆锥渐变》后,对conic-gradient有了深刻的了解。 概述…...