在我的另一篇文章中,我对CAS的原理,优缺点,适用场景进行了分析,可以参见这一篇文章乐观锁实现之CAS,在这篇文章中,我简要的讲一下CAS操作在我们多线程编程中怎么使用,以及为什么要用:
话不多说,直接上代码(Talk is cheap. Show me the code)
package cn.yqh.interview;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author 袁
* @create 2019/8/24-9:19
*/
public class VolatileTest {
private static volatile int count;
private static CountDownLatch cdl = new CountDownLatch(20);
// private static AtomicInteger atomicInteger = new AtomicInteger(0);
public static void increase() {
count++;
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 20; i++) {
new Thread(new Runnable() {
public void run() {
for (int i1 = 0; i1 < 10000; i1++) {
increase();
}
cdl.countDown();
}
}).start();
}
cdl.await();
System.out.println("count最终结果:" + count);
}
}
上面的代码中,创建了20个线程,每个线程都会循环调用10000次increase方法,其中的CountDownLatch 是一个很常用的多线程同步工具类,关于CountDownLatch 的使用可以参见我的另一篇文章:java多线程编程之CountDownLatch类的使用,但是我们运行这个main方法发现,得到的结果不是200000,而永远比200000要小,这是为什么呢:
通过分析字节码我们知道,因为volatile只能保证可见性,无法保证原子性,而自增操作并不是一个原子操作(原因见我的另一篇问文章:count++到底是不是原子操作),在并发的情况下,getstatic指令可能取到另一个线程正在处理的count,并且putstatic指令可能把较小的count值同步回主内存之中,导致我们每次都无法获得想要的结果。那么,应该怎么解决这个问题呢?
首先我们想到的是用synchronized来修饰increase方法。
使用synchronized修饰后,每次运行increase方法,必须先拿到类的锁,而且当一个线程拿到这个锁的时候,另一个线程值能够发生阻塞,所以就可以防止getstatic指令可能取到另一个线程正在处理的count,并且putstatic指令可能把较小的count值同步回主内存之中的问题,因此是肯定能得到正确的结果。但是,我们知道,每次自增都进行加锁,性能可能会稍微差了点,有更好的方案吗?
答案当然是肯定的,那么接下来就是我们的主角,CAS上场了:
我们可以使用Java并发包原子操作类(Atomic开头),例如以下代码。
我们将例子中的代码稍做修改:count改成使用AtomicInteger定义,“count++”改成使用“atomicInteger.getAndIncrement()”,AtomicInteger.getAndIncrement()是原子操作(原子操作下也可以避免getstatic指令可能取到另一个线程正在处理的count,并且putstatic指令可能把较小的count值同步回主内存之中的问题),所以我们就算不在increase方法上加锁,也可以保证多线程下数据的完整祥,因此我们可以确保每次都可以获得正确的结果,并且在性能上有不错的提升。
通过方法调用,我们可以发现,getAndIncrement方法调用getAndAddInt方法,最后调用的是compareAndSwapInt方法,即本文的主角CAS,也就是说,我们 通过使用CAS完成了再多线程之下数据完整性的保证,而没有使用synchronize关键字。故到这里,文章也就应该结束了。
这里扩展一点内容:
CAS之所以可以保证线程安全,是因为CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。
原子指令+unsafe类的帮助定位到内存地址,实现了CAS的线程安全。
一下内容引用自美团的一片文章:https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html
如下源代码释义所示,这部分主要为CAS相关操作的方法。
/**
* CAS
* @param o 包含要修改field的对象
* @param offset 对象中某field的偏移量
* @param expected 期望值
* @param update 更新值
* @return true | false
*/
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update);
public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update);
public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);
什么是CAS? 即比较并替换,实现并发算法时常用到的一种技术。CAS操作包含三个操作数——内存位置、预期原值及新值。执行CAS操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。我们都知道,CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。
典型应用
CAS在java.util.concurrent.atomic相关类、Java AQS、CurrentHashMap等实现上有非常广泛的应用。如下图所示,AtomicInteger的实现中,静态字段valueOffset即为字段value的内存偏移地址,valueOffset的值在AtomicInteger初始化时,在静态代码块中通过Unsafe的objectFieldOffset方法获取。在AtomicInteger中提供的线程安全方法中,通过字段valueOffset的值可以定位到AtomicInteger对象中value的内存地址,从而可以根据CAS实现对value字段的原子操作。
下图为某个AtomicInteger对象自增操作前后的内存示意图,对象的基地址baseAddress=“0x110000”,通过baseAddress+valueOffset得到value的内存地址valueAddress=“0x11000c”;然后通过CAS进行原子性的更新操作,成功则返回,否则继续重试,直到更新成功为止。