前言
思考下面一段代码:会有什么问题?如何解决?
public class Test {
private volatile static int sum = 0;
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
sum++;
}
});
thread.start();
}
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(sum);
}
}
1、什么是 CAS
CAS(Compare And Swap,比较并交换),通常指的是这样一种原子操作:针对一个变量,首
先比较它的内存值与某个期望值是否相同,如果相同,就给它赋一个新值。
CAS 的逻辑用伪代码描述如下:
if (value == expectedValue) {
value = newValue;
}
以上伪代码描述了一个由比较和赋值两阶段组成的复合操作,
CAS 可以看作是它们合并后的整体:一个不可分割的原子操作,并且其原子性是直接在硬件层面得到保障的。
CAS可以看做是乐观锁(对比数据库的悲观、乐观锁)的一种实现方式,Java原子类中的递增操
作就通过CAS自旋实现的。
CAS是一种无锁算法,在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。
2、CAS应用
在 Java 中,CAS 操作是由 Unsafe 类提供支持的,该类定义了三种针对不同类型变量的 CAS 操
作,如图
它们都是 native 方法,由 Java 虚拟机提供具体实现,这意味着不同的 Java 虚拟机对它们的实现
可能会略有不同。 底层C++源码有兴趣的同学可以后续在深入研究下实现方式。
3、CAS缺陷
CAS 虽然高效地解决了原子操作,但是还是存在一些缺陷的,主要表现在三个方面:
-
自旋 CAS 长时间地不成功,则会给 CPU 带来非常大的开销
-
只能保证一个共享变量原子操作
-
ABA问题
ABA 问题背景
AtomicInteger 存在的一个问题, 也是大部分 Atomic 相关类存在的, 就是 ABA 问题
简单来说, 就是线程1获取到 AtomicInteger 的 value 为10,
在准备做修改之前线程2对 AtomicInteger 的 value 改成了20
线程3又将值修改为原来的 10
此时线程一进行 CAS 操作, 发现内存中的值依旧是 10, OK, 更新成功,
备注:
根据实际情况,判断是否处理ABA问题。如果ABA问题并不会影响我们的业务结果,可以选择性处理或不处理;如果ABA会影响我们的业务结果的,这时就必须处理ABA问题了。
-
对于AtomicInteger等,没有什么可修改的属性;且我们只在意其结果值,所以对于这些类来说,本身就算发生了 ABA现象,也不会对原线程的结果造成什么影响。
-
但是如果这时候 A 是个地址,地址 A 引用的值就可能在前后两次操作中就完全不一样了。
比如共享变量是个地址,原来指向User-A对象。
线程1:期望值User-A对象,更新值User-B
线程2:先把User-A更新成User-C
线程3:把User-C改回了UserA的引用地址,同时修改了User-A的年龄属性。
线程1的User-A已经不是原来最初的自己了。
ABA 解决方案
数据库有个锁称为乐观锁,是一种基于数据版本实现数据同步的机制,每次修改一次数据,版本
就会进行累加。
同样,Java也提供了相应的原子引用类AtomicStampedReference<V>
补充:AtomicMarkableReference可以理解为上面AtomicStampedReference的简化版,就是
不关心修改过几次,仅仅关心是否修改过。因此变量mark是boolean类型,仅记录值是否有过修
改。