CAS(Compare-and-Swap),即比较并替换,是一种实现并发算法时常用到的技术,Java并发包中的很多类都使用了CAS技术。CAS也是现在面试经常问的问题,本文将深入的介绍CAS的原理。

一、简单实例

一个方法实现i++时,如下面的getAndIncrement1()方法,多线程调用时,会出现并发安全问题。这时,可以通过synchronized关键字显式加锁保证并发安全。

在JAVA并发包中,提供了很多的Atomic原子类,也可以保证线程安全,比如getAndIncrement2()中的AtomicInteger.getAndIncrement()方法。

package com.wuxiaolong.concurrent;import java.util.concurrent.atomic.AtomicInteger;/*** Description:** @author 诸葛小猿* @date 2020-09-14*/
public class Test1 {public static int i = 0;/*** 加锁保证多线程调用的并发安全* @return*/public synchronized static int getAndIncrement1(){i++;return i;}/*** AtomicInteger是线程安全的  * @return*/public static int getAndIncrement2(){AtomicInteger ai = new AtomicInteger();int in = ai.getAndIncrement();return in;}
}

二、什么是CAS

CAS:compare and swap或compare and exchange,比较和交互。作用是保证在没有锁的情况下,多个线程对一个值的更新是线程安全的。

CAS过程:它包含三个参数CAS(V,E,N),V表示要更新的变量,E表示预期值,N表示新值,仅当V值等于E值时,才会将V值设置为N值,如果V值和E值不同,说明已经有其他线程做了更新,则当前线程什么都不做。

如果使用CAS实现i++这个操作,可以有如下几步:

1.读取当前i的值(比如i=0),记为E

2.计算i++的值为1,记为V

3.再次读取i的值,记为N。

4.如果E==N,则将i的值更新为1,否则不更新。

CAS的过程很像乐观锁,乐观锁认为发生线程安全问题的概率比较小,所以不用直接加锁,只是更新数据时比较一下原来的数据有没有发生变化。

上面第四步中,如果E==N,并不能说明i的值没有改变过,可能一个线程执行第四步的时候,另一个线程将i改变后又变回来了,对于第一个线程来说,并不知道这个中间过程的存在。这个现象就是ABA问题。

ABA问题如何解决?其实也很简单,给i加一个版本号字段,每次i有变化都对版本号加1,每次更新i的时候除了比较E值,还比较版本号是否一致。这样就解决了ABA问题。在实际开发过程中,如果ABA问题对业务没有影响,就不用考虑这个问题。

三、CAS在AtomicInteger中的使用

CAS的底层实现、synchronized的底层实现、volatile的底层实现都是一样的。我们以上面提到的AtomicInteger类说明。

AtomicInteger是线程安全的,通常说AtomicInteger是无锁或自旋锁。这就是CAS在JDK中的应用。

AtomicInteger.getAndIncrement()方法的源码:

    /*** Atomically increments by one the current value.** @return the previous value*/public final int getAndIncrement() {return unsafe.getAndAddInt(this, valueOffset, 1);}

Unsafe.getAndAddInt()源码:

    public final int getAndAddInt(Object var1, long var2, int var4) {int var5;do {var5 = this.getIntVolatile(var1, var2);} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));return var5;}

Unsafe.compareAndSwapInt()源码:

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

这个方法是native修饰的,在JDK中就看不到源码实现了。由于java代码是在JVM中执行的,Oracle的JVM是Hotspot, 如果要看native方法的实现,可以找Hotspot的源码,这个源码是C和C++写的。Unsafe.java这个类的源码对应Hotspot源码中unsafe.cpp,这个是C++写的。

四、CAS的底层实现

CAS的底层实现,就必须了解Hotspot的源码。可以在查看OpenJdk的代码,这里就可以找到各种版本的源码。我们以jdk8u为中的unsafe.cpp为例,继续分析compareAndSwapInt方法。

// 地址:http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/a0eb08e2db5a/src/share/vm/prims/unsafe.cppUNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))UnsafeWrapper("Unsafe_CompareAndSwapInt");oop p = JNIHandles::resolve(obj);jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

可以看到调用到了Atomic::cmpxchg方法,继续分析找到这个方法:

