1. 是什么

要理解CAS,我们首先得了解乐观锁和悲观锁的概念。

1.1. 乐观锁与悲观锁

悲观锁:假设每次操作数据的时候总有人一起操作数据。因此我操作数据前先上锁,直到我操作完释放锁,别人都只能阻塞等待。

乐观锁:假设每次操作数据的时候没人跟我一起操作数据。因此我只在更新的时候检查一下有没有其他人修改了数据,有则重试直到成功。

1.2. CAS

CAS是乐观锁的一种。Java中的AQS、AtomicXXX都是基于CAS实现的。

CAS全称叫compare and set,即比较并设置某个变量的值,他是原子操作。

我们以CAS(A,B)为例,这里涉及了三个值,一个实际内存值A1,当前读取的值A(或者叫预期值A),及其修改值B。当且仅当A1== A时,把值修改为B

2. 如何使用

JUC包中Atomic类的实现都是通过CAS实现的

2.1. Atomic是什么

线程安全的原子类,底层使用CAS实现

2.2. Atomic使用

以AtomicInteger为例

public static void main(String[] args) throws InterruptedException
{
AtomicInteger val = new AtomicInteger(0);
Thread addThread = new Thread(()->{
for (int i = 0; i < 10000; i++)
{
val.addAndGet(1);
}
}); Thread decrThread = new Thread(()->{
for (int i = 0; i < 10000; i++)
{
val.decrementAndGet();
}
}); addThread.start();
decrThread.start(); addThread.join();
decrThread.join(); System.out.println(val.get());//0
}

2.3. Atomic原理分析

2.3.1. 构造方法

//使用的是Unsafe.compareAndSwapInt 方法
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset; //类加载的时候执行
static {
try {
//valueOffset保存的是AtomicInteger value属性在内存中的地址
//后面调用Unsafe的CAS方法会用到这个值
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
} //volatile:某线程更新后,其他线程立马看到修改后的值
private volatile int value; public AtomicInteger(int initialValue) {
value = initialValue;
}

可以看到主要有三个属性:Unsafe unsafe long valueOffsetvolatile int value

  • 关于Unsafe类的解释参考Unsafe.md,有了这个基础后源码分析就简单多了。
  • valueOffset是value变量在内存中的地址
  • value使用volatile修饰,这样就能保证可见性和有序性

2.3.2. addAndGet方法

  • AtomicInteger.addAndGet
public final int addAndGet(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}

调用Unsafe类的getAndAddInt方法对value增加delta

由于Unsafe的方法返回value原值,所以需要加上delta才是增加后的值

  • Unsafe.getAndAddInt
//传入Unsafe.getAndAddInt的参数为(AtomicInteger实例,AtomicInteger value属性的内存地址,增加的值)
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
//死循环+cas
do {
//获取对象o偏移offset地址的值,即value的值
v = getIntVolatile(o, offset);
//判断对象o在偏移offset地址的值 == v(刚刚获取的值)么?是的话把值+delta写入
} while (!compareAndSwapInt(o, offset, v, v + delta));
//返回原来的value
return v;
} //以下两个都native方法,调用C/C++的方法
public final native boolean compareAndSwapInt(Object o, long offset,
int expected,
int x); public native int getIntVolatile(Object o, long offset);

说明都在代码的注释上,不多说了

2.3.3. getAndIncrement

  • AtomicInteger.getAndIncrement
public final int getAndIncrement() {
//同AtomicInteger.addAndGet方法,调用Unsafe类的getAndAddInt方法对value增加delta,返回value原值
return unsafe.getAndAddInt(this, valueOffset, 1);
}

2.3.4. decrementAndGet

  • AtomicInteger.decrementAndGet
public final int decrementAndGet() {
//同AtomicInteger.addAndGet方法,调用Unsafe类的getAndAddInt方法对value增加delta,返回value原值
//只不过传入的delta是个负数,也就是相当于减去了一个数
return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
}

2.4. AtomicInteger的问题

2.4.1. CPU占用过高

多线程并发修改如果竞争特别激烈,那么cpu消耗过大,毕竟是死循环+CAS原子操作修改

2.4.2. ABA问题

假设有两个线程都要修改a的值,ThreadA和ThreadB,操作步骤如下

  • ThreadA:
第1步 get a为1
第2步 失去cpu
第7步 cas(a, 1, 2)
  • ThreadB:
第3步 get a为1
第4步 cas(a, 1, 3)
第5步 cas(a, 3, 1)
第6步 失去cpu

