楔子
前一篇文章,我们介绍了 synchronized,知道了 synchronized 可以解决某些数据的原子性问题,本篇文章我们以 AtomicInteger 为切入点,继续学习 CAS 无锁化的知识。
使用 synchronized 解决 i++的原子性
使用 synchronized 关键字,保证数据原子性
public class Test {
static int synchronizedValue = 0;
public static void main(String[] args) {
// 使用 synchronized 的++
synchronizedAdd();
}
private static void synchronizedAdd() {
for(int i = 0; i < 50; i++) {
new Thread(() -> {
synchronized(Test.class) {
System.out.println("synchronizedAdd:"+ ++Test.synchronizedValue);;
}
}).start();
}
}
}
结果:
synchronizedAdd:50
使用 AtomicInteger 解决自增原子性
static AtomicInteger atomicValue = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
// 使用 atomic 的++
atomicAdd();
}
private static void atomicAdd() {
for(int i = 0; i < 50; i++) {
new Thread(() -> System.out.println("atomicAdd:"+ Test.atomicValue.incrementAndGet())).start();
}
}
结果
atomicAdd:50
CAS 原理
什么叫 CAS 呢?英文全称 compare and swap,翻译成中文就是比较和交换的意思。专业的说法叫乐观锁。说人话就是,我们修改数据的时候,会去尝试比较一下,这个值有没有被其他人修改过,如果没有被修改,那么我们自己就可以修改;如果已经被修改过了,那么我们就重新获取最新的值,重复执行以上步骤再次去比较。接下来我们从 AtomicInteger 源码进一步分析CAS。
AtomicInteger 源码解析
观察 AtomicInteger 源码,我们发现,他可以分为几个部分。
- Unsafe:核心类,真正负责执行 CAS 操作的类
- value 和 valueOffset:值与偏移量
- API 接口:对外提供各种使用方式,主要是封装了 Unsafe 的一些操作
Unsafe
Unsafe 顾名思义,这是一个不安全的类,内部是大量的 native 方法,JDK 原则上是不允许我们使用他的,他是供给 JDK 内部使用的一个类。首先他的构造函数是私有化,不能自己手动去实例化他, 其次,虽然他提供了 Unsafe.getUnsafe()方法来获取一个实例,但是他有一个判断,如果他不是由系统加载的他就会直接抛出异常,以上提到的源码如下:
// (1)构造方法私有化
private Unsafe() {
}
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
// (2)如果 Unsafe 不是由 JVM 加载的,那么就会报错
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
valueOffset
类初始化的时候,会加载一个静态代码块,通过 unsafe 去确定一个 final 标记的偏移量,源码如下
private static final long valueOffset;
// 类初始化的时候,就会执行该静态代码块,通过 unsafe 去确定一个 final 标记的偏移量
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
接着我们回到我们自己写的方法 atomicAdd
private static void atomicAdd() {
for(int i = 0; i < 50; i++) {
new Thread(() -> System.out.println("atomicAdd:"+ Test.atomicValue.incrementAndGet())).start();
}
}
跟进 incrementAndGet 方法,这是一个包装方法,直接调用的 unsafe.getAndAddInt
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
跟进 unsafe.getAndAddInt 方法,好了以下,就是我们需要分析的代码了
public final int getAndAddInt(Object 当前对象, long 偏移量, int 自增值) {
int 对象的 value;
do {
// 这个本地方法的作用是,
// 从 AtomicInteger 对象实例,根据 valueOffset 偏移量,获取 value 这个字段的位置,从而获取到当前的 value 的值
对象的 value/计算出的对象的 value = this.getIntVolatile(当前对象, 偏移量);
}
// 如果【计算出的 value】和【当前对象+偏移量】的不一致,那么这一次的 compare 就为 false,那么就会进入下一次循环
// 如果【计算出的 value】和【当前对象+偏移量】的一致,那么这一次的 compare 就为 true,此时
// 【对象的 value】 = 【计算出的对象的 value】 + 【自增值】,并且跳出循环
while(!this.compareAndSwapInt(当前对象, 偏移量, 计算出的对象的 value, 计算出的对象的 value + 自增值));
return 对象的 value;
}
Atomic 原子类 CAS 常见的几种问题
ABA 问题
举个例子,比如当某个值为 A 的时候,你才进行操作。所以有可能会出现以下情况
- 初始为 A
- A——>B
- B——>A
- 开始操作
(2)到(3)这个步骤,这个值是被人改过的,但是这个值和我期望的值是一样的,所以我们去 compareAndSwapInt 的时候,会发现这个值还是 A,就设置成功了。
注:如何解决 ABA 问题呢?加个时间戳就搞定了,比较的时候带上时间戳。
无限循环问题
看源码,因为我们是走的 do…while循环,所以有可能会循环很多次,都比较不成功。
注:如何解决无限循环问题呢?jdk 给我们提供了一个思路,分段 CAS,感兴趣的读者可以参考 LongAdder 类。
自定义对象原子问题
AtomicInteger,只能保证一个变量的原子性。
注:复杂对象怎么办呢?jdk 给我们提供了 AtomicReference,他比较的是这个对象的引用是不是一个。