// 地址:http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/a0eb08e2db5a/src/share/vm/runtime/atomic.cppjbyte Atomic::cmpxchg(jbyte exchange_value, volatile jbyte* dest, jbyte compare_value) {assert(sizeof(jbyte) == 1, "assumption.");uintptr_t dest_addr = (uintptr_t)dest;uintptr_t offset = dest_addr % sizeof(jint);volatile jint* dest_int = (volatile jint*)(dest_addr - offset);jint cur = *dest_int;jbyte* cur_as_bytes = (jbyte*)(&cur);jint new_val = cur;jbyte* new_val_as_bytes = (jbyte*)(&new_val);new_val_as_bytes[offset] = exchange_value;while (cur_as_bytes[offset] == compare_value) {//关键方法jint res = cmpxchg(new_val, dest_int, cur); if (res == cur) break;cur = res;new_val = cur;new_val_as_bytes[offset] = exchange_value;}return cur_as_bytes[offset];
}

各种系统下各种cpu架构,都有相关的实现方法,具体的文件名称如下:

// 地址:http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/a0eb08e2db5a/src/share/vm/runtime/atomic.inline.hpp#ifndef SHARE_VM_RUNTIME_ATOMIC_INLINE_HPP
#define SHARE_VM_RUNTIME_ATOMIC_INLINE_HPP#include "runtime/atomic.hpp"// Linux
#ifdef TARGET_OS_ARCH_linux_x86
# include "atomic_linux_x86.inline.hpp"
#endif
#ifdef TARGET_OS_ARCH_linux_sparc
# include "atomic_linux_sparc.inline.hpp"
#endif
#ifdef TARGET_OS_ARCH_linux_zero
# include "atomic_linux_zero.inline.hpp"
#endif
#ifdef TARGET_OS_ARCH_linux_arm
# include "atomic_linux_arm.inline.hpp"
#endif
#ifdef TARGET_OS_ARCH_linux_ppc
# include "atomic_linux_ppc.inline.hpp"
#endif// Solaris
#ifdef TARGET_OS_ARCH_solaris_x86
# include "atomic_solaris_x86.inline.hpp"
#endif
#ifdef TARGET_OS_ARCH_solaris_sparc
# include "atomic_solaris_sparc.inline.hpp"
#endif// Windows
#ifdef TARGET_OS_ARCH_windows_x86
# include "atomic_windows_x86.inline.hpp"
#endif// AIX
#ifdef TARGET_OS_ARCH_aix_ppc
# include "atomic_aix_ppc.inline.hpp"
#endif// BSD
#ifdef TARGET_OS_ARCH_bsd_x86
# include "atomic_bsd_x86.inline.hpp"
#endif
#ifdef TARGET_OS_ARCH_bsd_zero
# include "atomic_bsd_zero.inline.hpp"
#endif#endif // SHARE_VM_RUNTIME_ATOMIC_INLINE_HPP

在src/os_cpu/目录下面有各种系统下各种cpu架构的代码实现,其中src/os_cpu/linux_x86/vm下是基于linux下x86架构的代码,cmpxchg方法的最终实现:

// 地址:http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/a0eb08e2db5a/src/os_cpu/linux_x86/vm/atomic_linux_x86.inline.hppinline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {int mp = os::is_MP();__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)": "=a" (exchange_value): "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp): "cc", "memory");return exchange_value;
}

其中__asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"一段代码是核心,asm指汇编语言,是机器语言,直接和cpu交互。

LOCK_IF_MP指“如果是多个CPU就加锁”,MP指Multi-Processors。程序会根据当前处理器的数量来决定是否为cmpxchg指令添加lock前缀。如果程序是在多处理器上运行,就为cmpxchg指令加上lock前缀(lock cmpxchg)。反之,如果程序是在单处理器上运行,就省略lock前缀(单个处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏障效果)。

// 地址:http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/a0eb08e2db5a/src/os_cpu/linux_x86/vm/atomic_linux_x86.inline.hpp// Adding a lock prefix to an instruction on MP machine
#define LOCK_IF_MP(mp) "cmp $0, " #mp "; je 1f; lock; 1: "

总结

从上面可以看出,CAS的本质就是:

lock cmpxchg 指令

但是cmpxchg这条cpu指令本身并不是原子性的,还是依赖了前面的lock指令。

关注公众号,输入“java-summary”即可获得源码。

完成,收工!

传播知识,共享价值】,感谢小伙伴们的关注和支持,我是【诸葛小猿】,一个彷徨中奋斗的互联网民工。

