Java多线程编程基础二(CAS)

文章引用:

1 http://www.jb51.net/article/86192.htm

2 https://blog.csdn.net/u014034854/article/details/49310523

3 http://ifeve.com/compare-and-swap/

4 https://blog.csdn.net/ls5718/article/details/52563959

5 https://www.jianshu.com/p/aaa272d65579


在JDK 5之前Java语言是靠synchronized关键字保证同步的,这会导致有锁

锁机制存在以下问题:

  1. 在多线程竞争下,加锁、释放锁会导致比较多的上下文切换和调度延时,引起性能问题。
  2. 一个线程持有锁会导致其它所有需要此锁的线程挂起。
  3. 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能风险。

volatile是不错的机制,但是volatile不能保证原子性。因此对于同步最终还是要回到锁机制上来。

独占锁是一种悲观锁,synchronized就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁用到的机制就是CAS,Compare and Swap。

一、CAS

CAS,compare and swap的缩写,中文翻译成比较并交换。

我们都知道,在java语言之前,并发就已经广泛存在并在服务器领域得到了大量的应用。所以硬件厂商老早就在芯片中加入了大量直至并发操作的原语,从而在硬件层面提升效率。在intel的CPU中,使用cmpxchg指令。

在Java发展初期,java语言是不能够利用硬件提供的这些便利来提升系统的性能的。而随着java不断的发展,Java本地方法(JNI)的出现,使得java程序越过JVM直接调用本地方法提供了一种便捷的方式,因而java在并发的手段上也多了起来。而在Doug Lea提供的cucurenct包中,CAS理论是它实现整个java包的基石。

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

通常将 CAS 用于同步的方式是从地址 V 读取值 A,执行多步计算来获得新 值 B,然后使用 CAS 将 V 的值从 A 改为 B。如果 V 处的值尚未同时更改,则 CAS 操作成功。

类似于 CAS 的指令允许算法执行读-修改-写操作,而无需害怕其他线程同时 修改变量,因为如果其他线程修改变量,那么 CAS 会检测它(并失败),算法 可以对该操作重新计算。

CAS的目的

利用CPU的CAS指令,同时借助JNI来完成Java的非阻塞算法。其它原子操作都是利用类似的特性完成的。而整个J.U.C都是建立在CAS之上的,因此对于synchronized阻塞算法,J.U.C在性能上有了很大的提升。

二、CAS存在的问题

CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题:

  1. ABA问题
  2. 循环时间长开销大
  3. 只能保证一个共享变量的原子操作

1、ABA问题

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

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

关于ABA问题参考文档: http://blog.hesey.net/2011/09/resolve-aba-by-atomicstampedreference.html

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操作。

三、CAS和synchronized适用场景

1、对于资源竞争较少的情况,使用synchronized同步锁进行线程阻塞和唤醒切换以及用户态内核态间的切换操作额外浪费消耗cpu资源;而CAS基于硬件实现,不需要进入内核,不需要切换线程,操作自旋几率较少,因此可以获得更高的性能。

2、对于资源竞争严重的情况,CAS自旋的概率会比较大,从而浪费更多的CPU资源,效率低于synchronized。以java.util.concurrent.atomic包中AtomicInteger类为例,首先说明一下我们最经常的做法以及改进的做法:

/**
 * <p>
 * Title:LockBemo
 * </p>
 * <p>
 * Description:在了解CAS之前,我们先来探讨一下如何实现自增(即i++操作):为保证它线程安全的,我们通常会采用volatile+
 * synchronized方法来保证其安全性,如下
 * </p>
 * 
 * @date 2018年4月24日 下午2:01:18
 */
class LockBemo {

    private volatile int value;

    public synchronized int incrementAndGet() {
        return ++value;
    }

}

/**
 * <p>
 * Title:AtomicDemo
 * </p>
 * <p>
 * Description:但实际上,我们知道有一种更加简便的方法:使用AtomicInteger原子类实现
 * </p>
 * 
 * @date 2018年4月24日 下午2:01:31
 */
class AtomicDemo {

    private AtomicInteger atomicInteger = new AtomicInteger();

