CAS机制的的解释和总结

CAS是什么?有什么作用?

CAS(Compare-and-Swap)是比较并交换是旧的预期值和内存地址中的值相等时才会去执行更新操作,CAS操作用来避免阻塞同步

CAS操作,该操作由sun.misc.Unsafe类里面的compareAndSwapInt()和compareAndSwapLong()等几个方法包装提供。

CAS的工作原理?

CAS指令需要有三个操作数,
分别是
内存位置(在Java中可以简单地理解为变量的内存地址,用V表示)、
旧的预期值(用A表示)
准备设置的新值(用B表示)。

CAS指令执行时,只有当变量的预期值A和内存地址V当中的实际值相同时,处理器才会用B更新V的值,否则它就不执行更新。 但是,不管是否更新了V的值,都会返回V的旧值,上述的处理过程是一个原子操作,执行期间不会被其他线程中断。

如何通过CAS操作避免阻塞同步?

测试的代码如代码清单1所示。这段代码里我们曾经通过20个线程自增10000次的操作来证明volatile变量不具备原子性,那么如何才能让它具备原子性呢?
方案之一是把race++操作或increase()方法用同步块包裹起来,这是使用锁的解决方案,如:代码清单2

但是如果改成AtomicInteger原子自增运算 如:代码清单3所示的写法,效率将会提高许多。

代码清单1 volatile的运算

/**
 * volatile变量自增运算测试
 *
 * @author zzm
 */
public class VolatileTest {

    public static volatile int race = 0;

    public static void increase() {
        race++;
    }

    private static final int THREADS_COUNT = 20;

    public static void main(String[] args) {
        Thread[] threads = new Thread[THREADS_COUNT];
        long start = System.currentTimeMillis();
        for (int i = 0; i < THREADS_COUNT; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i++) {
                        increase();
                    }
                }
            });
            threads[i].start();
        }

        // 等待所有累加线程都结束
        while (Thread.activeCount() > 2)
            Thread.yield();

        System.out.println(race);
        System.out.println("耗时:" + (System.currentTimeMillis() - start));
    }
}

执行结果:
186665
耗时:15

代码清单2 synchronized包裹increase()方法的volatile的运算

public class VolatileTest {

    public static volatile int race = 0;

    public synchronized static void increase() {
        race++;
    }

    private static final int THREADS_COUNT = 20;

    public static void main(String[] args) {
        Thread[] threads = new Thread[THREADS_COUNT];
        long start = System.currentTimeMillis();
        for (int i = 0; i < THREADS_COUNT; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i++) {
                        increase();
                    }
                }
            });
            threads[i].start();
        }

        // 等待所有累加线程都结束
        while (Thread.activeCount() > 2)
            Thread.yield();

        System.out.println(race);
        System.out.println("耗时:" + (System.currentTimeMillis() - start));
    }
}

执行结果:
200000
耗时:40

synchronized关键字会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高

尽管JAVA 1.6为synchronized做了优化,增加了从偏向锁到轻量级锁再到重量级锁的过度,但是在最终转变为重量级锁之后,性能仍然比较低。所以面对这种情况,我们就可以使用java中的“原子操作类”

CAS的应用

所谓原子操作类,指的是java.util.concurrent.atomic包下,一系列以Atomic开头的包装类。如AtomicBoolean,AtomicUInteger,AtomicLong。它们分别用于Boolean,Integer,Long类型的原子性操作。

代码清单3 Atomic的原子自增运算

/**
 * Atomic变量自增运算测试
 *
 * @author zzm
 */
public class AtomicTest {

    public static AtomicInteger race = new AtomicInteger(0);

    public static void increase() {
        race.incrementAndGet();
    }

    private static final int THREADS_COUNT = 20;

    public static void main(String[] args) throws Exception {
        Thread[] threads = new Thread[THREADS_COUNT];
        long start = System.currentTimeMillis();
        for (int i = 0; i < THREADS_COUNT; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i++) {
                        increase();
                    }
                }
            });
            threads[i].start();
        }
        while (Thread.activeCount() > 2)
            Thread.yield();

        System.out.println(race);
        System.out.println("耗时:" + (System.currentTimeMillis() - start));
    }
}

执行结果:
200000
耗时:19

使用AtomicInteger代替int后,程序输出了正确的结果,而Atomic操作类的底层正是用到了“CAS机制”
来看一下incrementAndGet()方法的原子性:
这段代码是一个无限循环,也就是CAS的自旋,循环体中做了三件事:

  1. 获取当前值
  2. 当前值+1,计算出目标值
  3. 进行CAS操作,如果成功则跳出循环,如果失败则重复上述步骤