从上述顺序看出a的值被线程B从1改为3又改为1, 而线程A以为a的值没有变化,仍然是1,进而把它改为2

2.4.2.1. 解决方案:版本号

我们可以给数据加上版本号来解决ABA问题,即更新的时候不仅比较内存值是否相等,还要比较数据的版本是否相等,只有内存值和版本号相等的情况下才进行更新。

用上面的例子进行说明:

  • ThreadA:
第1步 get a为(1, 1) //即数据为1,版本为1
第2步 失去cpu
第7步 cas(a, 1, 2, 1, 2)//即预期数据为1,要改为2;预期版本号为1,要改为2。这一步执行失败因为此时版本已经为3了,不为1
  • ThreadB:
第3步 get a为(1, 1) //即数据为1,版本为1
第4步 cas(a, 1, 3, 1, 2)//即预期数据为1,要改为3;预期版本号为1,要改为2
第5步 cas(a, 3, 1, 2, 3) //即预期数据为3,要改为1;预期版本号为2,要改为3
第6步 失去cpu

Java中已经有一个类实现了版本号:AtomicStampedReference,使用如下:

public static void main(String[] args)
{ //初始化版本号为0,值为0
AtomicStampedReference<Integer> val = new AtomicStampedReference<>(0,0);
//在版本号为0,值为0的基础上cas
val.compareAndSet(0, 1, 0, 1)
}
2.4.2.1.1. 原理分析
  • AtomicStampedReference构造方法
//Pair属性用volatile修饰
private volatile Pair<V> pair; public AtomicStampedReference(V initialRef, int initialStamp) {
//使用初始值和初始版本号构造pair
pair = Pair.of(initialRef, initialStamp);
} //Pair类
private static class Pair<T> {
//用final修饰,一旦初始化就不能改变,保证了线程安全
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
} //类加载的时候初始化Unsafe类和AtomicStampedReference的pair属性的内存地址
private static final sun.misc.Unsafe UNSAFE = sun.misc.Unsafe.getUnsafe();
private static final long pairOffset =
objectFieldOffset(UNSAFE, "pair", AtomicStampedReference.class);
  • AtomicStampedReference.compareAndSet
当前:0,0,修改成:1,1 Param:(0,1,0,1):(期望值,新的值,期望的版本号,新的版本号)
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
//期望值==实际内存值
expectedReference == current.reference &&
//期望版本号==实际内存版本号
expectedStamp == current.stamp && //第1种情况:值和版本号都没有改变,那么不需要做什么
//1.1 新值==实际内存值
((newReference == current.reference &&
//1.2 新版本号==实际版本号
newStamp == current.stamp) || ||第2中情况:值或版本号有改变,那么cas设置当前pair为新的pair
casPair(current, Pair.of(newReference, newStamp)));
} private boolean casPair(Pair<V> cmp, Pair<V> val) {
//调用Unsafe类的方法。把pair属性从cmp修改为val
return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}

3. 参考

Java源码分析系列笔记-4.CAS的更多相关文章

  1. Java源码分析系列之HttpServletRequest源码分析

    从源码当中 我们可以 得知,HttpServletRequest其实 实际上 并 不是一个类,它只是一个标准,一个 接口而已,它的 父类是ServletRequest. 认证方式 public int ...

  2. Java源码分析系列

    1) 深入Java集合学习系列:HashMap的实现原理 2) 深入Java集合学习系列:LinkedHashMap的实现原理 3) 深入Java集合学习系列:HashSet的实现原理 4) 深入Ja ...

  3. MyCat源码分析系列之——结果合并

    更多MyCat源码分析,请戳MyCat源码分析系列 结果合并 在SQL下发流程和前后端验证流程中介绍过,通过用户验证的后端连接绑定的NIOHandler是MySQLConnectionHandler实 ...

  4. MyCat源码分析系列之——BufferPool与缓存机制

    更多MyCat源码分析,请戳MyCat源码分析系列 BufferPool MyCat的缓冲区采用的是java.nio.ByteBuffer,由BufferPool类统一管理,相关的设置在SystemC ...

  5. [Tomcat 源码分析系列] (二) : Tomcat 启动脚本-catalina.bat

    概述 Tomcat 的三个最重要的启动脚本: startup.bat catalina.bat setclasspath.bat 上一篇咱们分析了 startup.bat 脚本 这一篇咱们来分析 ca ...

  6. MyBatis 源码分析系列文章导读

    1.本文速览 本篇文章是我为接下来的 MyBatis 源码分析系列文章写的一个导读文章.本篇文章从 MyBatis 是什么(what),为什么要使用(why),以及如何使用(how)等三个角度进行了说 ...

  7. spring源码分析系列 (8) FactoryBean工厂类机制

    更多文章点击--spring源码分析系列 1.FactoryBean设计目的以及使用 2.FactoryBean工厂类机制运行机制分析 1.FactoryBean设计目的以及使用 FactoryBea ...

  8. spring源码分析系列 (5) spring BeanFactoryPostProcessor拓展类PropertyPlaceholderConfigurer、PropertySourcesPlaceholderConfigurer解析

    更多文章点击--spring源码分析系列 主要分析内容: 1.拓展类简述: 拓展类使用demo和自定义替换符号 2.继承图UML解析和源码分析 (源码基于spring 5.1.3.RELEASE分析) ...

  9. spring源码分析系列 (1) spring拓展接口BeanFactoryPostProcessor、BeanDefinitionRegistryPostProcessor

    更多文章点击--spring源码分析系列 主要分析内容: 一.BeanFactoryPostProcessor.BeanDefinitionRegistryPostProcessor简述与demo示例 ...

  10. spring源码分析系列 (3) spring拓展接口InstantiationAwareBeanPostProcessor

    更多文章点击--spring源码分析系列 主要分析内容: 一.InstantiationAwareBeanPostProcessor简述与demo示例 二.InstantiationAwareBean ...

