1、什么是CAS

CAS 即 compare and swap 比较并交换, 涉及到三个参数,内存值V, 预期值A, 要更新为的值B, 拿着预期值A与内存值V比较,相等则符合预期,将内存值V更新为B, 不相等,则不能更新V。

为什么预期值A与内存值V不一样了呢?

在多线程环境下,对于临界区的共享资源,所有线程都可以访问修改,这时为了保证数据不会发生错误,通常会对访问临界区资源加锁,同一时刻最多只能让一个线程访问(独占模式下),这样会让线程到临界区时串行执行,加锁操作可能会导致并发性能降低,而循环CAS可以实现让多个线程不加锁去访问共享资源,却也可以保证数据正确性。 如 int share = 1,线程A获取到share的值1,想要将其修改为2,这时线程B抢先修改share = 3了,线程A这时拿着share =1 预期值与实际内存中已经变为3的值比较, 不相等,cas失败,这时就重新获取最新的share再次更新,需要不断循环,直到更新成功;这里可能会存在线程一直在进行循环cas,消耗cpu资源。

cas缺点:

1、存在ABA问题

2、循环cas, 可能会花费大量时间在循环,浪费cpu资源

3、只能更新一个值(也可解决,AtomicReference 原子引用类泛型可指定对象,实现一个对象中包含多个属性值来解决只能更新一个值的问题)

2、原子类 Atomic

原子类在JUC的atomic包下提供了 AtomicInteger,AtomicBoolean, AtomicLong等基本数据类型原子类,还有可传泛型的AtomicReference, 以及带有版本号的 AtomicStampedReference , 可实现对象的原子更新, 其具体是怎样保证在多线程环境下,不加锁的情况也可以原子操作, 是其内部借助了Unsafe类,来保证更新的原子性。

类图结构如下:

分别用AtomicInteger和 Integer 演示多个线程执行自增操作,是否能够保证原子性,执行结果是否正确

代码如下:

  1. /**
  2. * @author zdd
  3. * 2019/12/22 10:47 上午
  4. * Description: 演示AtomicInteger原子类原子操作
  5. */
  6. public class CasAtomicIntegerTest {
  7. static final Integer THREAD_NUMBER = 10;
  8. static AtomicInteger atomicInteger = new AtomicInteger(0);
  9. static volatile Integer integer = 0;
  10. public static void main(String[] args) throws InterruptedException {
  11. ThreadTask task = new ThreadTask();
  12. Thread[] threads = new Thread[THREAD_NUMBER];
  13. //1,开启10个线程
  14. for (int j = 0; j < THREAD_NUMBER; j++) {
  15. Thread thread = new Thread(task);
  16. threads[j]= thread;
  17. }
  18. for (Thread thread:threads) {
  19. //开启线程
  20. thread.start();
  21. //注: join 为了保证主线程在所有子线程执行完毕后再打印结果,否则主线程就阻塞等待
  22. // thread.join();
  23. }
  24. // 主线程休眠5s, 等待所有子线程执行完毕再打印
  25. TimeUnit.SECONDS.sleep(5);
  26. System.out.println("执行完毕,atomicInteger的值为: "+ atomicInteger.get());
  27. System.out.println("执行完毕,integer的值为 : "+ integer);
  28. }
  29. public static void safeIncr() {
  30. atomicInteger.incrementAndGet();
  31. }
  32. public static void unSafeIncr() {
  33. integer ++;
  34. }
  35. static class ThreadTask implements Runnable{
  36. @Override
  37. public void run() {
  38. // 任务体,分别安全和非安全方式自增1000次
  39. for (int i = 0; i < 1000; i++) {
  40. safeIncr();
  41. }
  42. for (int i = 0; i < 1000; i++) {
  43. unSafeIncr();
  44. }
  45. }
  46. }
  47. }

执行结果如下:

疑问:上文代码中注,我本想让主线程调用每个子线程 join方法,保证主线程在所有子线程执行完毕之后再执行打印结果,然而这样执行导致非安全的Integer自增结果也正确,猜想是在执行join方法,导致这10个子线程排队有序在执行了? 因此注释了该行代码 ,改为让主线程休眠几秒来保证在子线程执行后再打印。