如代码清单4所示。

代码清单4 incrementAndGet()方法的JDK源码

/**
 * Atomically increment by one the current value.
 * @return the updated value
 */
public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}


继续查看compareAndSet()方法的源码如下,使用的是sun.misc.Unsafe中的方法来实现的

代码清单5 compareAndSet()方法的JDK源码


    /**
     * Atomically sets the value to the given updated value
     * if the current value {@code ==} the expected value.
     *
     * @param expect the expected value
     * @param update the new value
     * @return {@code true} if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

从思想上来说,synchronized属于悲观锁,悲观的认为程序中的并发情况严重,所以严防死守,
CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去重试更新。

CAS的缺点?

1) CPU开销过大

在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很到的压力。

2) 不能保证代码块的原子性

CAS机制所保证的知识一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用锁了。
还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。

3) ABA问题

CAS存在一个逻辑漏洞:如果一个变量V初次读取的时候是A值,并且在准备赋值的时候检查到它仍然为A值,那就能说明它的值没有被其他线程改变过了吗?
这是不能的,因为如果在这段期间它的值曾经被改成B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA问题”。

怎么解决ABA问题呢?

ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。
真正要做到严谨的CAS机制,我们在compare阶段不仅要比较期望值A和地址V中的实际值还要比较变量的版本号是否一致

从Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference类就实现了用版本号作比较额CAS机制。
这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

拓展总结:Java中可以通过什么方式来实现原子操作?

在Java中可以通过锁和循环CAS的方式来实现原子操作。
1、循环CAS的方式
JVM中的CAS操作正是利用了处理器提供的CMPXCHG指令实现的。自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止(若不成功则CPU也不放弃时间片,循环发起 CAS 操作,直至成功)。

2、使用锁机制

锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。
JVM内部实现了很多种锁机制,有偏向锁、轻量级锁和互斥锁。
有意思的是除了偏向锁,JVM实现锁的方式都用了循环CAS,即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时候使用循环CAS释放锁。

以下代码实现了一个基于CAS线程安全的计数器方法safeCount和一个非线程安全的计数器count。

public class Counter {
	private AtomicInteger atomicI = new AtomicInteger(0);
    private int i = 0;
    public static void main(String[] args) {
        final Counter cas = new Counter();
        List<Thread> ts = new ArrayList<Thread>(600);
        long start = System.currentTimeMillis();
        for (int j = 0; j < 100; j++) {
            Thread t = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int i = 0; i < 10000; i++) {
                        cas.count();
                        cas.safeCount();
                    }
                }
            });
            ts.add(t);
        }
        for (Thread t : ts) {
            t.start();
        }
    // 等待所有线程执行完成
        for (Thread t : ts) {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("非线程安全计数器:" + cas.i);
        System.out.println("CAS线程安全计数器:" + cas.atomicI.get());
        System.out.println("耗时:" +System.currentTimeMillis() - start);
    }
    /**        * 使用CAS实现线程安全计数器        */
    private void safeCount() {
        for (;;) {
            int i = atomicI.get();
            boolean suc = atomicI.compareAndSet(i, ++i);
            if (suc) {
                break;
            }
        }
    }
    /**
     * 非线程安全计数器
     */
    private void count() {
        i++;
    }
}

执行结果:
非线程安全计数器: 959014
CAS线程安全计数器: 1000000
耗时: 90

CAS在虚拟机中的应用?

对象创建在虚拟机中是非常频繁的行为,即使仅仅修改一个指针所指向的位置,在并发情况下也并不是线程安全的,
可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存的情况。
解决方案:对分配内存空间的动作进行同步处理——实际上虚拟机是采用CAS配上失败重试的方式保证更新操作的原子性

CAS 性能优化

大量的线程同时并发修改一个 AtomicInteger,可能有很多线程会不停的自旋,进入一个无限重复的循环中。这些线程不停地获取值,然后发起 CAS 操作,但是发现这个值被别人改过了,于是再次进入下一个循环,获取值,发起 CAS 操作又失败了,再次进入下一个循环。在大量线程高并发更新 AtomicInteger 的时候,这种问题可能会比较明显,导致大量线程空循环,自旋转,性能和效率都不是特别好。那么如何优化呢?

Java8 有一个新的类,LongAdder,它就是尝试使用分段 CAS 以及自动分段迁移的方式来大幅度提升多线程高并发执行 CAS 操作的性能,这个类具体是如何优化性能的呢?如图:
在这里插入图片描述

参考:深入理解java虚拟机 —— 周志明
文章:什么是CAS机制?
文章:什么是乐观锁,什么是悲观锁

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值