随机推荐

  1. 远程连接到轻量应用服务器PG数据库

    不建议这样做,但是开发时方便需要.进入正题. PG是不支持远程连接的,需要连接直接该参数. 在其data目录里,有二个配置文件: pg_hba.conf:配置数据库的访问权限 postgresql.c ...

  2. creative打靶学习笔记(4)

    参考视频[Tryhackme系列网安课程-Creative-难度3-哔哩哔哩] https://b23.tv/6qzkzyh nmap扫描![](https://cdn.nlark.com/yuque ...

  3. 第一次3D打印,一个简单的小方块(rhino)

    一.建模 打开犀牛,我们选择立方体 我们点击上册的中心点 输入0,然后回车0 而后我们输长度:10,回车确认 同样的,宽度10 高度同样是10 回车确认后,我们得到一个正方形 二.导出模型 我们选择文 ...

  4. seata-server 1.3.0整合nacos,使用nacos做注册和配置中心

    前言 关于seata版本的选择和更详细的安装,可以参考 SpringCloud Alibaba之Seata入门及踩坑 本篇博客是整合nacos,nacos直接下载安装解压运行就可以了. seata的下 ...

  5. BeanCreationException: Error creating bean with name 'dataSource' defined in class path resource

    在练习中遇到的,我的情况是: 打算在common中建立student实体类,想到可能其他模块也会用到这个类,但是一些注解比如,@TableId等等需要用到mybatis-plus的依赖,所以我就把依赖 ...

  6. Unity性能优化-降低功耗,发热量,耗电量之OnDemandRendering篇

    公司游戏项目,手机运行严重发烫,耗电量飞快.在暂时无法做其他美术性和技术性优化的情况下,我写了这个公司内部文档,并做了个实验,今天干脆公布出来,希望对大家有用. --官方文档: Unity - Scr ...

  7. hashtable底层

    一.单线程环境下 底层:hash表结构 (数组 + 链表) 使用无参构造创建对象时 会默认长度11的数组 加载因子0.75 Hashtable<Object, Object> hashta ...

  8. (转)python批量提取PDF第一页输出为图片

    一:步骤 1.使用input输入路径 2.生成图片存户路径同存放路径 3.生成图片为PNG格式 4.支持自定义截取页数,建议为第一页 二:安装扩展类 pip install PyMuPDF 三:示例代 ...

  9. 尝试通过DeepSeek来优化提高WordPress网站的打开速度,还真有效!

    头图由AI生成 嗯,用户问的是WordPress网站如何优化打开速度.首先,我需要考虑用户可能的背景.他们可能是一个WordPress网站的管理员或者拥有者,发现网站加载速度不够快,想要进行优化.用户 ...

  10. grafana最新任意文件读取

    一.Grafana简介 Grafana是一个跨平台的开源的度量分析和可视化工具,可以通过将采集的数据查询然后可视化的展示,并及时通知.它主要有以下六大特点: 1.展示方式:快速灵活的客户端图表,面板插 ...