AtomicInteger如何保证原子性,AtomicInteger持有Unsafe对象,其大部分方法是本地方法,底层实现可保证原子操作。

  1. public class AtomicInteger extends Number implements java.io.Serializable {
  2. // setup to use Unsafe.compareAndSwapInt for updates
  3. private static final Unsafe unsafe = Unsafe.getUnsafe();

来看一下 AtomicInteger 的自增方法 incrementAndGet(),先自增,再返回增加后的值。

代码如下:

  1. public final int incrementAndGet() {
  2. //调用unsafe的方法
  3. return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
  4. }

继续看unsafe如何实现

  1. public final int getAndAddInt(Object var1, long var2, int var4) {
  2. int var5;
  3. do {
  4. //1.获取当前对象的内存中的值A
  5. var5 = this.getIntVolatile(var1, var2);
  6. //2. var1,var2联合获取内存中的值V,var5是期望中的值A, var5+var4 是将要更新为的新值
  7. } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
  8. //3. 更新成功,跳出while循环,返回更新成功时内存中的值(可能下一刻就被其他线程修改)
  9. return var5;
  10. }

执行流程图如下:

Unsafe 的compareAndSwapInt是本地方法,可原子地执行更新操作,更新成功返回true,否则false

  1. public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

3、CAS的ABA问题

什么是ABA问题?

例如 线程A获取变量atomicInteger =100, 想要将其修改为2019 (此时还未修改), 这时线程B抢先进来将atomicInteger先修改为101,再修改回atomicInteger =100,这时线程A开始去更新atomicInteger的值了,此时预期值和内存值相等,更新成功atomicInteger =2019;但是线程A 并不知道这个值其实已经被人修改过了。

代码演示如下:

  1. /**
  2. * zdd
  3. * Description: cas的ABA问题
  4. */
  5. public class CasTest1 {
  6. // static AtomicInteger atomicInteger = new AtomicInteger(100);
  7. /* 这里使用原子引用类,传入Integer类型,
  8. * 和AtomicInteger一样,AtomicReference使用更灵活,泛型可指定任何引用类型。
  9. * 也可用上面注释代码
  10. */
  11. static AtomicReference<Integer> reference = new AtomicReference<>(100);
  12. public static void main(String[] args) {
  13. //1.开启线程A
  14. new Thread(()-> {
  15. Integer expect = reference.get();
  16. try {
  17. //模拟执行任务,让线程B抢先修改
  18. TimeUnit.SECONDS.sleep(3);
  19. } catch (InterruptedException e) {
  20. e.printStackTrace();
  21. }
  22. System.out.println( "执行3s任务后, 修改值是否成功 "+ reference.compareAndSet(expect,2019)+ " 当前值为: "+ reference.get());
  23. },"A").start();
  24. //2.开启线程B
  25. new Thread(()-> {
  26. // expect1 =100
  27. Integer expect1 = reference.get();
  28. //1,先修改为101,再修改回100,产生ABA问题
  29. reference.compareAndSet(expect1,101);
  30. //expect2 =101
  31. Integer expect2 = reference.get();
  32. reference.compareAndSet(expect2, 100);
  33. },"B").start();
  34. }
  35. }

执行结果如下:可见线程A修改成功

  1. A 执行3s任务后, 修改值是否成功:true 当前值为: 2019

4、ABA问题的解决方式

解决CAS的ABA问题,是参照数据库乐观锁,添加一个版本号,每更新一次,次数+1,就可解决ABA问题了。

AtomicStampedReference

  1. /**
  2. * zdd
  3. * 2019/11/4 6:30 下午
  4. * Description:
  5. */
  6. public class CasTest1 {
  7. //设置初始值和版本号
  8. static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100,1);
  9. public static void main(String[] args) {
  10. //2,采用带有版本号的
  11. new Thread(()-> {
  12. Integer expect = stampedReference.getReference();
  13. int stamp = stampedReference.getStamp();
  14. try {
  15. //休眠3s,让线程B执行完ABA操作
  16. TimeUnit.SECONDS.sleep(3);
  17. } catch (InterruptedException e) {
  18. e.printStackTrace();
  19. }
  20. //此时 stamp=1,与实际版本号3不等,这里更新失败就是stamp没有获取到最新的
  21. System.out.println("是否修改成功: "+stampedReference.compareAndSet(expect, 101, stamp, stamp +1));
  22. System.out.println("当前 stamp 值: " + stampedReference.getStamp()+ "当前 reference: " +stampedReference.getReference());
  23. },"A").start();
  24. new Thread(()-> {
  25. Integer expect = stampedReference.getReference();
  26. int stamp = stampedReference.getStamp();
  27. try {
  28. //休眠1s,让线程A获取都旧的值和版本号
  29. TimeUnit.SECONDS.sleep(1);
  30. } catch (InterruptedException e) {
  31. e.printStackTrace();
  32. }
  33. // 1,100 -> 101, 版本号 1-> 2
  34. stampedReference.compareAndSet(expect, 101 , stamp, stamp+1);
  35. //2, 101 ->100, 版本号 2->3
  36. Integer expect2 = stampedReference.getReference();
  37. stampedReference.compareAndSet(expect2, 100, stampedReference.getStamp(), stampedReference.getStamp() + 1);
  38. },"B").start();
  39. }
  40. }