    public int incrementAndGet() {
        return atomicInteger.incrementAndGet();
    }

}

/**
 * <p>
 * Title:AtomicTest
 * </p>
 * <p>
 * Description:我们知道它在自增时,调用的本身的incrementAndGet()方法,那么我们就来探究下这个方法是如何保证线程安全性的:
 * </p>
 * 
 * @date 2018年4月24日 下午2:01:46
 */
public class AtomicTest {

    private volatile int value;

    private AtomicInteger locked = new AtomicInteger();

    public final int incrementAndGet() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next)) {
                return next;
            }
        }
    }

    public final int get() {
        return value;
    }

    public final boolean compareAndSet(int expect, int update) {
        return locked.compareAndSet(expect, update);
    }

}

如果incrementAndGet(current, next)方法成功执行,则直接返回;如果线程竞争激烈,导致incrementAndGet(current, next)方法一直不能成功执行,则会一直循环等待,直到耗尽cpu分配给该线程的时间片,从而大幅降低效率。

模拟实现一个CAS来加深对其操作的理解:

package collection.cas;

/**
 * <p>
 * Title:SimulatedCAS
 * </p>
 * <p>
 * Description:模拟实现一个CAS来加深对其操作的理解
 * </p>
 * 
 * @date 2018年4月24日 下午2:07:05
 */
public class SimulatedCAS {

    private int value;

    public synchronized int get() {
        return value;
    }

    public synchronized int compareAndSwap(int expectedValue, int newValue) {
        int oldValue = value;
        if (oldValue == expectedValue) {
            value = newValue;
        }
        return oldValue;
    }

    public synchronized boolean compareAndSet(int expectedValue, int newValue) {
        return (expectedValue == compareAndSwap(expectedValue, newValue));
    }

}

3 CAS错误的使用场景

package collection.cas;

import java.util.concurrent.atomic.AtomicInteger;

/**
 * <p>
 * Title:WrongCasDemo
 * </p>
 * <p>
 * Description:CAS错误的使用场景
 * </p>
 * 
 * @date 2018年4月24日 下午2:26:27
 */
public class CasDemo {

    private final int THREAD_NUM = 1000;

    private final int MAX_VALUE = 20000000;

    private AtomicInteger atomicInteger = new AtomicInteger(0);

    private int syncI = 0;

    public void casAdd() throws InterruptedException {
        long begin = System.currentTimeMillis();
        Thread[] threads = new Thread[THREAD_NUM];
        for (int i = 0; i < THREAD_NUM; i++) {
            threads[i] = new Thread(new Runnable() {

                @Override
                public void run() {
                    while (atomicInteger.get() < MAX_VALUE) {
                        atomicInteger.getAndIncrement();
                    }
                }
            });

            threads[i].start();
        }

        for (int j = 0; j < THREAD_NUM; j++) {
            threads[j].join();
        }

        System.out.println("CAS costs time:" + (System.currentTimeMillis() - begin));
    }

    public void syncAdd() throws InterruptedException {
        long begin = System.currentTimeMillis();
        Thread[] threads = new Thread[THREAD_NUM];
        for (int i = 0; i < THREAD_NUM; i++) {
            threads[i] = new Thread(new Runnable() {

                @Override
                public void run() {
                    while (syncI < MAX_VALUE) {
                        synchronized ("syncI") {
                            ++syncI;
                        }
                    }
                }
            });

            threads[i].start();
        }

        for (int j = 0; j < THREAD_NUM; j++) {
            threads[j].join();
        }

        System.out.println("sync costs time:" + (System.currentTimeMillis() - begin));
    }

}

在我的双核cpu上运行,结果如下:
这里写图片描述

            while (atomicInteger.get() < MAX_VALUE) {
                atomicInteger.getAndIncrement();
            }

的操作是一个耗时非常少的操作,上面代码执行完之后会立刻进入循环,继续执行,从而导致线程冲突严重。

4 改进的CAS使用场景

为了解决上述问题,只需要让每一次循环执行的时间变长,即可以大幅减少线程冲突。修改代码如下:

