CAS机制是什么?
CAS 就是 Compare And Swap,意为 比较并交换值。
CAS机制给线程安全提供了新的保障思路,之前通过加锁的方式’将指令打包成一个整体’(指的某sync锁),来实现线程安全。CAS出现之后可以不进行加锁也能保证线程的安全。
从代码角度来看:当多个线程对一个资源进行CAS(比较并交换值)操作时,CAS机制保证了只能有一个线程操作成功,并且不会阻塞其余的线程,其余线程只会收到操作失败的提示。具体的操作步骤下面会有说明。
CAS 是乐观锁的一种具体的实现。
CAS如何保证线程安全?
Java中的 CAS 机制是基于 JNI 调用 C 给 CPU 上锁实现线程安全的,具体过程可以参考数据库。
JNI:Java Navicat Interface,JNI可以理解为是一种协议或者规范。
JNI 相当于 Java 和 C 之间的桥梁,通过 JNI 协议可以让 Java调用 C,可以通过协议让 C 调用 Java。
Java 语言就是通过 JNI 实现了跨语言调用。例如:Java调用C脚本(Navicat修饰的方法)。
CAS的原理:
123 :必须前两个值相等才可以修改
112 可以修改
123 不可以修改
132 不可以修改
第一位:内存中的值(主存)
第二位:旧值
第三位:新值
由于Java语言是跨平台语言,所以不同的平台有不同的CAS实现原理:
- java的CAS使用了 Unsafe 这类进行 cas操作。
- Unsafe类 通过 jvm对不同操作系统实现的 Atomic::cmpxchg 指令实现。
- Atomic::cmpxchg指令是由 汇编语言 实现的,并且使用了CPU硬件的lock机制保证了 指令操作的 原子性。
所以 CAS机制可以保证线程安全是因为有了硬件的支持,才能在软件做到不加锁 的线程安全。
Unsafe类通过静态方法获取
private static final Unsafe unsafe = Unsafe.getUnsafe();
原子类(AtomicInteger等)
java.util.concurrent.atomic.AtomicInteger。
atomic包下还有很多类型的原子类,java.util.concurrent 就是常说的JUC编程。
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// 声明的属性
private static final Unsafe unsafe = Unsafe.getUnsafe();
private volatile int value;
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//其中的几个方法
public final int get() {return value;}
public final void set(int newValue) {value = newValue; }
//将当前value自增1
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
}
参数理解:
- unsafe:调用CAS操作的工具类。
- value:对于AtomicInteger类来说 保存的就是 Integer类型的值。
- valueOffset:value值对应的内存中编译的地址,这样也是为了能够能快的在内存中找到value值。
CAS操作的过程
使用CAS机制同时启动两个线程并且对一个资源进行自增操作。
private static AtomicInteger atomicInteger = new AtomicInteger(0);
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
atomicInteger.getAndIncrement();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
atomicInteger.getAndIncrement();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(atomicInteger.get());
}
==================================结果==================================
D:\JDK\jdk-8u291\bin\java.exe "-javaagent:D:\idea\IntelliJ IDEA 2021.2\lib\idea_rt.jar=59466:D:\idea\IntelliJ IDEA 2021.2\bin" -Dfile.encoding=UTF-8 -classpath D:\JDK\jdk-8u291\jre\lib\charsets.jar;D:\JDK\jdk-8u291\jre\lib\deploy.jar;D:\JDK\jdk-8u291\jre\lib\ext\access-bridge-64.jar;D:\JDK\jdk-8u291\jre\lib\ext\cldrdata.jar;D:\JDK\jdk-8u291\jre\lib\ext\dnsns.jar;D:\JDK\jdk-8u291\jre\lib\ext\jaccess.jar;D:\JDK\jdk-8u291\jre\lib\ext\jfxrt.jar;D:\JDK\jdk-8u291\jre\lib\ext\localedata.jar;D:\JDK\jdk-8u291\jre\lib\ext\nashorn.jar;D:\JDK\jdk-8u291\jre\lib\ext\sunec.jar;D:\JDK\jdk-8u291\jre\lib\ext\sunjce_provider.jar;D:\JDK\jdk-8u291\jre\lib\ext\sunmscapi.jar;D:\JDK\jdk-8u291\jre\lib\ext\sunpkcs11.jar;D:\JDK\jdk-8u291\jre\lib\ext\zipfs.jar;D:\JDK\jdk-8u291\jre\lib\javaws.jar;D:\JDK\jdk-8u291\jre\lib\jce.jar;D:\JDK\jdk-8u291\jre\lib\jfr.jar;D:\JDK\jdk-8u291\jre\lib\jfxswt.jar;D:\JDK\jdk-8u291\jre\lib\jsse.jar;D:\JDK\jdk-8u291\jre\lib\management-agent.jar;D:\JDK\jdk-8u291\jre\lib\plugin.jar;D:\JDK\jdk-8u291\jre\lib\resources.jar;D:\JDK\jdk-8u291\jre\lib\rt.jar;E:\idea-peoject\thread-demo\out\production\thread-demo com.lhj.juc.juc_atomicIntegerDemo_01
100000
Process finished with exit code 0
CAS的执行步骤
前提对虚拟机、内存结构有一定了解。
题外话:
每次创建一个线程就会在虚拟机中创建一个虚拟机栈,线程的生命周期跟虚拟机栈的生命周期相同。
对象都存放在主内存(主存)也就是堆中,线程操作主存中的变量时,会将主存中变量值的副本复制一份,放到线程本地。线程本地的副本指向堆中的变量(栈指向堆)。
- 1)有线程1和线程2将 value 的值读取到 线程本地的 oldValud 中(oldValue 代表一个局部变量, 在栈上。每个线程有自己的栈)
- 2)线程2先执行操作 ,线程2判断 oldValue 等于 value 等于 0,直接对 value 赋值。操作成功后oldValue 同步 value 的值。
- CAS操作是直接操作主内存(主存)的,而不是操作寄存器(线程本地的变量副本)
- CAS的 读、比较、写 的操作 是一条硬件指令,是原子的。
注:感兴趣的话可以去看看 CPU的 “总线锁定”
直接对 value 赋值。
操作成功后 线程2 oldValue 同步 value 的值。
3)此时线程1 执行 CAS操作,oldValue 和 value 不相等, 不能进行赋值。
线程1会进入循环,在循环里重新读取 value 的值赋给 oldValue
线程1进行第二次执行 CAS, 此时 oldValue 和 value 相同, 于是直接执行赋值操作
通过形如上述代码就可以实现一个原子类。不需要使用重量级锁,就可以高效的完成多线程的自增操作。
CAS的ABA问题
ABA问题是指:
线程A和B同时共享一个资源变量
线程B想要修改value值,需要经过两个过程:
- 先读取 value 的值, 记录到 oldNum 变量中.
- 使用 CAS 判定当前 value 的值是否为 A, 如果为 A, 就修改成 其他值
但是在线程B执行这两个操作之间,线程A可以将value 进行修改,并且最终改回原来的值。
此时线程B无法判定 value的是 是初始值,还是经过其他线程运算后的值。
大部分情况下 线程改变了值,最终改回去 不会出现问题。但是不排除一些特殊情况:
正常的过程
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 问题搞的鬼
解决ABA问题:
给value 增加一个属性 version ,每次有线程成功执行CAS操作之后 就给version自增1。通过版本号线程就可以判断value是否被修改过。