在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
- compareAndSwapObject
- compareAndSwapInt
- 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;
}
对象属性进行原子操作
- 获取属性偏移量
- 通过 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