文章目录
First let’s look at atomic
- 原子操作类,指的是 java.util.concurrent.atomic 包下,一系列以 Atomic 开头的包装类。如 AtomicBoolean,AtomicUInteger,AtomicLong。它们分别用于 Boolean,Integer,Long 类型的原子性操作。在某些情况下,性能会比 synchronized 更好
- 而 Atomic 操作类的底层正是用到了 “CAS 机制”
Basic concept
- compare ans swap,即比较-替换,这个名称来源于unsafe类的一系列compareAndSwap***()方法。
- CAS 机制中使用了 3 个基本操作数:内存地址 V,旧的预期值 A,要修改的新值 B。
- 更新一个变量的时候,只有当变量的预期值 A 和内存地址 V 当中的实际值相同时,才会将内存地址 V 对应的值修改为 B并返回true,否则什么都不做并返回false。
- CAS一定要volatile变量配合,这样才能保证可见性,否则旧的预期值A对某条线程来说,永远是一个不会变的值A,只要某次CAS操作失败,永远都不可能成功
- CAS在底层硬件级别一定是原子的,同时只有一个线程执行CAS操作
Underlying principle
- 使用了Unsafe类
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;// 内存地址偏移量
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;//本类的几乎所有方法都是围绕它展开的
·······
·······
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
}
- 上面是AtomicInteger类的部分代码,其中使用了unsafe类
- Unsafe 是 CAS 的核心类,存在于 sun.misc 包中,由于 Java 方法无法直接访问底层系统,而需要通过本地(native)方法来访问, Unsafe 类相当一个后门,基于该类可以像 C 指针一样直接操作特定内存的数据。Unsafe类中的大部分方法都是native修饰的,都可以直接调用OS底层资源
- 变量 value 就是该原子操作类的值,用 volatile 修饰,保证了多线程之间的内存可见性。
- 变量 vauleOffset非常重要,表示该变量值(即value,AtomicInteger.class.getDeclaredField(“value”)就是用于获取value值的)在内存中的偏移量,因为 Unsafe 就是根据内存偏移量来获取数据的。
compareAndSet方法
- compareAndSet方法的作用就是去执行CAS机制
- 从下图可以看出AtomicInteger和unsafe的关系,AtomicInteger的compareAndSet方法调用了unsafe的CAS方法,即compareAndSwap***(),这个方法是用native修饰的,表示该方法是JNI,Java Native Interface,即Java调用C代码的接口,在IDEA里已经不能继续跟踪,因为它的实现被隐藏了
- 调用 UnSafe 类中的 CAS 方法,JVM 会帮我们实现出 CAS 汇编指令。这是一种完全依赖硬件的功能,通过它实现了原子操作。
- 也就是说CAS底层是一种CPU级别的并发原语。
- 由于 CAS 是一种系统级源语,源语属于操作系统用语范畴,是由若干条指令组成,用于完成某一个功能的过程,并且原语的执行必须是连续的,在执行的过程中不允许被中断,不会造成数据不一致问题
incrementAndGet方法
- 再来看AtomicInteger的另一个方法:incrementAndGet。这个方法的作用是Atomically increments by one the current value.(也就是在当前值的基础上+1,返回值是+1之后的值,还有一个getAndIncrement方法,差不多,只不过返回+1之前的值)
- 它同样是调用了compareAndSwapInt这个native方法(其实AtomicInteger类几乎所有方法都调用了unsafe类,这里只是随便挑两个举例)。
- 我们可以看到,在unsafe的getAndAddInt这个方法里,实际上体现了自旋的思想(通过do while)。如果compareAndSwapInt即返回true,即CAS执行成功,while循环就会退出,并返回修改之前的那个值即v,v是通过getIntVolatile获取的(也称为获取快照值),这也是个native方法。反之,如果真实值和期望值不同,那么就只能重新getIntVolatile,再次去进行CAS操作
- 那么compareAndSwapInt这个native方法在干什么呢?
- 它就是调用了底层的C代码,利用offset即内存偏移量找到value在内存中的位置。然后应该就利用了底层的CAS汇编指令去执行CAS了
- 这个偏移量怎么得到的?就在AtomicInteger类的static代码块中
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));//通过这个方法得到
} catch (Exception ex) { throw new Error(ex); }
}
- 为什么要放在static代码块里呢?因为Atomic类一旦被加载到内存,value的内存地址其实就固定了,获取偏移量就是为了获取value的内存地址,不管这个内存地址里是否有值(将来它被new了就有值了)
- 而这个objectFieldOffset方法,可想而知又是一个native方法,因为操作内存Java代码是干不了的(还是C/C++好啊!)。如下:
/**
* Report the location of a given static field, in conjunction with {@link
* #staticFieldBase}.
*/
public native long objectFieldOffset(Field f);
- 最后,再看看compareAndSwapInt的实现:
- 这段代码位于unsafe.cpp中,这是一段CPU原语,有时间再来研究。
Question:why use CAS rather than synchronized?
- synchronized加锁,同时只能一个线程读写;CAS没有加锁(从源码可以看出来),而是通过CAS机制保障原子性,同时可以多个线程读
- CAS在读多写少的情况下效率更高
CAS disadvantage?
- 循环时间长开销很大
- 如果 CAS 失败,会一直尝试,如果 CAS 长时间一直不成功,可能会给 CPU 带来很大的开销(比如线程数很多,每次比较都是失败,就会一直循环),所以希望是线程数比较小的场景。
- 只能保证一个共享变量的原子操作
- 对于多个共享变量操作时,循环 CAS 就无法保证操作的原子性。
- ABA 问题
What is CAS’s ABA problem?
AtomicReference类
- 先来看看AtomicReference类,即原子引用类,这个类其实和AtomicInteger类大体上是一样的,只不过它的value是一个泛型,也就是说,这个类可以替代之前的Atomic***基本数据类型包装类,同时让你在操作自定义类的时候也可采用CAS机制。
- 如下,是它的部分代码:
public class AtomicReference<V> implements java.io.Serializable {
private static final long serialVersionUID = -1848883965231344442L;
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicReference.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile V value;
基本操作:
User z3 = new User("z3", 22);
User li4 = new User("li4", 25);
AtomicReference<User> atomicReference = new AtomicReference<User>();
atomicReference.set(z3);
atomicReference.compareAndSet(z3, li4);//使用CAS机制
使用AtomicReference类演示ABA问题:
AtomicReference<Integer> atomicReference = new AtomicReference<Integer>(100); // 初始值 100
new Thread(() -> {
atomicReference.compareAndSet(100, 101);
atomicReference.compareAndSet(101, 100);
}).start();
new Thread(() -> {
try {
// Thread.sleep(1000);
TimeUnit.SECONDS.sleep(2); // 保证上面线程先执行
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicReference.compareAndSet(100, 2019);
System.out.println(atomicReference.get()); //输出为: 2019
}).start();
AtomicStampedReference类
- AtomicStampedReference类是AtomicReference类的“加强版”,就是为了解决ABA问题。它创建了一个内部类Pair,Pair包括了reference和stamp两个变量。部分代码如下:
public class AtomicStampedReference<V> {
private static class Pair<T> {
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);
}
}
private volatile Pair<V> pair;
Use AtomicStampedReference solve ABA problem
- 思路其实就是加一个版本号stamp,每次修改值的同时版本号递增
- ABA问题发生后,虽然值一样,但版本号不一样,就可以判断出现了ABA
AtomicStampedReference<Integer> asr = new AtomicStampedReference<Integer>(100, 1); // 初始版本为1
new Thread(() -> { // 用于产生ABA问题
int stamp = asr.getStamp();
System.out.println(Thread.currentThread().getName() + " 的版本号为:" + stamp);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
asr.compareAndSet(100, 101, asr.getStamp(), asr.getStamp() + 1 ); // 版本号递增
asr.compareAndSet(101, 100, asr.getStamp(), asr.getStamp() + 1 );
}).start();
new Thread(() -> {
int stamp = asr.getStamp();
System.out.println(Thread.currentThread().getName() + " 的版本号为:" + stamp);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean b = asr.compareAndSet(100, 2019, stamp, stamp + 1);
System.out.println(b); // false 即修改失败
System.out.println(asr.getReference()); // 结果:100
}).start();
- compareAndSet方法的内部处理如下,逻辑比较好理解
- 执行到最后会调用casPair这个方法,最后调用unsafe类的compareAndSwapObject这个JNI
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
private boolean casPair(Pair<V> cmp, Pair<V> val) {
return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}