执行结果如下:

  1. 是否修改成功: false
  2. 当前 stamp 值: 3 当前 reference: 100

5、利用cas实现自旋锁

  1. package cas;
  2. import java.util.concurrent.TimeUnit;
  3. import java.util.concurrent.atomic.AtomicReference;
  4. /**
  5. * @author zdd
  6. * 2019/12/22 9:12 下午
  7. * Description: 利用cas手动实现自旋锁
  8. */
  9. public class SpinLockTest {
  10. static AtomicReference<Thread> atomicReference = new AtomicReference<>();
  11. public static void main(String[] args) {
  12. SpinLockTest spinLockTest = new SpinLockTest();
  13. //测试使用自旋锁,达到同步锁一样的效果 ,开启2个子线程
  14. new Thread(()-> {
  15. spinLockTest.lock();
  16. System.out.println(Thread.currentThread().getName()+" 开始执行,startTime: "+System.currentTimeMillis());
  17. try {
  18. //休眠3s
  19. TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace();
  20. }
  21. System.out.println(Thread.currentThread().getName()+" 结束执行,endTime: "+System.currentTimeMillis());
  22. spinLockTest.unLock();
  23. },"线程A").start();
  24. new Thread(()-> {
  25. spinLockTest.lock();
  26. System.out.println(Thread.currentThread().getName()+" 开始执行,startTime: "+System.currentTimeMillis());
  27. try {
  28. //休眠3s
  29. TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace();
  30. }
  31. System.out.println(Thread.currentThread().getName()+" 结束执行,endTime: "+System.currentTimeMillis());
  32. spinLockTest.unLock();
  33. },"线程B").start();
  34. }
  35. public static void lock() {
  36. Thread currentThread = Thread.currentThread();
  37. for (;;) {
  38. boolean flag =atomicReference.compareAndSet(null,currentThread);
  39. //cas更新成功,则跳出循环,否则一直轮询
  40. if(flag) {
  41. break;
  42. }
  43. }
  44. }
  45. public static void unLock() {
  46. Thread currentThread = Thread.currentThread();
  47. Thread momeryThread = atomicReference.get();
  48. //比较内存中线程对象与当前对象,不等抛出异常,防止未获取到锁的线程调用unlock
  49. if(currentThread != momeryThread) {
  50. throw new IllegalMonitorStateException();
  51. }
  52. //释放锁
  53. atomicReference.compareAndSet(currentThread,null);
  54. }
  55. }

执行结果如下图:

6、总结

通过全文,我们可以知道cas的概念,它的优缺点;原子类的使用,内部借助Unsafe类循环cas更新操作实现无锁情况下保证原子更新操作,进一步我们能够自己利用循环cas实现自旋锁SpinLock,它与同步锁如ReentrantLock等区别在于自旋锁是在未获取到锁情况,一直在轮询,线程时非阻塞的,对cpu资源占用大,适合查询多修改少场景,并发性能高;同步锁是未获取到锁,阻塞等待,两者各有适用场景。

