非阻塞同步之 CAS

原创
2018/06/23 10:16
阅读数 131

为解决线程安全问题,互斥同步相当于以时间换空间。多线程情况下,只有一个线程可以访问同步代码。这种同步也叫阻塞同步(Blocking Synchronization).

这种同步属于一种悲观并发策略。认为只要不同步,共享数据就会被并发访问。随着硬件指令集的发展,我们可以采用基于冲突检测的乐观并发策略。

先进行操作,如果没有其他线程操作共享数据,就操作成功;否则采取补偿措施,去重试直到成功。这种策略不需要把线程挂起,因此称为非阻塞同步。(Non-Blocking Synchrinization)

要保证两个操作的原子性,需要借助处理器指令来完成。这类指令有:

  1. 测试并设置(test-and-set)

  2. 获取并增加(fetch-and-increment)

  3. 交换(swap)

  4. 比较并交换(compare-and-swap,简称 CAS)

  5. 加载链接、条件存储(load-linked/store-conditional)

CAS 指令需要有 3 个操作数,分别为内存位置(V,变量的内存地址),旧的预期值(A),和新值(B)。CAS 指令执行时,当且仅当 V 的值复合旧的预期值时,处理器使用 B 更新 V 值。否则不更新,上述操作是一个原子操作。

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”这其实和乐观锁的冲突检查+数据更新的原理是一样的。

Java 中的 CAS 操作:

public class CasTest {

    //private Integer count = 0;
    private final AtomicInteger count = new AtomicInteger(0);


    private ThreadPoolExecutor executor = new ThreadPoolExecutor(4, 20,
            10, TimeUnit.MINUTES, new ArrayBlockingQueue<Runnable>(100));

    private void increase() {
        // count++;
        count.incrementAndGet();
    }

    @Test
    public void testMutiThreadAdd() {
        for (int i = 0; i < 5; i++) {
            executor.execute(() -> {
                for (int j = 0; j < 1000; j++) {
                    increase();
                }
            });
        }

        try {
            Thread.sleep(4000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(count);
    }

}

使用 atomicInteger 后,每次都能输出一致的结果。increamentAndGet( ) 通过 CAS 保证了 自增操作的原子性;

CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题。ABA问题,循环时间长开销大和只能保证一个共享变量的原子操作

1.  ABA问题。因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。

Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

2. 循环时间长开销大。自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

3. 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作.

参考: 深入理解Java 虚拟机

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部