并发编程,为了保证数据的安全,需要满足三个特性:原子性、可见性、有序性。Java 中可以通过锁和 CAS 的方式来实现原子操作。
前面 synchronized 的文章中介绍过,synchronized 是一个重量级操作,性能较差,CAS 在保证原子性中有较好的性能。此外,synchronized 的优化中,偏向锁、轻量级锁也都用的到了 CAS。
那么,CAS 到底是什么?今天就来揭秘一下:
CAS,Compare And Swap,即比较并交换。Doug lea 大神在同步组件中大量使用 CAS 技术鬼斧神工地实现了 Java 多线程的并发操作。整个 AQS 同步组件、Atomic 原子类操作等等都是以 CAS 实现的。可以说 CAS 是整个 J.U.C 的基石。
CAS 比较交换的过程 CAS(V,A,B): V-一个内存地址存放的实际值、A-旧的预期值、B-即将更新的值,当且仅当预期值 A 和内存值 V 相同时,将内存值修改为 B 并返回 true,否则什么都不做,并返回 false。
synchronized 是线程获取锁是一种悲观锁策略,即假设每一次执行临界区代码都会产生冲突,所以当前线程获取到锁的之后会阻塞其他线程获取该锁。
CAS(无锁操作)是一种乐观锁策略,它假设所有线程访问共享资源的时候不会出现冲突,所以出现冲突时就不会阻塞其他线程的操作,而是重试当前操作直到没有冲突为止。
如下代码,目的是启动 10 个线程,每个线程将 a 累加 1000 次,最终得到 a=10000。
public class CASTest {
public int a = 0;
public void increase() {
a++;
}
public static void main(String[] args) {
final CASTest test = new CASTest();
for (int i = 0; i < 10; i++) {
new Thread() {
public void run() {
for (int j = 0; j < 1000; j++)
test.increase();
};
}.start();
}
while (Thread.activeCount() > 1) {
// 保证前面的线程都执行完
Thread.yield();
}
System.out.println(test.a);
}
}
结果:每次运行结果都小于 10000。
原因分析:
当线程 1 将 a 加到 2 时,a=2 刷新到主内存; 线程 2 执行增加运算时,到主内存读取 a=2,此时线程 3 也要执行增加运算,也到主内存中读取到 a=2; 线程 2 和线程 3 执行的都是 a=2+1,将 a=3 刷新到主内存。 相当于两次加 1 运算只将 a 增加了 1,也就是说存在执行了多次加 1 运算却只是将 a 增加 1 的情况,所以 10000 次加 1 运算,得到的结果会小于 10000。
原子性问题,解决方案 synchronized 和 CAS。
解决方案一:synchronized 加锁
public synchronized void increase() {
a++;
}
通过 synchronized 加锁之后,每次只能有一个线程访问 increase()方法,能够保证最终得到 10000。但是 synchronized 加锁是个重量级操作,程序执行效率很低。
解决方案二:CAS
public AtomicInteger a = new AtomicInteger();
public void increase() {
a.getAndIncrement();
}
利用 CAS,保证 a=a+1 是原子性操作,最终得到结果 10000。
探究 CAS 原理,其实就是探究上个例子中 a.getAndIncrement()如何保证 a=a+1 是原子性操作,先通过源码看下。
AtomicInteger 类结构
public class AtomicInteger extends Number implements java.io.Serializable {
// 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;
}
a.getAndIncrement()的实现如下
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
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;
}
getIntVolatile(var1, var2)
:根据对象 var1 和对象中该变量地址 var2,获取变量的值 var5。
this.compareAndSwapInt(var1, var2, var5, var5 + var4);
:
可见,getAndIncrement()的原子性是通过 compareAndSwapInt()中的第二步比较和替换保证的,那么 compareAndSwapInt()又是怎么保证原子性的呢?
compareAndSwapInt 方法是 JNI(Java Native InterfaceJAVA 本地调用),java 通过 C 来调用 CPU 底层指令实现的。
compareAndSwapInt 方法中的比较替换操作之前插入一个 lock 前缀指令,这个指令能过确保后续操作的原子性。
lock 前缀指令确保后续指令执行的原子性:
在Pentium及之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其它处理器暂时无法通过总线访问内存,很显然,这个开销很大。
在新的处理器中,Intel使用缓存锁定来保证指令执行的原子性,缓存锁定将大大降低lock前缀指令的执行开销。
CPU 提供了两种方法来实现多处理器的原子操作:总线加锁和缓存加锁。
CAS 需要检查操作值有没有发生改变,如果没有发生改变则更新。但是存在这样一种情况:如果一个值原来是 A,变成了 B,然后又变成了 A,那么在 CAS 检查的时候会认为没有改变,但是实质上它已经发生了改变,这就是 ABA 问题。
解决方案可以沿袭数据库中常用的乐观锁方式,添加一个版本号可以解决。原来的变化路径 A->B->A 就变成了 1A->2B->3A。
在 java 1.5 后的 atomic 包中提供了 AtomicStampedReference 来解决 ABA 问题,解决思路就是这样的。
使用 CAS 时非阻塞同步,也就是说不会将线程挂起,会自旋(无非就是一个死循环)进行下一次尝试,如果自旋 CAS 长时间地不成功,则会给 CPU 带来非常大的开销。
优化:限制 CAS 自旋的次数,例如 BlockingQueue 的 SynchronousQueue。
当对一个共享变量执行操作时 CAS 能保证其原子性,如果对多个共享变量进行操作,CAS 就不能保证其原子性。
解决方案:把多个变量整成一个变量
在 J.U.C 下的 atomic 包提供了一系列原子操作类。
AtomicInteger、AtomicLong、AtomicBoolean
以 AtomicInteger 为例总结一下常用的方法:
addAndGet(int delta) :以原子方式将输入的数值delta与实例中原本的值相加,并返回最后的结果;
incrementAndGet() :以原子的方式将实例中的原值进行加1操作,并返回最终相加后的结果;
getAndSet(int newValue):将实例中的值更新为新值newValue,并返回旧值;
getAndIncrement():以原子的方式将实例中的原值加1,返回的是自增前的旧值;
用法:
public class AtomicDemo {
private static AtomicInteger atomicInteger = new AtomicInteger(1);
public static void main(String[] args) {
System.out.println(atomicInteger.getAndIncrement());
System.out.println(atomicInteger.get());
}
}
输出结果:
1
2
AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray(引用类型数组)。
以 AtomicIntegerArray 来总结下常用的方法:
addAndGet(int i, int delta):以原子更新的方式将数组中索引为i的元素与输入值delta相加;
getAndIncrement(int i):以原子更新的方式将数组中索引为i的元素自增加1;
compareAndSet(int i, int expect, int update):将数组中索引为i的位置的元素进行更新;
用法:
AtomicIntegerArray 与 AtomicInteger 的方法基本一致,只不过在 AtomicIntegerArray 的方法中会多一个指定数组索引位 i。
通过 getAndAdd 方法将位置为 1 的元素加 5,从结果可以看出索引为 1 的元素变成了 7,该方法返回的也是相加之前的数为 2。
public class AtomicDemo {
private static int[] value = new int[]{1, 2, 3};
private static AtomicIntegerArray integerArray = new AtomicIntegerArray(value);
public static void main(String[] args) {
//对数组中索引为1的位置的元素加5
int result = integerArray.getAndAdd(1, 5);
System.out.println(integerArray.get(1));
System.out.println(result);
}
}
输出结果:
7
2
AtomicReference
用法:
public class AtomicDemo {
private static AtomicReference<User> reference = new AtomicReference<>();
public static void main(String[] args) {
User user1 = new User("a", 1);
reference.set(user1);
User user2 = new User("b",2);
User user = reference.getAndSet(user2);
System.out.println(user);
System.out.println(reference.get());
}
static class User {
private String userName;
private int age;
public User(String userName, int age) {
this.userName = userName;
this.age = age;
}
@Override
public String toString() {
return "User{" +
"userName='" + userName + '\'' +
", age=" + age +
'}';
}
}
}
输出结果:
User{userName='a', age=1}
User{userName='b', age=2}
如果需要更新对象的某个字段,并在多线程的情况下,能够保证线程安全,atomic 同样也提供了相应的原子操作类:
AtomicIntegeFieldUpdater:原子更新整型字段类;
AtomicLongFieldUpdater:原子更新长整型字段类;
AtomicStampedReference:原子更新引用类型,这种更新方式会带有版本号。解决CAS的ABA问题。
用法:
public class AtomicDemo {
private static AtomicIntegerFieldUpdater updater = AtomicIntegerFieldUpdater.newUpdater(User.class,"age");
public static void main(String[] args) {
User user = new User("a", 1);
int oldValue = updater.getAndAdd(user, 5);
System.out.println(oldValue);
System.out.println(updater.get(user));
}
static class User {
private String userName;
public volatile int age;
public User(String userName, int age) {
this.userName = userName;
this.age = age;
}
@Override
public String toString() {
return "User{" +
"userName='" + userName + '\'' +
", age=" + age +
'}';
}
}
}
输出结果:
1
6
CAS 即比较和替换,可以高效的解决原子性问题。
CAS 原子操作原理:使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替换当前变量的值。
Java 中的 CAS:atomic 包下原子操作类,如 AtomicInteger 常用于修饰共享变量来保证原子性。