面试必问CAS,你懂了吗?

23 篇文章 0 订阅

在我的另一篇文章中,我对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进行原子性的更新操作,成功则返回,否则继续重试,直到更新成功为止。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值