一、什么是CAS
CAS即比较并替换,是一种轻量级锁,一般用于并发量不大的场景,CAS机制中用了3个变量:内存值V,旧的预期值A,要修改的新值B;只有当内存中的值和旧的预期值相等的情况下才更新值为B,否则该线程会一直自旋等待,下面我们用大白话来解释CAS。
二、CAS 应用
想象一下假如现在我们有2个线程,对共享变量进行i++操作,如果不加锁会出现什么情景呢,先看一个代码
private static int count = 0;
public static void main(String[] args) {
CountDownLatch latch = new CountDownLatch(2);
for(int i=0;i<2;i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
for(int i=0;i<100;i++) {
count++;
}
latch.countDown();
}
}).start();
}
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("count=" + count);
}
这份代码理想情况下,按照预期会输出count=200,但是大多数情况下它的输出都小于200,这是因为i++操作不是原子的,这个时候我们可能会想到加锁,先来看看加synchronized
public class JAVA的CAS机制 {
private static int count = 0;
public static void main(String[] args) {
CountDownLatch latch = new CountDownLatch(2);
for(int i=0;i<2;i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (JAVA的CAS机制.class) {
for(int i=0;i<100;i++) {
count++;
}
}
latch.countDown();
}
}).start();
}
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("count=" + count);
}
}
这份代码会输出count=200,因为我们通过synchronized来保证每次只有一个线程对共享变量进行操作,虽然synchronized可以解决第一份代码的问题,但是synchronized相当于一个悲观锁,每次只允许一个线程操作。那我们这里要说的CAS是怎么用呢,它是怎么解决第一份代码的问题呢,我们先看一段代码
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) {
CountDownLatch latch = new CountDownLatch(2);
for (int i = 0; i < 2; i++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 100; i++) {
count.getAndIncrement();
}
latch.countDown();
}
}).start();
}
try {
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("count=" + count.get());
}
这份代码输出一直都是count=200,这里使用了JAVA原子包里面提供的类atomicInteger,这是一个原子操作类,如果大家感兴趣可以看看它的源代码,底层就是使用了CAS机制来实现,CAS相当于synchronized的轻量级实现,是一个乐观锁,假如某个线程没有更新到值,那么它会一直自旋并不断重试,直到最后更新值。在并发量不大的时候,建议大家使用CAS。
三、AtomicInteger源码分析
JAVA的CAS机制大量用在了JAVA并发原子类里面,这里我们以AtomicInteger类来分析其具体实现
先来看定义
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;
/**
* Creates a new AtomicInteger with the given initial value.
*
* @param initialValue the initial value
*/
public AtomicInteger(int initialValue) {
value = initialValue;
}
/**
* Creates a new AtomicInteger with initial value {@code 0}.
*/
public AtomicInteger() {
}
/**
* Gets the current value.
*
* @return the current value
*/
public final int get() {
return value;
}
/**
* Sets to the given value.
*
* @param newValue the new value
*/
public final void set(int newValue) {
value = newValue;
}
其中Unsafe类是CAS的核心实现类,JAVA通过它来访问本地方法;而valueOffset则是变量在内存中的偏移地址,
;而用valatile修饰得变量value保证了多线程内存可见性。
再看看getAndIncrement
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
调用unsafe.getAndInt,假设当前内存中变量值为2,同时有2个线程来操作这个变量,假设A线程拿到了这个变量,准备修改为3,B线程也准备修改这个变量,发现目前的值3和自己手里的变量副本2不一样,所以更新失败,这就相当于使用了CAS
四、CAS的问题
4.1 CPU消耗大
CAS线程会不停自旋,如果并发量大的话,将会不停重试,还不释放CPU,极端情况下会耗光资源。
4.2 只能保证某个变量的原子操作
从第三份代码我们看到,CAS只能保证单个变量的原子性,不能保证某个代码块或者某个对象的原子性(这里可以用并发包里面的原子更新类AtomicReference实现)
4.3 经典的ABA问题
就是说变量A在某个线程修改后成B后,另一个线程又将其修改为A,导致其他线程误认为这个变量实际没有变过,这就是经典的ABA问题,要解决这个,我们可以加一个版本号,每次比较的时候,不仅需要比较值,还需要看版本号是否一致,一致才修改,在JAVA并发包里面AtomicStampedReference类就是用了版本号来解决ABA问题。