0%

从i++到CAS操作

前言

i++不是一个原子操作, 原子操作的意思是, 执行代码的时候, 不会发生线程的切换.
即使i++只有一行代码, 但是也不是一个原子操作, 主要就是三件事情, 很惭愧, 就做了一点微小的工作(

  1. 取值: 从内存中取值到寄存器.
  2. 自增: 寄存器进行i+1.
  3. 回写: 将自增后的值回写到内存中.

多线程下的问题

i++不是一个原子操作, 那么就可能在多线程下出现问题.
比如发生以下操作.

  1. Thread 1从内存中取值i = 0, 在寄存器中自增i = 1.
  2. Thread 1在回写到内存前, 时间片结束, 切换到Thread 2.
  3. Thread 2从内存中取值i = 0, 在寄存器中自增i = 1.
  4. Thread 2在回写到内存前, 时间片结束, 切换到Thread 1.
  5. Thread 1将寄存器中的i = 1回写到内存中, 内存中i = 1, 执行结束, 切换到Thread 2.
  6. Thread 1将寄存器中的i = 1回写到内存中, 内存中i = 1, 执行结束.

最终结果, i = 1, 但是我们期望的是两个线程执行了i++, 最终结果应该是i = 2.
这时就轮到java.util.concurrent包出场了, 下面是一个简单的例子.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Main {
private static int i = 0; // 加上 volatile 也没用
private static AtomicInteger j = new AtomicInteger(0);
public static void main(String[] args) throws Exception {
ExecutorService threadPool = Executors.newFixedThreadPool(128);
for(int i = 0; i < 1_000_000; i++) {
threadPool.execute(() -> Main.i++);
threadPool.execute(() -> Main.j.incrementAndGet());
}
threadPool.shutdown();
threadPool.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
System.out.println(Main.i); // 999063
System.out.println(Main.j); // 1000000
}
}

可以看到AtomicInteger类可以在多线程下, 输出我们预期的结果, 而普通的i++, 则与预期结果不符.

Unsafe 类的存在

主要的代码就是incrementAndGet这个方法. 我们可以进去看看.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class AtomicInteger extends Number implements java.io.Serializable {
private static final Unsafe unsafe = Unsafe.getUnsafe();
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
}
public final class Unsafe {
public static Unsafe getUnsafe() {
Class<?> caller = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(caller.getClassLoader()))
throw new SecurityException("Unsafe"); // 判断调用 Unsafe 的类是否是 BootstrapClassLoader 加载的类
return theUnsafe;
}
}

源码#L87-L92
可以看到, 里面借助了sun.misc.Unsafe这个类, 它是一个不安全的类, 可以绕过JVM直接操作本地内存.
所以为了不让人滥用, 调用getUnsafe()时会判断调用Unsafe的类是否是BootstrapClassLoader加载的类.
我们自己写的类是由sun.misc.Launcher$AppClassLoader加载的, 所以如果我们自己去调用getUnsafe(), 肯定会抛出SecurityException.
当然我们也可以通过反射获取.

1
2
3
4
5
6
7
public class UnsafeHelper {
public static Unsafe getUnsafe() {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
return (Unsafe) unsafeField.get(null);
}
}

深入 getAndAddInt

源码#L1028-L1034

1
2
3
4
5
6
7
8
9
10
11
12
public final class Unsafe {
// unsafe.getAndAddInt(this, valueOffset, 1) + 1;
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
public native int getIntVolatile(Object o, long offset);
public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
}

整个核心就是compareAndSwapInt这个CAS操作. 整个方法的逻辑就是

  1. 读取Objectvalue属性在内存中的偏移量地址offset, 写入变量v.
  2. compareAndSwapInt方法, 判断Object的地址offset的值, 是否为v, 是则将v + delta写入地址offset.
  3. 如果地址offset的值和变量v不相等, 说明有其他线程修改了, 那么就再循环一次, 回到步骤1.

总结

再深入就是c++和汇编层次的代码了, 技术有限就不细说了(太菜), 建议查看参考资料.
总的来说, CAS就是一个比较操作, 直接操作内存地址, 就不会发生寄存器的值来不及回写到内存中的问题.

参考资料

点击进入云乞讨模式!