1 CAS介绍
CAS,Compare And Swap,即比较并交换。同步组件中大量使用CAS技术实现了Java多线程的并发操作。整个AQS同步组件、Atomic原子类操作等等都是以CAS实现的,甚至ConcurrentHashMap在1.8的版本中也调整为了CAS+Synchronized。可以说CAS是整个JUC的基石。
2 CAS原理剖析
CAS是如何提高性能的呢?
CAS的思想很简单:三个参数,一个当前内存值V、旧的预期值A、即将更新的值B,当且仅当旧的预期值A和内存值V相同时,将内存值修改为B并返回true,否则什么都不做,并返回false。如果CAS操作失败,通过自旋的方式等待并再次尝试,直到成功。
CAS在 先比较后修改 这个CAS过程中,根本没有获取锁,释放锁的操作,是硬件层面的原子操作,跟JMM内存模型没有关系。大家可以理解为直接使用其他的语言,在JVM虚拟机之外直接操作计算机硬件,正因为如此,对比synchronized的同步,少了很多的逻辑步骤,使得性能大为提高。
JUC下的atomic类都是通过CAS来实现的,下面就是一个AtomicInteger原子操作类的例子,在其中使用了Unsafe unsafe = Unsafe.getUnsafe()。Unsafe 是CAS的核心类,它提供了硬件级别的原子操作。
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); }
}
//操作的值也进行了volatile修饰,保证内存可见性
private volatile int value;
继续查看AtomicInteger的addAndGet()方法:
public final int addAndGet(int delta) {
return unsafe.getAndAddInt(this, valueOffset, delta) + delta;
}
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;
}
其内部调用unsafe的getAndAddInt方法,查看看compareAndSwapInt方法,该方法为native方法,有四个参数,分别代表:对象、对象的地址、预期值、修改值。:
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
Unsafe 是一个比较危险的类,主要是用于执行低级别、不安全的方法集合。尽管这个类和所有的方法都是公开的(public),但是这个类的使用仍然受限,你无法在自己的java程序中直接使用该类,因为只有授信的代码才能获得该类的实例。可是为什么Unsafe的native方法就可以保证是原子操作呢?
3 native关键词
前面提到了sun.misc.Unsafe这个类,里面的方法使用native关键词声明本地方法,为什么要用native?
Java无法直接访问底层操作系统,但有能力调用其他语言编写的函数or方法,是通过JNI(Java Native Interfface)实现。使用时,通过native关键字告诉JVM这个方法是在外部定义的。但JVM也不知道去哪找这个原生方法,此时需要通过javah命令生成.h文件。
示例步骤(c语言为例):
javac生成.class文件,比如javac NativePeer.java
javah生成.h文件,比如javah NativePeer
编写c语言文件,在其中include进上一步生成的.h文件,然后实现其中声明而未实现的函数
生成dll共享库,然后Java程序load库,调用即可native可以和任何除abstract外的关键字连用,这也说明了这些方法是有实体的,并且能够和其他Java方法一样,拥有各种Java的特性。native方法有效地扩充了jvm,实际上我们所用的很多代码已经涉及到这种方法了,通过非常简洁的接口帮我们实现Java以外的工作。
native优势:
很多层次上用Java去实现是很麻烦的,而且Java解释执行的效率也差了c语言啥的很多,纯Java实现可能会导致效率不达标,或者可读性奇差。
Java毕竟不是一个完整的系统,它经常需要一些底层的支持,通过JNI和native method我们就可以实现jre与底层的交互,得到强大的底层操作系统的支持,使用一些Java本身没有封装的操作系统的特性。
4 多CPU的CAS处理
CAS可以保证一次的读-改-写操作是原子操作,在单处理器上该操作容易实现,但是在多处理器上实现就有点儿复杂了。CPU提供了两种方法来实现多处理器的原子操作:总线加锁或者缓存加锁。
总线加锁:总线加锁就是就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占使用共享内存。但是这种处理方式显得有点儿霸道,不厚道,他把CPU和内存之间的通信锁住了,在锁定期间,其他处理器都不能其他内存地址的数据,其开销有点儿大。
缓存加锁:其实针对于上面那种情况我们只需要保证在同一时刻对某个内存地址的操作是原子性的即可。缓存加锁就是缓存在内存区域的数据如果在加锁期间,当它执行锁操作写回内存时,处理器不在输出LOCK#信号,而是修改内部的内存地址,利用缓存一致性协议来保证原子性。缓存一致性机制可以保证同一个内存区域的数据仅能被一个处理器修改,也就是说当CPU1修改缓存行中的i时使用缓存锁定,那么CPU2就不能同时缓存了i的缓存行。
5 CAS缺陷
CAS虽然高效地解决了原子操作,但是还是存在一些缺陷的,主要表现在三个方法:循环时间太长、只能保证一个共享变量原子操作、ABA问题。
循环时间太长如果CAS一直不成功呢?这种情况绝对有可能发生,如果自旋CAS长时间地不成功,则会给CPU带来非常大的开销。在JUC中有些地方就限制了CAS自旋的次数,例如BlockingQueue的SynchronousQueue。
只能保证一个共享变量原子操作看了CAS的实现就知道这只能针对一个共享变量,如果是多个共享变量就只能使用锁了。
ABA问题CAS需要检查操作值有没有发生改变,如果没有发生改变则更新。但是存在这样一种情况:如果一个值原来是A,变成了B,然后又变成了A,那么在CAS检查的时候会发现没有改变,但是实质上它已经发生了改变,这就是所谓的ABA问题。对于ABA问题其解决方案是加上版本号,即在每个变量都加上一个版本号,每次改变时加1,即A —> B —> A,变成1A —> 2B —> 3A。CAS的ABA隐患问题,Java提供了AtomicStampedReference来解决。AtomicStampedReference通过包装[E,Integer]的元组来对对象标记版本戳stamp,从而避免ABA问题。对于上面的案例应该线程1会失败。
下面我们将通过一个例子可以可以看到AtomicStampedReference和AtomicInteger的区别。我们定义两个线程,线程1负责将100 —> 110 —> 100,线程2执行 100 —>120,看两者之间的区别。
public class Demo4ABA {
private static AtomicInteger ai = new AtomicInteger(100);
private static AtomicStampedReference air = new AtomicStampedReference(100, 1);
//ABA问题演示:
//1. 线程1先对数据进行修改 A-B-A过程
//2. 线程2也对数据进行修改 A-C的过程
public static void main(String[] args) throws InterruptedException {
// AtomicInteger可以看到不会有任何限制随便改
// 线程2修改的时候也不可能知道要A-C 的时候,A是原来的A还是修改之后的A
Thread at1 = new Thread(new Runnable() {
public void run() {
ai.compareAndSet(100, 110);
ai.compareAndSet(110, 100);
}
});
Thread at2 = new Thread(new Runnable() {
public void run() {
try {
//为了让线程1先执行完,等一会
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("AtomicInteger:" + ai.compareAndSet(100, 120));
System.out.println("执行结果:" + ai.get());
}
});
at1.start();
at2.start();
//顺序执行,AtomicInteger案例先执行
at1.join();
at2.join();
//AtomicStampedReference可以看到每次修改都需要设置标识Stamp,相当于进行了1A-2B-3A的操作
//线程2进行操作的时候,虽然数值都一样,但是可以根据标识很容易的知道A是以前的1A,还是现在的3A
Thread tsf1 = new Thread(new Runnable() {
public void run() {
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 预期引用:100,更新后的引用:110,预期标识getStamp() 更新后的标识getStamp() + 1
air.compareAndSet(100, 110, air.getStamp(), air.getStamp() + 1);
air.compareAndSet(110, 100, air.getStamp(), air.getStamp() + 1);
}
});
Thread tsf2 = new Thread(new Runnable() {
public void run() {
//tsf2先获取stamp,导致预期时间戳不一致
int stamp = air.getStamp();
try {
TimeUnit.MILLISECONDS.sleep(100); //线程tsf1执行完
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("AtomicStampedReference:" + air.compareAndSet(100, 120, stamp, stamp + 1));
int[] stampArr = {stamp + 1};
System.out.println("执行结果:" + air.get(stampArr));
}
});
tsf1.start();
tsf2.start();
}
运行结果充分展示了AtomicInteger的ABA问题和AtomicStampedReference解决ABA问题。