多线程之美6一CAS与自旋锁的更多相关文章

  1. 我们常说的 CAS 自旋锁是什么

    CAS(Compare and swap),即比较并交换,也是实现我们平时所说的自旋锁或乐观锁的核心操作. 它的实现很简单,就是用一个预期的值和内存值进行比较,如果两个值相等,就用预期的值替换内存值, ...

  2. 多线程之美5一 AbstractQueuedSynchronizer源码分析<一>

    AQS的源码分析 目录结构 1.什么是CAS ? 2.同步器类结构 3.CLH同步队列 4.AQS中静态内部类Node 5.方法分析 ​ 5.1.acquire(int arg ) ​ 5.2.rel ...

  3. 一文读懂原子操作、内存屏障、锁(偏向锁、轻量级锁、重量级锁、自旋锁)、Disruptor、Go Context之上半部分

    我不想卷,我是被逼的 在做了几年前端之后,发现互联网行情比想象的差,不如赶紧学点后端知识,被裁之后也可接个私活不至于饿死.学习两周Go,如盲人摸象般不知重点,那么重点谁知道呢?肯定是使用Go的后端工程 ...

  4. CAS机制与自旋锁

    CAS(Compare-and-Swap),即比较并替换,java并发包中许多Atomic的类的底层原理都是CAS. 它的功能是判断内存中某个地址的值是否为预期值,如果是就改变成新值,整个过程具有原子 ...

  5. 并发编程--CAS自旋锁

    在前两篇博客中我们介绍了并发编程--volatile应用与原理和并发编程--synchronized的实现原理(二),接下来我们介绍一下CAS自旋锁相关的知识. 一.自旋锁提出的背景 由于在多处理器系 ...

  6. java多线程之锁 -- 偏向锁、轻量级锁、自旋锁、重量级锁

    转载至:https://blog.csdn.net/zqz_zqz/article/details/70233767 之前做过一个测试,详情见这篇文章<多线程 +1操作的几种实现方式,及效率对比 ...

  7. Java多线程(5):CAS

    您好,我是湘王,这是我的博客园,欢迎您来,欢迎您再来- 在JDK1.5之前,Java的多线程都是靠synchronized来保证同步的,这会引起很多性能问题,例如死锁.但随着Java的不断完善,JNI ...

  8. SpinLock 自旋锁, CAS操作(Compare & Set) ABA Problem

    SpinLock 自旋锁 spinlock 用于CPU同步, 它的实现是基于CPU锁定数据总线的指令. 当某个CPU锁住数据总线后, 它读一个内存单元(spinlock_t)来判断这个spinlock ...

  9. (转)乐观的并发策略——基于CAS的自旋

    悲观者与乐观者的做事方式完全不一样,悲观者的人生观是一件事情我必须要百分之百完全控制才会去做,否则就认为这件事情一定会出问题:而乐观者的人生观则相反,凡事不管最终结果如何,他都会先尝试去做,大不了最后 ...

随机推荐

  1. WSL记录

    cmder(mini版)作为wsl的终端,很好用,可以split屏.但是:千万不要在settings里面设置start up(启动) 里面设置 命令行“bash -cur_console:p1”!目前 ...

  2. 一个流行的网页动画JS库

    animejs https://animejs.com/ Anime.js (/ˈæn.ə.meɪ/) is a lightweight JavaScript animation library wi ...

  3. Java并发编程核心概念一览

    作者博客地址 https://muggle.javaboy.org. 并行相关概念 同步和异步 同步和异步通常来形容一次方法的调用.同步方法一旦开始,调用者必须等到方法结束才能执行后续动作:异步方法则 ...

  4. codesmith设置mysql的连接字符串

    .net core,codesmith连不上 server=192.168.3.240;Initial Catalog=tpmdb;User=root;Password=root .net frame ...

  5. 打开nginx配置的站点报错500

    打开站点报错500的原因 有很多,这里只说明一点:nginx 的fastcgi.conf配置引起的问题 环境说明 1 站点目录结构 wwwroot website public application ...

  6. 十分钟读懂JavaScript原型和原型链

    原型(prototype)这个词来自拉丁文的词proto,意谓“最初的”,意义是形式或模型.在JavaScript中,原型的探索也有很多有趣的地方,接下来跟随我的脚步去看看吧. 原型对象释义 每一个构 ...

  7. Docker下打包FastDFS镜像以及上传遇到的问题

    官方地址:https://github.com/happyfish100/fastdfs 一.先下载个包,然后解压(自己找个目录下载即可) [root@localhost soft]# wget ht ...

  8. 使用docker-compose快速搭建gitlab

    1. 准备工作: centos7 [root@dev_vonedao_95 gitlab]# docker -v Docker version , build 633a0ea [root@dev_vo ...

  9. C 语言 基础篇

    1.机器语言 2.汇编语言 3.高级语言:C.C++.Java(基于虚拟机) C语言开发:Unix,Linux,Mac OS,iOS,Android,Windows,Ubuntu 开发环境:visua ...

  10. Openstack Sahara组件和架构简介

    1.简介 Apache Hadoop是目前被广泛使用的主流大数据处理计算框架,Sahara项目旨在使用用户能够在Openstack平台上便于创建和管理Hadoop以及其他计算框架集群,实现类似AWS的 ...