CAS(compare and swap)比较并交换。给定一个预期值和一个新值,首先比较内存中的值与预期值是否相等(比较),如果相等则将内存中的值改为新值(交换)。否则,说明在此期间有其他线程修改了值,则修改失败。通常CAS伴随自旋,即失败后重新从主内存中读取最新的值最为预期值,再次尝试修改。CAS的伪代码如下:
for(;;){
expect = readFromMemory(); // 从内存中读取最新的值作为预期值
newValue = expect + 1;
if(expect == newFromMemory()){ //如果相等说明再次期间没有其他线程修改过值
write(newValue); //将最新值写回内存中
}
}
首先我们来看一个熟悉的例子,下面的代码会创建多个线程同时修改共享变量count,很明显,这将会有原子性问题,count最终的结果将会不确定。
public class AtomicTest {
static int count = 0;
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(100);
for (int i = 0; i < 100; i++) {
new Thread(() -> {
for (int j = 0; j < 100; j++) {
count++;
}
countDownLatch.countDown();
}).start();
}
countDownLatch.await();
System.out.println(count);
}
}
下面利用CAS的思想,自己实现一个支持原子自增的原子类:
public class UnsafeFactory {
/**
* 获取 Unsafe 对象
* @return
*/
public static Unsafe getUnsafe() {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe) field.get(null);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
public class MyAtomicInteger {
private Unsafe unsafe;
private volatile int value;
public long offset;
public MyAtomicInteger() {
unsafe = UnsafeFactory.getUnsafe();
try {
// 偏移量
offset = unsafe.objectFieldOffset(MyAtomicInteger.class.getDeclaredField("value"));
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
public void increment() {
int expect;
do {
// 根据偏移量从内存中获取最新的值
expect = unsafe.getIntVolatile(this, offset);
} while (!unsafe.compareAndSwapInt(this, offset, expect, expect + 1));
}
public int get() {
return value;
}
}
其实我们上面写的MyAtomicInteger类,JDK已经帮我们实现了,我们只需要拿过来用就行了。此外JDK还提供了许多其他的原子类
基本类型: AtomicInteger、AtomicLong、AtomicBoolean;
引用类型: AtomicReference、AtomicStampedRerence、AtomicMarkableReference;
数组类型: AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
这些原子类的使用方式的都很简单,且实现原理与我们上面的MyAtomicInteger大差不差,在此就不再赘述了。
使用CAS实现锁,可以避免线程进入阻塞,带来额外的系统调用开销,在某种程度上,性能会比synchronied、Lock这类可能会阻塞线程的锁性能更高。当然,如果临界区(指同一时刻只能有一个线程执行)的代码如果业务逻辑比较复杂,执行时间较长,并发较高,那么可能不适用使用CAS锁,因为这会造成大量的线程自旋等待,给CPU造成巨大的负担。
CAS锁实现也很简单,我们可以利用CAS去修改某个变量,修改成功的线程就等于获取到锁了,而其他线程则自旋等待,直到获取锁的线程释放了锁。
public class CasLock {
private AtomicReference<Thread> atomicReference = new AtomicReference<>();
public void lock() {
Thread currentThread = Thread.currentThread();
for (;;) {
if (atomicReference.compareAndSet(null, currentThread)) {
break;
}
}
}
public void unlock() {
Thread currentThread = Thread.currentThread();
atomicReference.compareAndSet(currentThread, null);
}
}
所谓的ABA问题指的是某个线程将值改成B,然后又改为A。举个形象的例子,你往银行卡里存了100万后,有人把你银行卡里的钱全都转走了,然后又转回来了,此时你再去查余额,发现还是100万,但你能说在次期间你银行卡的钱没被别人动过吗?很明显是不能的,当然如果你只是关心银行卡中的余额是否正确,那么你就不需要关心ABA问题,但如果你关心是否有其他人动作你的卡,那么你就需要关心ABA问题了。
解决ABA问题的方式也很简单,就是加入一个版本号,每次更新后版本号+1,这样我们就可以通过对比版本号知道期间有没有被修改过了。JDK也提供了一个带版本号的原子类AtomicStampedReference
/**
* expectedReference: 预期值
* newReference: 新值
* expectedStamp: 预期版本号
* newStamp: 最新版本号
*/
public boolean compareAndSet(V expectedReference,V newReference,int expectedStamp,int newStamp)
如在下面的代码中,我会启动俩个线程,线程1读取内存中的值和版本号后,会阻塞一秒。在这一秒期间,线程2会将值从1修改为2,再改回1。一秒后线程1被唤醒,通过比较版本号会发现期间数据被动过,从而修改失败
public class AtomicStampedReferenceTest {
public static void main(String[] args) {
// 定义AtomicStampedReference Pair.reference值为1, Pair.stamp为1
AtomicStampedReference atomicStampedReference = new AtomicStampedReference<>(1, 1);
new Thread(() -> {
int[] stampHolder = new int[1];
int value = (int) atomicStampedReference.get(stampHolder);
int stamp = stampHolder[0];
System.out.println("Thread1 read value: " + value + ", stamp: " + stamp);
// 阻塞1s
LockSupport.parkNanos(1000000000L);
// Thread1通过CAS修改value值为3
if (atomicStampedReference.compareAndSet(value, 3, stamp, stamp + 1)) {
System.out.println("Thread1 update from " + value + " to 3");
} else {
System.out.println("Thread1 update fail!");
}
}, "Thread1").start();
new Thread(() -> {
int[] stampHolder = new int[1];
int value = (int) atomicStampedReference.get(stampHolder);
int stamp = stampHolder[0];
System.out.println("Thread2 read value: " + value + ", stamp: " + stamp);
// Thread2通过CAS修改value值为2
if (atomicStampedReference.compareAndSet(value, 2, stamp, stamp + 1)) {
System.out.println("Thread2 update from " + value + " to 2");
// do something
value = (int) atomicStampedReference.get(stampHolder);
stamp = stampHolder[0];
System.out.println("Thread2 read value: " + value + ", stamp: " + stamp);
// Thread2通过CAS修改value值为1
if (atomicStampedReference.compareAndSet(value, 1, stamp, stamp + 1)) {
System.out.println("Thread2 update from " + value + " to 1");
}
}
}, "Thread2").start();
}
}
最后总结一下CAS的缺点:
• CAS自旋长时间不成功,会给CPU带来较大的压力
• 只能保证一个变量的原子操作