在JDK 5之后,Java类库中才开始使用CAS操作,该操作由sun.misc.Unsafe类里面的 compareAndSwapInt()和compareAndSwapLong()等几个方法包装提供。HotSpot虚拟机在内部对这些方法做了特殊处理,即时编译出来的结果就是一条平台相关的处理器CAS指令,没有方法调用的过程, 或者可以认为是无条件内联进去了。不过由于Unsafe类在设计上就不是提供给用户程序调用的类(Unsafe::getUnsafe()的代码中限制了只有启动类加载器(Bootstrap ClassLoader)加载的Class才能访问它),因此在JDK 9之前只有Java类库可以使用CAS,譬如J.U.C包里面的整数原子类,其中的 compareAndSet()和getAndIncrement()等方法都使用了Unsafe类的CAS操作来实现。而如果用户程序也有用CAS操作的需求,那要么就采用反射手段突破Unsafe的访问限制,要么就只能通过Java类库API来间接使用它。直到JDK 9之后,Java类库才在VarHandle类里开放了面向用户程序使用的CAS操作。

Unsafe 实现 CAS

核心 API


  1. compareAndSwapObject
  2. compareAndSwapInt
  3. compareAndSwapInt


原子变量提供的原子性来自CAS操作,CAS来自Unsafe,然后由CPU的 cmpxchg 指令来保证。

Atomic 工具包

Atomic 工具类分为一下几类

  • 普通原子类型:提供对boolean、int、long和对象的原子性操作。
  • AtomicBoolean
  • AtomicInteger
  • AtomicLong
  • AtomicReference
  • 原子类型数组:提供对数组元素的原子性操作。
  • AtomicLongArray
  • AtomicIntegerArray
  • AtomicReferenceArray
  • 原子类型字段更新器:提供对指定对象的指定字段进行原子性操作。
  • AtomicLongFieldUpdater
  • AtomicIntegerFieldUpdater
  • AtomicReferenceFieldUpdater
  • 带版本号的原子引用类型:以版本戳的方式解决原子类型的ABA问题。
  • AtomicStampedReference
  • AtomicMarkableReference
  • 原子累加器(JDK1.8):AtomicLong和AtomicDouble的升级类型,专门用于数据统计,性能更高。
  • DoubleAccumulator
  • DoubleAdder
  • LongAccumulator
  • LongAdder

使用案例

多线程累加

下面是创建 1000 个线程分别对 total 进行累加,由于没有对 total 做任何线程安全操作所以我们打印的结果一定是小于或者等于 1000000

public class AtomicTest {

public static int total = 0;

public static void main(String[] args) throws InterruptedException {
CountDownLatch downLatch = new CountDownLatch(1000);
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
total++;
}
downLatch.countDown();
}).start();
}
downLatch.await();
System.out.println(total);
}
}

// 结果输出
// 999843

通过 AtomicInteger 实现

我们可以通过 integer 的原子类,AtomicInteger 类来进行自增操作,然后调用 ​​getAndIncrement()​​ 方法,保证每次都是原子操作。最终的结果输出:1000000。

public class AtomicTest2 {

public static AtomicInteger total = new AtomicInteger(0);

public static void main(String[] args) throws InterruptedException {
CountDownLatch downLatch = new CountDownLatch(1000);
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
total.getAndIncrement();
}
downLatch.countDown();
}).start();
}
downLatch.await();
System.out.println(total.get());
}
}

// 结果输出
// 1000000

再来看看 ​​getAndIncrement​​ 方法它主要是做了哪些操作呢 ?

public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}

// 然后调用 unsafe 类的本地方法
// unsafe.getAndAddInt 方法
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

return var5;
}

对象属性进行原子操作

  1. 获取属性偏移量
  2. 通过 CAS 方式进行修改
public class UnsafeTest {
public static Unsafe U;

static {
try {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
U = (Unsafe)f.get(null);
} catch (Throwable e) {
e.printStackTrace();
}
}

public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InstantiationException {

User user = new User();
// 获取字段
Field age = user.getClass().getDeclaredField("age");
// 获取字段相对Java对象的"起始地址"的偏移量
long offset = U.objectFieldOffset(age);
// 设置值
boolean success = U.compareAndSwapInt(user, offset, 10, 20) ;
System.out.println("修改结果: " + success ) ;
// 打印数据
System.out.println("查询结果: " + user.getAge());
}


}

class User {
private int age;

public User() {
this.age = 10;
}

public int getAge() {
return age;
}
}

//输出结果
修改结果: true
查询结果: 20

注意事项

ABA 问题

ABA 是在 CAS 场景下面容易出现的问题,当一个数据的值为 A1, 然后再被修改为 B ,最后再修改为 A2。 并且 A1 和 A2 相等;但是另外一个线程查询到的结果是 A1 , 此时更新为 C ,也可以成功。这样的风险就是后者不清楚数据是否被改变,并且能够成功修改。


举个例子:你钱包里面有 100 块钱,然后小偷偷走后又给你换回来了,你看到的还是钱包里面的 100 块钱吗?再比如你老婆和其他的男人出轨了,然后再还回来,你看到的还是你的老婆吗?其实这些问题已经触犯到道德和FL的范围。


针对 ABA 问题,我们可以通过 ​​AtomicStampedReference​​ 解决。

AtomicStampedReference

AtomicStampedReference 维护一个对象引用以及一个整数“stamp”,该整数可以进行原子更新。 实现说明:此实现通过创建表示“装箱”[reference,integer] 对的内部对象来维护戳记引用。 在此之前我们先对 ABA 问题进行一个模拟:

public class ABATest {

private static AtomicInteger index = new AtomicInteger(10);

public static void main(String[] args) {
new Thread(()-> {
index.compareAndSet(10, 101);
index.compareAndSet(101, 10);
System.out.println(Thread.currentThread() + ": 10->101->10");
}, "Tom").start();

new Thread(()-> {
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean result = index.compareAndSet(10, 1000);
System.out.println(Thread.currentThread() + ": 更新结果:" + result + ", 更新后的值: " + index.get());
}, "Jack").start();
}
}

通过 AtomicStampedReference 来解决这个问题: 就是我们在进入第二个线程之前,先读取当前的版本号,然后进入更新。这个读取的版本号可能是一个旧的值。如果出现这种情况,那么我们执行 ​​compareAndSet​​ 就会返回失败。

public class ABATest2 {

private static AtomicStampedReference index = new AtomicStampedReference(10, 1);

public static void main(String[] args) {
new Thread(() -> {
index.compareAndSet(10, 101, index.getStamp(), index.getStamp() + 1);
index.compareAndSet(101, 10, index.getStamp(), index.getStamp() + 1);
System.out.println(Thread.currentThread() + ": 10->101->10");
}, "Tom").start();

int stamp = index.getStamp();
new Thread(() -> {

try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean result = index.compareAndSet(10, 1000, stamp, stamp + 1);
System.out.println(Thread.currentThread() + ": 更新结果:" + result + ", 更新后的值: " + index.getReference());
}, "Jack").start();
}
}

返回结果如下所示:

Thread[Tom,5,main]: 10->101->10
Thread[Jack,5,main]: 更新结果:false, 更新后的值: 10

参考文档