之前在分析volatile关键字的时候有提到了volatile不能保证原子性,而且书上提到可以通过加锁(synchronized和JUC中的原子类)来保证原子性。这边炒一波冷饭,对于synchronized我也在上篇讨论过它在同一时间只能允许一个线程去访问一段特定的代码,从而保护一些变量或者数据不会被其他线程所修改来实现原子性。至于为什么,上篇中也有解释这里就不再赘述了。对于Java JUC包中的原子类,它们的底层就是通过CAS来实现对变量的原子操作。在平常看书或者看其他大神博客的过程中,CAS这个概念经常会被提起,既然不明白为什么不进行了解一番呢?
CAS(compare and swap)是什么?
(宏观来说)在计算机科学中,比较和交换(CAS)是多线程中用于实现同步的原子指令(来自Wikipedia)。(回到现实来说)CAS中包含三个参数,内存中的值V、期望值A和要修改的值B。 拿期望值A与内存中的值V进行比较,如果相同,则用B替换内存中的值V,否则什么都不做。如果单线程的情况下,所有的操作都是一个线程来完成,就不需要考虑其他的问题。但是如果是多线程的话,同时访问内存就会有不一样的情况。
如果A、B线程能自觉的排队去访问主内存中的值就没这篇文章什么事了。 在某一时刻,线程A、B同时想去修改主内存中的共享变量。根据CAS原则有3个步骤要走,读值比较替换。它们同时读到了内存中的值V,并将值以期望值A的形式存到自己的工作内存(根据Java内存模型,每个线程有自己的工作内存来对变量进行操作)中。接着就是线程进行写值操作的时候了,内存中的值只有一个,若想操作它总要有个先来后到。若A线程眼疾手快抢占先机(假设这时并没有第三个线程改变内存中的值V),用期望值A与内存中的值V比较,发现一致然后将自己的更新值B替换主内存的值V。然后线程B来了,在做比较的时候发现自己的期望值A与内存中的值V不相等,只能悻悻而归。
CAS的优点?
上面说到,volatile关键字不能保证原子性,开发过程中可以通过加锁的形式来保证。这里要鞭尸一波synchronized关键字,多线程的情况下,它能保证原子性。但是当一个线程获得锁后,其他线程就被挂起,当获得锁的线程释放锁后其他线程才能重新去竞争锁。每一次线程的阻塞和唤醒都需要操作系统的介入,需要在用户态和和内核态之间切换,而这种切换会消耗大量的系统资源。而使用CAS基于指令来实现,不需要进入内核或者切换线程,所以性能上会比synchronized关键字要好。
CAS的缺陷?
上面讲了它的有点,现在嘴臭一波。
- CPU开销大
在并发量大的情况下,CAS自旋的概率会变大。若多个线程反复的去尝试更新一个变量却一直不成功,会一直循环等待重试,直到耗尽CPU分配给该线程的时间片,对CPU造成巨大的压力。 - 不能保证代码块的一致性
CAS机制只能保证一个变量的原子性操作,多个变量时还是只能使用synchronized关键字。 - ABA问题
在线程读取变量和替换变量值的过程中存在一定的时间差,在这个时间差中内存中的变量值可能从A变成B再变成A,当前线程无法判断当前V值是否发生变化。对于如何解决这个ABA的问题,《并发编程艺术》中给出为每一个变量添加标识,一旦对变量的值修改后,对标识也进行操作。在每次CAS比较的过程中,同时去比较标识的值来判断当前的V值是否发生变化。Java提供了AtomicStampedReference来解决ABA的问题,它通过创建Pair内部对象来维护标记的引用。源码部分也还好理解的, 当前的引用和标识与预期的引用和标识相等,并且更新后的引用和标志与当前的引用和标志相等则直接返回true,否则通过生成一个新的Pair对象与当前Pair进行CAS替换。
public class AtomicStampedReference<V> {
private static class Pair<T> {
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
}
复制代码
CAS具体的使用场景?
这里也就拿上面说到的JUC下原子操作类来举例子。原子操作类是在java.util.concurrent.atomic包下一系列以Atomic开头的包装类。AtomicInteger也可以保证共享变量在多线程环境下是线程安全的。
AtomicInteger源码
public class AtomicInteger extends Number implements java.io.Serializable {
private static final Unsafe unsafe = Unsafe.getUnsafe();
// value成员属性的内存地址相对于对象内存地址的偏移量
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;
public final int get() {
return value;
}
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
}
复制代码
在调试AtomicInteger源码时发现,不同的AtomicInteger的对象中valueOffset的值都是相同的,由此产生疑问,最后查阅资料发现valueOffSet为value成员属性的内存地址相对于对象内存地址的偏移量。
Unsafe是CAS的核心类,Java方法无法直接访问底层的系统,需要通过本地方法(native)来访问。Unsafe可以直接操作特定的内存数据。value值用volatile关键字修饰,保证了变量在多线程操作中的内存可见性。
马后炮
对于线程、锁这边的知识点很多都是互相关联的,很难对这边这么多的概念进行一个系统的罗列。最近写的几篇都是偏理论的而且大部分还是书上的内容,写的还是比较虚,不过每次在推敲着写的时候会发现很多不起眼的其他相关知识,如果感觉有用还是会贴到文章的句子中,还是希望继续写下去的时候能多有一些自己的想法。
最后还是那句话,学习的最终目的并不是为了面试,面试只是一个激励学习的动机。把握面试题,享受学习新知识的乐趣。
参考:
《并发编程的艺术》