【搞定Java并发编程】第9篇:CAS详解

红太狼 2022-04-06 05:27 324阅读 0赞

上一篇:volatile关键字详解:https://blog.csdn.net/pcwl1206/article/details/84881395

目 录

一、CAS基本概念

1.1、CAS的定义

1.2、CAS的3个操作数

二、Java如何实现原子操作

2.1、相关概念

2.2、处理器如何实现原子操作

2.3、Java如何实现原子操作

三、原子变量

四、AtomicInteger源码解析

五、模拟CAS算法


我们在上篇文章 volatile 关键字的讲解中提到:volatile 不能保证变量状态的“原子性操作”。

所谓的原子性就是:一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉

在Java基础系列文章中有一篇关于: i++ 和 ++i 详解的文章,因为 i++操作不具有原子性。

今天我们就来讲解Java提供的一种原子性的操作算法:CAS算法。先把CAS的关键概念列举出来,后文再具体讲解Java是如何实现其原子操作的。

一、CAS基本概念

1.1、CAS的定义

CAS(Compare And Swap):是一种硬件对并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问。

CAS是一种无锁的非阻塞算法的实现。

1.2、CAS的3个操作数

CAS包含了3个操作数:

1、内存值:需要读写的内存值V;

2、预估值:进行比较的值A,即内存中的旧值;

3、更新值:拟写入的新值B。

  • 当且仅当 V = A 时,CAS通过原子方式用新值 B 来更新 V 的值,否则不会执行任何操作。

二、Java如何实现原子操作

本小节内容参考:https://blog.csdn.net/a724888/article/details/60871077

2.1、相关概念

  • 原子和原子操作

原子(atom)本意是“不能被进一步分割的最小粒子”,而原子操作(atomic operation)意为”不可被中断的一个或一系列操作” 。在多处理器上实现原子操作就变得有点复杂。下文中会讲解在Inter处理器和Java里是如何实现原子操作的。

  • 相关术语






























术语名称 英文名称 解释
缓存行 Cache line 缓存的最小操作单位
比较并交换 Compare and Swap CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较下在旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换
CPU流水线 CPU pipeline CPU流水线的工作方式就象工业生产上的装配流水线,在CPU中由5~6个不同功能的电路单元组成一条指令处理流水线,然后将一条X86指令分成5~6步后再由这些电路单元分别执行,这样就能实现在一个CPU时钟周期完成一条指令,因此提高CPU的运算速度
内存顺序冲突 Memory order violation 内存顺序冲突一般是由假共享引起,假共享是指多个CPU同时修改同一个缓存行的不同部分而引起其中一个CPU的操作无效,当出现这个内存顺序冲突时,CPU必须清空流水线

2.2、处理器如何实现原子操作

32位 IA-32 处理器使用基于对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。

2.2.1 处理器自动保证基本内存操作的原子性

首先处理器会自动保证基本的内存操作的原子性。处理器保证从系统内存当中读取或者写入一个字节是原子的,意思是当一个处理器读取一个字节时,其他处理器不能访问这个字节的内存地址。奔腾6和最新的处理器能自动保证单处理器对同一个缓存行里进行16/32/64位的操作是原子的,但是复杂的内存操作处理器不能自动保证其原子性,比如跨总线宽度,跨多个缓存行,跨页表的访问。但是处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性

2.2.2 使用总线锁保证原子性

第一个机制是通过总线锁保证原子性。如果多个处理器同时对共享变量进行读改写(i++就是经典的读改写操作)操作,那么共享变量就会被多个处理器同时进行操作,这样读改写操作就不是原子的,操作完之后共享变量的值会和期望的不一致,举个例子:如果 i = 1,我们进行两次 i++ 操作,我们期望的结果是 3,但是有可能结果是 2。如下图

watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3Bjd2wxMjA2_size_16_color_FFFFFF_t_70

原因是:有可能多个处理器同时从各自的缓存中读取变量 i,分别进行加1操作,然后分别写入系统内存当中。那么想要保证读改写共享变量的操作是原子的,就必须保证CPU1读改写共享变量的时候,CPU2不能操作缓存了该共享变量内存地址的缓存。

处理器使用总线锁就是来解决这个问题的。所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占使用共享内存。

2.2.3 使用缓存锁保证原子性

第二个机制是通过缓存锁定保证原子性。在同一时刻我们只需保证对某个内存地址的操作是原子性即可,但总线锁定把CPU和内存之间通信锁住了,这使得锁定期间,其他处理器不能操作其内存地址的数据,所以总线锁定的开销比较大,最近的处理器在某些场合下使用缓存锁定代替总线锁定来进行优化

