#CAS原理分析
我们知道多线程操作共享资源时,会出现三个问题:可见性、有序性以及原子性。
一般情况下,我们采用synchronized同步锁(独占锁、互斥锁),即同一时间只有一个线程能够修改共享变量,其他线程必须等待。但是这样的话就相当于单线程,体现不出来多线程的优势。
那么我们有没有另一种方式来解决这三个问题呢?
之前提到的一个volatile关键字,它可以解决可见性和有序性的问题。而且如果操作的共享变量是基本数据类型,并且同一时间只对变量==进行读取或者写入的操作==,那么原子性问题也得到了解决,就不会产生多线程问题了。
但是通常,我们都要先==读取共享变量,然后操作共享变量,最后写入共享变量==,那么这个时候怎么保证整个操作的原子性呢?一种解决方式就是CAS技术。
CAS(Compare and Swap)即比较并交换。在讲解这个之前,先了解两个重要概念:悲观锁与乐观锁。
一. 悲观锁与乐观锁
- 悲观锁: 假定会发生并发冲突,即共享资源会被某个线程更改。所以当某个线程获取共享资源时,会阻止别的线程获取共享资源。也称独占锁或者互斥锁,例如java中的synchronized同步锁。
- 乐观锁: 假设不会发生并发冲突,只有在最后更新共享资源的时候会判断一下在此期间有没有别的线程修改了这个共享资源。如果发生冲突就重试,直到没有冲突,更新成功。CAS就是一种乐观锁实现方式。
悲观锁会阻塞其他线程。乐观锁不会阻塞其他线程,如果发生冲突,采用死循环的方式一直重试,直到更新成功。
二. CAS的实现原理
CAS的原理很简单,包含三个值当前内存值(V)、预期原来的值(A)以及期待更新的值(B)。
如果内存位置V的值与预期原值A相匹配,那么处理器会自动将该位置值更新为新值B,返回true。否则处理器不做任何操作,返回false。
实现CAS最重要的一点,就是比较和交换操作的一致性,否则就会产生歧义。
比如当前线程比较成功后,准备更新共享变量值的时候,这个共享变量值被其他线程更改了,那么CAS函数必须返回false。
要实现这个需求,java中提供了Unsafe类,它提供了三个函数,分别用来操作基本类型int和long,以及引用类型Object。==并能保证此次操作的原子性==
public final native boolean compareAndSwapObject
(Object obj, long valueOffset, Object expect, Object update);
public final native boolean compareAndSwapInt
(Object obj, long valueOffset, int expect, int update);
public final native boolean compareAndSwapLong
(Object obj, long valueOffset, long expect, long update);
参数的意义:
- obj 和 valueOffset:表示这个共享变量的内存地址。这个共享变量是obj对象的一个成员属性,valueOffset表示这个共享变量在obj类中的内存偏移量。所以通过这两个参数就可以直接在内存中修改和读取共享变量值。
- expect: 表示预期原来的值。
- update: 表示期待更新的值。
接下来我们来看看java并发框架下的atomic包是如何使用CAS的。
三. JUC并发框架下的原子类(atomic)
JUC 包中的原子类是哪4类?
####基本类型
使用原子的方式更新基本类型
- AtomicInteger:整形原子类
- AtomicLong:长整型原子类
- AtomicBoolean :布尔型原子类
####数组类型
使用原子的方式更新数组里的某个元素
- AtomicIntegerArray:整形数组原子类
- AtomicLongArray:长整形数组原子类
- AtomicReferenceArray :引用类型数组原子类
####引用类型
- AtomicReference:引用类型原子类
- AtomicStampedRerence:原子更新引用类型里的字段原子类
- AtomicMarkableReference :原子更新带有标记位的引用类型
####对象的属性修改类型
- AtomicIntegerFieldUpdater:原子更新整形字段的更新器
- AtomicLongFieldUpdater:原子更新长整形字段的更新器
- AtomicStampedReference :原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于解决原子的更新数据和数据的版本号,可以解决使用 CAS 进行原子更新时可能出现的 ABA 问题。
调用JUC并发框架下原子类的方法时,不需要考虑多线程问题。那么我们分析它是怎么解决多线程问题的。以AtomicInteger类为例
3.1成员变量
// 通过它来实现CAS操作的。因为是int类型,所以调用它的compareAndSwapInt方法
private static final Unsafe unsafe = Unsafe.getUnsafe();
// value这个共享变量在AtomicInteger对象上内存偏移量,
// 通过它直接在内存中修改value的值,compareAndSwapInt方法中需要这个参数
private static final long valueOffset;
// 通过静态代码块,在AtomicInteger类加载时就会调用
static {
try {
// 通过unsafe类,获取value变量在AtomicInteger对象上内存偏移量,其中objectFieldOffset是一个本地方法
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
// 共享变量,AtomicInteger就保证了对它多线程操作的安全性。
// 使用volatile修饰,解决了可见性和有序性问题。
private volatile int value;
有三个重要的属性:
- unsafe: 通过它实现CAS操作,因为共享变量是int类型,所以调用compareAndSwapInt方法。
- valueOffset: 共享变量value在AtomicInteger对象上内存偏移量
- value: 共享变量,使用volatile修饰,解决了可见性和有序性问题。
##3.2 重要方法
###3.2.1 get与set方法
// 直接读取。因为是volatile关键子修饰的,总是能看到(任意线程)对这个volatile变量最新的写入
public final int get() {
return value;
}
// 直接写入。因为是volatile关键子修饰的,所以它修改value变量也会立即被别的线程读取到。
public final void set(int newValue) {
value = newValue;
}
因为value变量是volatile关键字修饰的,它总是能读取(任意线程)对这个volatile变量最新的写入。它修改value变量也会立即被别的线程读取到。
3.2.2 compareAndSet方法
// 如果value变量的当前值(内存值)等于期望值(expect),那么就把update赋值给value变量,返回true。
// 如果value变量的当前值(内存值)不等于期望值(expect),就什么都不做,返回false。
// 这个就是CAS操作,使用unsafe.compareAndSwapInt方法,保证整个操作过程的原子性
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
通过调用unsafe的compareAndSwapInt方法实现CAS函数的。==但是CAS函数只能保证比较并交换操作的原子性,但是更新操作并不一定会执行==。
比如我们想让共享变量value自增。
共享变量value自增是三个操作,1.读取value值,2.计算value+1的值,3.将value+1的值赋值给value。分析这三个操作:
- 读取value值,因为value变量是volatile关键字修饰的,能够读取到任意线程对它最后一次修改的值,所以没问题。
- 计算value+1的值:这个时候就有问题了,可能在计算这个值的时候,其他线程更改了value值,因为没有加同步锁,所以其他线程可以更改value值。
- 将value+1的值赋值给value: 使用CAS函数,如果返回false,说明在当前线程读取value值到调用CAS函数方法前,共享变量被其他线程修改了,那么value+1的结果值就不是我们想要的了,因为要重新计算。
3.2.3 getAndAddInt方法
public final int getAndAddInt(Object obj, long valueOffset, int var) {
int expect;
// 利用循环,直到更新成功才跳出循环。
do {
// 获取value的最新值
expect = this.getIntVolatile(obj, valueOffset);
// expect + var表示需要更新的值,如果compareAndSwapInt返回false,说明value值被其他线程更改了。
// 那么就循环重试,再次获取value最新值expect,然后再计算需要更新的值expect + var。直到更新成功
} while(!this.compareAndSwapInt(obj, valueOffset, expect, expect + var));
// 返回当前线程在更改value成功后的,value变量原先值。并不是更改后的值
return expect;
}
这个方法在Unsafe类中,利用do_while循环,先利用当前值,计算更新值,然后通过compareAndSwapInt方法设置value变量,如果compareAndSwapInt方法返回失败,表示value变量的值被别的线程更改了,所以循环获取value变量最新值,再通过compareAndSwapInt方法设置value变量。直到设置成功。跳出循环,返回更新前的值。
// 将value的值当前值的基础上加1,并返回当前值
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
// 将value的值当前值的基础上加-1,并返回当前值
public final int getAndDecrement() {
return unsafe.getAndAddInt(this, valueOffset, -1);
}
// 将value的值当前值的基础上加delta,并返回当前值
public final int getAndAdd(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta);
}
// 将value的值当前值的基础上加1,并返回更新后的值(即当前值加1)
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
// 将value的值当前值的基础上加-1,并返回更新后的值(即当前值加-1)
public final int decrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, -1) - 1;
}
// 将value的值当前值的基础上加delta,并返回更新后的值(即当前值加delta)
public final int addAndGet(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}
都是利用unsafe.getAndAddInt方法实现的。
3.3 能不能给我简单介绍一下 AtomicInteger 类的原理?
AtomicInteger 类主要利用 CAS (compare and swap) + volatile 和 native 方法来保证原子操作,从而避免 synchronized 的高开销,执行效率大为提升。
CAS的原理是拿期望的值和原本的一个值作比较,如果相同则更新成新的值。UnSafe 类的objectFieldOffset() 方法是一个本地方法,这个方法是用来拿到“原来的值”的内存地址,返回值是 valueOffset。另外 value 是一个volatile变量,在内存中可见,因此 JVM 可以保证任何时刻任何线程总能拿到该变量的最新值。