关于java:Java多线程之CAS

11次阅读

共计 3907 个字符,预计需要花费 10 分钟才能阅读完成。

CAS (Compare and Swap)

CAS 字面意思为比拟并替换.CAS 有 3 个操作数,别离是:内存值 M,期望值 E,更新值 U。当且仅当内存值 M 和期望值 E 相等时,将内存值 M 批改为 U,否则什么都不做。

1.CAS 的利用场景

CAS 只实用于线程抵触较少的状况

CAS 的典型利用场景是:

  • 原子类
  • 自旋锁

1.1 原子类

原子类是 CAS 在 Java 中最典型的利用。

咱们先来看一个常见的代码片段。

if(a==b) {a++;}

如果 a++ 执行前,a 的值被批改了怎么办?还能失去预期值吗?呈现该问题的起因是在并发环境下,以上代码片段不是原子操作,随时可能被其余线程所篡改。

解决这种问题的最经典形式是利用原子类的 incrementAndGet 办法。

public class AtomicIntegerDemo {public static void main(String[] args) throws InterruptedException {ExecutorService executorService = Executors.newFixedThreadPool(3);
        final AtomicInteger count = new AtomicInteger(0);
        for (int i = 0; i < 10; i++) {executorService.execute(new Runnable() {
                @Override
                public void run() {count.incrementAndGet();
                }
            });
        }

        executorService.shutdown();
        executorService.awaitTermination(3, TimeUnit.SECONDS);
        System.out.println("Final Count is :" + count.get());
    }

}

J.U.C 包中提供了 AtomicBooleanAtomicIntegerAtomicLong 别离针对 BooleanIntegerLong 执行原子操作,操作和下面的示例大体类似,不做赘述。

1.2 自旋锁

利用原子类(实质上是 CAS),能够实现自旋锁。

所谓自旋锁,是指线程重复查看锁变量是否可用,直到胜利为止。因为线程在这一过程中放弃执行,因而是一种忙期待。一旦获取了自旋锁,线程会始终放弃该锁,直至显式开释自旋锁。

示例:非线程平安示例

public class AtomicReferenceDemo {

    private static int ticket = 10;

    public static void main(String[] args) {ExecutorService executorService = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 5; i++) {executorService.execute(new MyThread());
        }
        executorService.shutdown();}

    static class MyThread implements Runnable {

        @Override
        public void run() {while (ticket > 0) {System.out.println(Thread.currentThread().getName() + "卖出了第" + ticket + "张票");
                ticket--;
            }
        }

    }

}

输入后果:

pool-1-thread-2 卖出了第 10 张票
pool-1-thread-1 卖出了第 10 张票
pool-1-thread-3 卖出了第 10 张票
pool-1-thread-1 卖出了第 8 张票
pool-1-thread-2 卖出了第 9 张票
pool-1-thread-1 卖出了第 6 张票
pool-1-thread-3 卖出了第 7 张票
pool-1-thread-1 卖出了第 4 张票
pool-1-thread-2 卖出了第 5 张票
pool-1-thread-1 卖出了第 2 张票
pool-1-thread-3 卖出了第 3 张票
pool-1-thread-2 卖出了第 1 张票

很显著,呈现了反复售票的状况。

【示例】应用自旋锁来保障线程平安

能够通过自旋锁这种非阻塞同步来保障线程平安,上面应用 AtomicReference 来实现一个自旋锁。

public class AtomicReferenceDemo2 {

    private static int ticket = 10;

    public static void main(String[] args) {threadSafeDemo();
    }