频繁使用的内存会缓存在处理器的L1,L2和L3高速缓存里,那么原子操作就可以直接在处理器内部缓存中进行,并不需要声明总线锁。在奔腾6和最近的处理器中可以使用“缓存锁定”的方式来实现复杂的原子性。所谓“缓存锁定”就是如果缓存在处理器缓存行中内存区域在LOCK操作期间被锁定,当它执行锁操作回写内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性。因为缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时会起缓存行无效。在上例中,当CPU1修改缓存行中的 i 使用缓存锁定,那么CPU2就不能同时缓存了 i 的缓存行。

但是有两种情况下处理器不会使用缓存锁定。第一种情况是:当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行(cache line),则处理器会调用总线锁定。第二种情况是:有些处理器不支持缓存锁定。对于Inter486和奔腾处理器,就算锁定的内存区域在处理器的缓存行中也会调用总线锁定。

以上两个机制我们可以通过Inter处理器提供了很多LOCK前缀的指令来实现。比如位测试和修改指令BTS,BTR,BTC,交换指令XADD,CMPXCHG和其他一些操作数和逻辑指令,比如ADD(加),OR(或)等,被这些指令操作的内存区域就会加锁,导致其他处理器不能同时访问它。

2.3、Java如何实现原子操作

在Java中可以通过循环CAS的方式来实现原子操作。

2.3.1、使用CAS实现原子操作

JVM中的CAS操作正是利用了上文中提到的处理器提供的CMPXCHG指令实现的。自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止,以下代码实现了一个基于CAS线程安全的计数器方法safeCount和一个非线程安全的计数器count。

  1. public class Counter {
  2. private AtomicInteger atomicI = new AtomicInteger(0);
  3. private int i = 0;
  4. public static void main(String[] args){
  5. final Counter cas = new Counter();
  6. List<Thread> ts = new ArrayList<Thread>(600);
  7. long start = System.currentTimeMillis();
  8. for (int j = 0; j < 100; j++) {
  9. Thread t = new Thread(new Runnable(){
  10. @Override
  11. public void run() {
  12. for (int i = 0; i < 10000; i++) {
  13. cas.count();
  14. cas.safeCount();
  15. }
  16. }
  17. });
  18. ts.add(t);
  19. }
  20. for(Thread t : ts){
  21. t.start();
  22. }
  23. // 等待所有线程执行完成
  24. for(Thread t : ts){
  25. try{
  26. t.join();
  27. }catch(InterruptedException e){
  28. e.printStackTrace();
  29. }
  30. }
  31. System.out.println("非线程安全计数器:" + cas.i);
  32. System.out.println("线程安全计数器:" + cas.atomicI.get());
  33. System.out.println(System.currentTimeMillis() - start + "毫秒");
  34. }
  35. // 使用CAS实现线程安全计数器
  36. private void safeCount(){
  37. for(;;){
  38. int i = atomicI.get();
  39. boolean suc = atomicI.compareAndSet(i, ++i);
  40. if(suc){
  41. break;
  42. }
  43. }
  44. }
  45. // 非线程安全计数器
  46. private void count(){
  47. i++;
  48. }
  49. }

运行结果:

20181208153002330.png

从Java1.5开始JDK的并发包里提供了一些类来支持原子操作,如AtomicBoolean(用原子方式更新的 boolean 值),AtomicInteger(用原子方式更新的 int 值),AtomicLong(用原子方式更新的 long 值),这些原子包装类还提供了有用的工具方法,比如以原子的方式将当前值自增1和自减1。

在Java并发包中有一些并发框架也使用了自旋CAS的方式来实现原子操作,比如LinkedTransferQueue类的Xfer方法。CAS虽然很高效的解决原子操作,但是CAS仍然存在三大问题:ABA问题、循环时间长开销大以及只能保证一个共享变量的原子操作。

  • 1.ABA问题

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

  1. public boolean compareAndSet(
  2. V expectedReference, // 预期引用
  3. V newReference, // 更新后的引用
  4. int expectedStamp, // 预期标志
  5. int newStamp // 更新后的标志
  6. )
  • 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操作。

2.3.2、使用锁机制实现原子操作

锁机制保证了只有获得锁的线程能够操作锁定的内存区域。JVM内部实现了很多种锁机制,有偏向锁,轻量级锁和互斥锁。有意思的是除了偏向锁,JVM实现锁的方式都用到的循环CAS。当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时候使用循环CAS释放锁。详细说明可以参见文章Java SE1.6中的Synchronized。


三、原子变量

原子变量是类的小工具包,支持在单个变量上解除锁的线程安全编程。事实上,此包中的类可以将volatile值、字段和数组元素的概念扩展到那些也提供了原子条件更新操作的类。