高并发系列——CAS操作及CPU底层操作解析相关推荐

  1. java中acquire()_Java高并发系列之AQS中acquire源码解析

    我们知道,AQS中最重要的两个方法就是acquire和release方法.我们本文来走读走读acquire的源码. 首先,tryAcquire是需要子类具体去实现,其作用就是设置state的值,如果设 ...

  2. shell 获取命令执行结果_java高并发系列 第31天:获取线程执行结果,这6种方法你都知道?...

    这是java高并发系列第31篇. 环境:jdk1.8. java高并发系列已经学了不少东西了,本篇文章,我们用前面学的知识来实现一个需求: 在一个线程中需要获取其他线程的执行结果,能想到几种方式?各有 ...

  3. java高并发系列 - 第1天:必须知道的几个概念

    java高并发系列-第1天:必须知道的几个概念 同步(Synchronous)和异步(Asynchronous) 同步和异步通常来形容一次方法调用,同步方法调用一旦开始,调用者必须等到方法调用返回后, ...

  4. Java高并发系列5-线程池

    Java高并发系列5-线程池 接上一篇Java并发系列4-并发容器我们继续 在编程中经常会使用线程来异步处理任务,但是每个线程的创建和销毁都需要一定的开销.如果每次执行一个任务都需要开个新线程去执行, ...

  5. Java 高并发系列1-开篇

    Java 高并发系列1-开篇 我们都知道在Android开发中, 一个Android程序启动之后会有一个主线程,也就是UI线程, 而网络加载数据, 本地文件长时间读写,图片压缩,等等,很多耗时操作会阻 ...

  6. java高并发系列 - 第6天:线程的基本操作,必备技能

    新建线程 新建线程很简单.只需要使用new关键字创建一个线程对象,然后调用它的start()启动线程即可. Thread thread1 = new Thread1(); t1.start(); 那么 ...

  7. java高并发系列 - 第3天:有关并行的两个重要定律

    java高并发系列第3篇文章,一个月,咱们一起啃下java高并发,欢迎留言打卡,一起坚持一个月,拿下java高并发. 有关为什么要使用并行程序的问题前面已经进行了简单的探讨.总的来说,最重要的应该是处 ...

  8. httpd开启status模块_Nginx高并发系列之二——Nginx开启ssl模块

    在上一期中我们安装配置了nginx1.13.7版本并且成功启动,那么这一期就针对安装的Nginx支持ssl模块--即开启https功能.如果还有不知道如何安装与配置的请移至: 水番丘山:Nginx高并 ...

  9. [Java高并发系列(5)][详细]Java中线程池(1)--基本概念介绍

    1 Java中线程池概述 1.1 什么是线程池? 在一个应用当中, 我们往往需要多次使用线程, 这意味着我们需要多次创建和销毁线程.那么为什么不提供一个机制或概念来管理这些线程呢? 该创建的时候创建, ...

最新文章

  1. 机器学习中的优化算法!
  2. ubuntu 10.04 安装 pyquery
  3. webpack入门启动webpack工程
  4. 区块链演进及跨域安全解决方案
  5. Linux 内核网络协议栈 ------sk_buff 结构体 以及 完全解释 (2.6.16)
  6. 读C#开发实战1200例子记录-2017年8月14日10:03:55
  7. 用指针比较三个数大小_《测量力的大小》教案
  8. c语言的数组在栈区还是,用数组实现栈的功能的C语言代码?
  9. 从零开始学习python编程-从零开始学python编程一:首行代码
  10. stm32for循环几个机械周期_带你了解包装机械设备的可调度性分析
  11. 一分钟了解阿里云产品:专有网络VPC五大热点技术问题分析
  12. 《机器人编程实战》一一2.1 为什么需要更多努力
  13. HTML转义字符参照表
  14. 重启共享文件服务器,windows server 2008 文件服务器不定期出现大量CLOSE_WAIT状态的连接,必须重启服务器,客户端才能访问共享。...
  15. 一阶低通滤波器方程_一阶有源低通滤波电路与最经典一阶低通滤波器电路图
  16. 图片合成雾的方法概述
  17. Ubuntu进入登录界面鼠标键盘失灵
  18. 人脸识别方案(一)-ocr
  19. R语言逻辑回归 logistic regression
  20. 服务器托管如何选择合适的idc服务器托管商

热门文章

  1. leaq c 汇编语言,汇编 LEA 指令
  2. geojson mysql存储_mongodb存储geoJson
  3. 如何爬取今日头条图片
  4. 机器视觉毕业设计 深度学习驾驶人脸疲劳检测系统 - python opencv
  5. jenkins自动构建部署vue
  6. HIVE 的时间转换函数
  7. 【移动开发】ido手机阅读器
  8. axmath排版突然乱了
  9. iPhone XS靠A12芯片绝杀,走肾的苹果要“走芯”了
  10. 去掉button边框样式,背景完全透明,显示出背景颜色或背景图