package collection.cas;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.lang.Thread.UncaughtExceptionHandler;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * <p>
 * Title:CorrectCASDemo
 * </p>
 * <p>
 * Description:改进WrongCASDemo
 * </p>
 * 
 * @date 2018年4月24日 下午2:29:22
 */
public class CorrectCASDemo {

    private final int THREAD_NUM = 10;

    private final int MAX_VALUE = 20000000;

    private AtomicInteger atomicInteger = new AtomicInteger(0);

    private int syncI = 0;

    private String path = "/Users/test.txt";

    public static void main(String[] args) {
        CorrectCASDemo casDemo = new CorrectCASDemo();
        try {
            casDemo.casAdd();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void casAdd() throws InterruptedException {
        long begin = System.currentTimeMillis();
        Thread[] threads = new Thread[THREAD_NUM];
        for (int i = 0; i < THREAD_NUM; i++) {
            threads[i] = new Thread(new Runnable() {

                @Override
                public void run() {
                    while (atomicInteger.get() < MAX_VALUE) {
                        atomicInteger.getAndIncrement();

                        // 阻塞
                        try (InputStream in = new FileInputStream(new File(path))) {
                            while (in.read() != -1) {
                            }
                        } catch (IOException e) {
                            throw new RuntimeException(e);
                        }
                    }
                }
            });

            threads[i].setName("线程" + i);
            threads[i].setUncaughtExceptionHandler(new UncaughtExceptionHandler() {

                @Override
                public void uncaughtException(Thread t, Throwable e) {
                    System.out.println(t.getName() + "出现了异常");
                    e.printStackTrace();
                }
            });
            threads[i].start();
        }

        for (int j = 0; j < THREAD_NUM; j++) {
            threads[j].join();
        }

        System.out.println("CAS costs time:" + (System.currentTimeMillis() - begin));
    }

    public void syncAdd() throws InterruptedException {
        long begin = System.currentTimeMillis();
        Thread[] threads = new Thread[THREAD_NUM];
        for (int i = 0; i < THREAD_NUM; i++) {
            threads[i] = new Thread(new Runnable() {

                @Override
                public void run() {
                    while (syncI < MAX_VALUE) {
                        synchronized ("syncI") {
                            ++syncI;
                        }

                        // 阻塞
                        try (InputStream in = new FileInputStream(new File(path))) {
                            while (in.read() != -1) {
                            }
                        } catch (IOException e) {
                            throw new RuntimeException(e);
                        }
                    }
                }
            });

            threads[i].setName("线程" + i);
            threads[i].setUncaughtExceptionHandler(new UncaughtExceptionHandler() {

                @Override
                public void uncaughtException(Thread t, Throwable e) {
                    System.out.println("线程:" + t.getName() + "出现了异常");
                    e.printStackTrace();
                }
            });
            threads[i].start();
        }

        for (int j = 0; j < THREAD_NUM; j++) {
            threads[j].join();
        }

        System.out.println("sync costs time:" + (System.currentTimeMillis() - begin));
    }

}

在while循环中,增加了一个读取文件内容的操作,该操作大概需要耗时40ms,从而可以减少线程冲突。测试结果如下:
这里写图片描述
可见在资源冲突比较小的情况下,采用CAS方式和synchronized同步效率差不多。为什么CAS相比synchronized没有获得更高的性能呢?

测试使用的jdk为1.7,而从jdk1.6开始,对锁的实现引入了大量的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减少锁操作的开销。而其中自旋锁的原理,类似于CAS自旋,甚至比CAS自旋更为优化。具体内容请参考 深入JVM锁机制1-synchronized。

5 总结

  • 使用CAS在线程冲突严重时,会大幅降低程序性能;CAS只适合于线程冲突较少的情况使用。
  • synchronized在jdk1.6之后,已经改进优化。synchronized的底层实现主要依靠Lock-Free的队列,基本思路是自旋后阻塞,竞争切换后继续竞争锁,稍微牺牲了公平性,但获得了高吞吐量。在线程冲突较少的情况下,可以获得和CAS类似的性能;而线程冲突严重的情况下,性能远高于CAS。

猜你喜欢

转载自blog.csdn.net/u013412772/article/details/80062228
今日推荐