类AtomicBoolean、AtomicInteger、AtomicLong和AtomicReferennce的实例各自提供相应类型的单个变量的访问和更新。每个类也为该类型提供适当的实用工具方法。

类AtomicIntegerArray、AtomicLongArray和AtomicReferenceArray类进一步扩展了原子操作,对这些类型的数组提供了支持。这些类在为其数组元素提供volatile访问语义方面也引人注目,这对于普通数组来说是不受支持的。

核心方法:boolean compareAndSet(expectedValue, updateValue);

Java.util.concurrent.atomic包下提供了如下常用的原子操作类:

1、原子更新基本类型

AtomicBoolean:原子更新布尔类型

AtomicInteger:原子更新整型

AtomicLong:原子更新长整型

2、原子更新数组

AtomicIntegerArray:原子更新数组里的元素

AtomicLongArray:原子更新长整型数组里的元素

AtomicReferenceArray:原子更新引用类型数组里的元素

3、原子更新引用类型

AtomicReference:原子更新引用类型

AtomicReferenceFieldReference:原子更新引用类型里的字段

AtomicMarkableReference:原子更新带有标记位的引用类型

4、原子更新字段类

AtomicIntegerFieldUpdater:原子更新整型的字段的更新器

AtomicLongFieldUpdater:原子更新长整型的字段的更新器

AtomicStampedReference:原子更新带有版本号的引用类型


四、AtomicInteger源码解析

这里以AtomicInteger源码为例进行解析,看其内部如何实现的CAS算法来保证原子性的。

先挑几个比较重要的方法出来进行单独讲解:

  • getAndIncrement()方法

    public final int getAndIncrement() {

    1. for (;;) {
    2. int current = get();
    3. int next = current + 1;
    4. if (compareAndSet(current, next))
    5. return current;
    6. }

    }

getAndIncrement() 相当于int类型中的 i++ 操作。

这个方法的做法是:先获取到当前的 value 属性值,然后将 value 加 1,赋值给一个局部的 next 变量。然而,这两步都是非线程安全的,但是内部有一个死循环,不断去做compareAndSet操作,直到成功为止。也就是值修改的根本在compareAndSet方法里面。

其他的几个方法类似,比如:getAndDecrement()、getAndAdd(int delta)、incrementAndGet()、decrementAndGet()、addAndGet(int delta)。这些方法的核心都在于:不断去做compareAndSet操作,直到成功为止

  • compareAndSet(int expect, int update)

    public final boolean compareAndSet(int expect, int update) {

    1. return unsafe.compareAndSwapInt(this, valueOffset, expect, update);

    }

CAS算法:通过对比“valueOffset上的value”与expect是否相同,来决定是否修改value值为update值。

compareAndSet所做的实际上是:调用 Sun 的 UnSafe 的 compareAndSwapInt 方法来完成的。此方法为 native 方法,compareAndSwapInt 基于的是CPU 的 CAS指令来实现的。所以基于 CAS 的操作可认为是无阻塞的,一个线程的失败或挂起不会引起其它线程也失败或挂起。并且由于 CAS 操作的是 CPU 原语,所以性能比较好。

  • getAndSet(int newValue)

    public final int getAndSet(int newValue) {

    1. for (;;) {
    2. int current = get();
    3. if (compareAndSet(current, newValue))
    4. return current;
    5. }

    }

getAndSet(int newValue)方法:主要是用来获取旧值的,不管旧值和预期值是否相等,都会将旧值返回。

  • get()

    public final int get() {

    1. return value;

    }

get()方法:主要是用来获取当前值的。

  • set(int newValue)

    public final void set(int newValue) {

    1. value = newValue;

    }