    private static void threadSafeDemo() {SpinLock lock = new SpinLock();
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 5; i++) {executorService.execute(new MyThread(lock));
        }
        executorService.shutdown();}

    static class SpinLock {private AtomicReference<Thread> atomicReference = new AtomicReference<>();

        public void lock() {Thread current = Thread.currentThread();
            while (!atomicReference.compareAndSet(null, current)) {}}

        public void unlock() {Thread current = Thread.currentThread();
            atomicReference.compareAndSet(current, null);
        }

    }

    static class MyThread implements Runnable {

        private SpinLock lock;

        public MyThread(SpinLock lock) {this.lock = lock;}

        @Override
        public void run() {while (ticket > 0) {lock.lock();
                if (ticket > 0) {System.out.println(Thread.currentThread().getName() + "卖出了第" + ticket + "张票");
                    ticket--;
                }
                lock.unlock();}
        }

    }

}

输入后果:

pool-1-thread-2 卖出了第 10 张票
pool-1-thread-1 卖出了第 9 张票
pool-1-thread-3 卖出了第 8 张票
pool-1-thread-2 卖出了第 7 张票
pool-1-thread-3 卖出了第 6 张票
pool-1-thread-1 卖出了第 5 张票
pool-1-thread-2 卖出了第 4 张票
pool-1-thread-1 卖出了第 3 张票
pool-1-thread-3 卖出了第 2 张票
pool-1-thread-1 卖出了第 1 张票

2.CAS 的原理

Java 次要利用 Unsafe 这个类提供的 CAS 操作。Unsafe 的 CAS 依赖的是 JVM 针对不同的操作系统实现的硬件指令 Atomic::cmpxchgAtomic::cmpxchg 的实现应用了汇编的 CAS 操作,并应用 CPU 提供的 lock 信号保障其原子性。

3.CAS 带来的问题

个别状况下,CAS 比锁性能更高。因为 CAS 是一种非阻塞算法,所以其防止了线程阻塞和唤醒的等待时间。

然而,事物总会有利有弊,CAS 也存在三大问题:

  • ABA 问题
  • 循环工夫长开销大
  • 只能保障一个共享变量的原子性

如何解决这三个问题:

3.1 ABA 问题

如果一个变量首次读取的时候是 A 值,它的值被改成了 B,起初又被改回为 A,那 CAS 操作就会误认为它素来没有被扭转过

J.U.C 包提供了一个带有标记的 原子援用类 如:AtomicStampedReference 来解决这个问题 ,它能够通过管制变量值的版本来保障 CAS 的正确性。大部分状况下 ABA 问题不会影响程序并发的正确性,如果须要解决 ABA 问题,改用 传统的互斥同步可能会比原子类更高效
解决方案:减少标记位,例如:AtomicMarkableReference、AtomicStampedReference

3.2 循环工夫长开销大

自旋 CAS(一直尝试,直到胜利为止)如果长时间不胜利,会给 CPU 带来十分大的执行开销

如果 JVM 能反对处理器提供的 pause 指令那么效率会有肯定的晋升,pause 指令有两个作用:

  • 它能够提早流水线执行指令(de-pipeline), 使 CPU 不会耗费过多的执行资源,提早的工夫取决于具体实现的版本,在一些处理器上延迟时间是零。
  • 它能够防止在退出循环的时候因内存程序抵触(memory order violation)而引起 CPU 流水线被清空(CPU pipeline flush),从而进步 CPU 的执行效率。

解决方案:因为是 while 循环,耗费必然大。设置尝试次数下限

3.3 只能保障一个共享变量的原子性

当对一个共享变量执行操作时,咱们能够应用循环 CAS 的形式来保障原子操作,然而对多个共享变量操作时,循环 CAS 就无奈保障操作的原子性,这个时候就能够用锁。

或者有一个取巧的方法,就是把多个共享变量合并成一个共享变量来操作。比方有两个共享变量 i = 2, j = a,合并一下 ij=2a,而后用 CAS 来操作 ij。从 Java 1.5 开始 JDK 提供了 AtomicReference 类来保障援用对象之间的原子性
解决方案:用 AtomicReference 把多个变量封装成一个对象来进行 CAS 操作.

关注公众号:java 宝典

正文完
 0