java多线程之CAS

CAS (Compare and Swap)

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

1.CAS的应用场景

CAS 只适用于线程冲突较少的情况

CAS 的典型应用场景是:

  • 原子类
  • 自旋锁

1.1 原子类

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

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

1if(a==b) { 2 a++; 3}

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

解决这种问题的最经典方式是应用原子类的 incrementAndGet 方法。

1public class AtomicIntegerDemo { 2 3 public static void main(String[] args) throws InterruptedException { 4 ExecutorService executorService = Executors.newFixedThreadPool(3); 5 final AtomicInteger count = new AtomicInteger(0); 6 for (int i = 0; i < 10; i++) { 7 executorService.execute(new Runnable() { 8 @Override 9 public void run() { 10 count.incrementAndGet(); 11 } 12 }); 13 } 14 15 executorService.shutdown(); 16 executorService.awaitTermination(3, TimeUnit.SECONDS); 17 System.out.println("Final Count is : " + count.get()); 18 } 19 20}

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

1.2 自旋锁

利用原子类(本质上是 CAS),可以实现自旋锁。

所谓自旋锁,是指线程反复检查锁变量是否可用,直到成功为止。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。

示例:非线程安全示例

1public class AtomicReferenceDemo { 2 3 private static int ticket = 10; 4 5 public static void main(String[] args) { 6 ExecutorService executorService = Executors.newFixedThreadPool(3); 7 for (int i = 0; i < 5; i++) { 8 executorService.execute(new MyThread()); 9 } 10 executorService.shutdown(); 11 } 12 13 static class MyThread implements Runnable { 14 15 @Override 16 public void run() { 17 while (ticket > 0) { 18 System.out.println(Thread.currentThread().getName() + " 卖出了第 " + ticket + " 张票"); 19 ticket--; 20 } 21 } 22 23 } 24 25}

输出结果:

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

很明显,出现了重复售票的情况。

【示例】使用自旋锁来保证线程安全

可以通过自旋锁这种非阻塞同步来保证线程安全,下面使用 AtomicReference 来实现一个自旋锁。

1public class AtomicReferenceDemo2 { 2 3 private static int ticket = 10; 4 5 public static void main(String[] args) { 6 threadSafeDemo(); 7 } 8 9 private static void threadSafeDemo() { 10 SpinLock lock = new SpinLock(); 11 ExecutorService executorService = Executors.newFixedThreadPool(3); 12 for (int i = 0; i < 5; i++) { 13 executorService.execute(new MyThread(lock)); 14 } 15 executorService.shutdown(); 16 } 17 18 static class SpinLock { 19 20 private AtomicReference<Thread> atomicReference = new AtomicReference<>(); 21 22 public void lock() { 23 Thread current = Thread.currentThread(); 24 while (!atomicReference.compareAndSet(null, current)) {} 25 } 26 27 public void unlock() { 28 Thread current = Thread.currentThread(); 29 atomicReference.compareAndSet(current, null); 30 } 31 32 } 33 34 static class MyThread implements Runnable { 35 36 private SpinLock lock; 37 38 public MyThread(SpinLock lock) { 39 this.lock = lock; 40 } 41 42 @Override 43 public void run() { 44 while (ticket > 0) { 45 lock.lock(); 46 if (ticket > 0) { 47 System.out.println(Thread.currentThread().getName() + " 卖出了第 " + ticket + " 张票"); 48 ticket--; 49 } 50 lock.unlock(); 51 } 52 } 53 54 } 55 56}

输出结果:

1pool-1-thread-2 卖出了第 10 张票 2pool-1-thread-1 卖出了第 9 张票 3pool-1-thread-3 卖出了第 8 张票 4pool-1-thread-2 卖出了第 7 张票 5pool-1-thread-3 卖出了第 6 张票 6pool-1-thread-1 卖出了第 5 张票 7pool-1-thread-2 卖出了第 4 张票 8pool-1-thread-1 卖出了第 3 张票 9pool-1-thread-3 卖出了第 2 张票 10pool-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宝典 a

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

KVM调整cpu和内存

一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid

Java并发机制底层实现原理

锁的膨胀过程预备知识CAS硬件对并发的支持在大多数处理器架构(包括IA32和Sparc)中采用的方法是实现一个比较并交换(CAS)指令,CAS包含了3个操作数——内存位置(V),预期原值(A),拟写入的新值(B),当且仅当VA时,CAS才会通过原子方式用新值(B)来更新(V)原有的值,无论操作成