set(int newValue)方法:给value设置一个新值。

  • 完整的AtomicInteger源码:

    package java.util.concurrent.atomic;
    import sun.misc.Unsafe;

    public class AtomicInteger extends Number implements java.io.Serializable {

    1. private static final long serialVersionUID = 6214790243416807050L;
    2. // setup to use Unsafe.compareAndSwapInt for updates
    3. private static final Unsafe unsafe = Unsafe.getUnsafe();
    4. private static final long valueOffset;
    5. static {
    6. try {
    7. valueOffset = unsafe.objectFieldOffset
    8. (AtomicInteger.class.getDeclaredField("value"));
    9. } catch (Exception ex) { throw new Error(ex); }
    10. }
    11. private volatile int value;
    12. // 构造器:用initialValue创建一个AtomicInteger
    13. public AtomicInteger(int initialValue) {
    14. value = initialValue;
    15. }
    16. public AtomicInteger() {
    17. }
    18. // 获取当前的value值
    19. public final int get() {
    20. return value;
    21. }
    22. // 给value设置一个新值
    23. public final void set(int newValue) {
    24. value = newValue;
    25. }
  1. public final void lazySet(int newValue) {
  2. unsafe.putOrderedInt(this, valueOffset, newValue);
  3. }
  4. // 获取旧值
  5. public final int getAndSet(int newValue) {
  6. for (;;) {
  7. int current = get();
  8. if (compareAndSet(current, newValue))
  9. return current;
  10. }
  11. }
  12. /*compareAndSet所做的实际上是:调用 Sun 的 UnSafe 的 compareAndSwapInt 方法来完成的,
  13. 此方法为 native 方法,compareAndSwapInt 基于的是CPU 的 CAS指令来实现的。
  14. 所以基于 CAS 的操作可认为是无阻塞的,一个线程的失败或挂起不会引起其它线程也失败或挂起。
  15. 并且由于 CAS 操作是 CPU 原语,所以性能比较好。
  16. */
  17. // CAS算法:通过对比“valueOffset上的value”与expect是否相同,来决定是否修改value值为update值。
  18. public final boolean compareAndSet(int expect, int update) {
  19. return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
  20. }
  21. public final boolean weakCompareAndSet(int expect, int update) {
  22. return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
  23. }
  24. /*i++:这个方法的做法为先获取到当前的 value 属性值,然后将 value 加 1,
  25. 赋值给一个局部的 next 变量,然而,这两步都是非线程安全的,但是内部有一个死循环,
  26. 不断去做compareAndSet操作,直到成功为止,也就是修改的根本在compareAndSet方法里面
  27. */
  28. public final int getAndIncrement() {
  29. for (;;) {
  30. int current = get();
  31. int next = current + 1;
  32. if (compareAndSet(current, next))
  33. return current;
  34. }
  35. }
  36. // 自减 i--
  37. public final int getAndDecrement() {
  38. for (;;) {
  39. int current = get();
  40. int next = current - 1;
  41. if (compareAndSet(current, next))
  42. return current;
  43. }
  44. }
  45. // 加法
  46. public final int getAndAdd(int delta) {
  47. for (;;) {
  48. int current = get();
  49. int next = current + delta;
  50. if (compareAndSet(current, next))
  51. return current;
  52. }
  53. }
  54. // ++i
  55. public final int incrementAndGet() {
  56. for (;;) {
  57. int current = get();
  58. int next = current + 1;
  59. if (compareAndSet(current, next))
  60. return next;
  61. }
  62. }
  63. // --i
  64. public final int decrementAndGet() {
  65. for (;;) {
  66. int current = get();
  67. int next = current - 1;
  68. if (compareAndSet(current, next))
  69. return next;
  70. }
  71. }
  72. public final int addAndGet(int delta) {
  73. for (;;) {
  74. int current = get();
  75. int next = current + delta;
  76. if (compareAndSet(current, next))
  77. return next;
  78. }
  79. }
  80. public String toString() {
  81. return Integer.toString(get());
  82. }
  83. public int intValue() {
  84. return get();
  85. }
  86. public long longValue() {
  87. return (long)get();
  88. }
  89. public float floatValue() {
  90. return (float)get();
  91. }
  92. public double doubleValue() {
  93. return (double)get();
  94. }
  95. }

五、模拟CAS算法

  1. /**模拟 CAS 算法*/
  2. public class TestCompareAndSwap {
  3. public static void main(String[] args) {
  4. final CompareAndSwap cas = new CompareAndSwap();
  5. for (int i = 0; i < 10; i++) {
  6. new Thread(new Runnable() {
  7. @Override
  8. public void run() {
  9. int expectedValue = cas.get();
  10. boolean b = cas.compareAndSet(expectedValue, (int) (Math.random() * 101));
  11. System.out.println(b);
  12. }
  13. }).start();
  14. }
  15. }
  16. }
  17. class CompareAndSwap {
  18. private int value;
  19. // 获取内存值
  20. public synchronized int get() {
  21. return value;
  22. }
  23. // 比较
  24. public synchronized int compareAndSwap(int expectedValue, int newValue) {
  25. int oldValue = value;
  26. if (oldValue == expectedValue) {
  27. this.value = newValue;
  28. }
  29. return oldValue;
  30. }
  31. // 设置
  32. public synchronized boolean compareAndSet(int expectedValue, int newValue) {
  33. return expectedValue == compareAndSwap(expectedValue, newValue);
  34. }
  35. }

上一篇:volatile关键字详解:https://blog.csdn.net/pcwl1206/article/details/84881395

参考及推荐:

1、CAS操作:https://blog.csdn.net/a724888/article/details/60871077

发表评论

表情:
评论列表 (有 0 条评论,324人围观)

还没有评论,来说两句吧...

相关阅读