版权声明:本文为博主原创文章,遵循CC 4.0 by-sa版权协议,转载请附上原文出处链接和本声明。

这几天准备梳理一下Java多线程和并发的相关知识,主要是系统的梳理一下J.U.C包里的一些东西,特别是以前看过很多遍的AQS和实现类,还有各种并发安全的集合类。最重要的就是这个CAS操作,可以说是整个J.U.C包的灵魂之处。

1.什么是CAS?

CAS:Compare and Swap, 翻译成比较并交换。

看到这个定义,可以说是没有任何意义的一句话,但是确实最能概括CAS操作过程的一句话。

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该 位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前 值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”

以下这段JAVA代码,基本上反映了CAS操作的过程。但是请注意,真实的CAS操作是由CPU完成的,CPU会确保这个操作的原子性,CAS远非JAVA代码能实现的功能(下面我们会看到CAS的汇编代码)。

	/**
* 假设这段代码是原子性的,那么CAS其实就是这样一个过程
*/
public boolean compareAndSwap(int v,int a,int b) {
if (v == a) {
v = b;
return true;
}else {
return false;
}
}
 

通常将 CAS 用于同步的方式是从地址 V 读取值 A,执行多步计算来获得新 值 B,然后使用 CAS 将 V 的值从 A 改为 B。如果 V 处的值尚未同时更改,则 CAS 操作成功。

这段话的意思是,CAS操作可以防止内存中共享变量出现脏读脏写问题,多核的CPU在多线程的情况下经常出现的问题,通常我们采用锁来避免这个问题,但是CAS操作避免了多线程的竞争锁,上下文切换和进程调度。

类似于 CAS 的指令允许算法执行读-修改-写操作,而无需害怕其他线程同时 修改变量,因为如果其他线程修改变量,那么 CAS 会检测它(并失败),算法 可以对该操作重新计算。

2.JAVA中的CAS操作实现原理

CAS通过调用JNI的代码实现的。JNI:Java Native Interface为JAVA本地调用,允许java调用其他语言。

Unsafe类的compareAndSwapInt()方法为例来说,compareAndSwapInt就是借助C语言和汇编代码来实现的。

下面从分析比较常用的CPU(intel x86)来解释CAS的实现原理。

下面是JDK中sun.misc.Unsafe类的compareAndSwapInt()方法的源代码:

// native方法,是没有其Java代码实现的,而是需要依靠JDK和JVM的实现
public final native boolean compareAndSwapInt(Object o, long offset,
int expected,
int x);
 

可以看到这是个本地方法。这个本地方法在openjdk中依次调用的c++代码为:unsafe.cpp,atomic.cpp和atomicwindowsx86.inline.hpp。这个本地方法的最终实现在openjdk的如下位置:openjdk-7-fcs-src-b147-27jun2011\openjdk\hotspot\src\oscpu\windowsx86\vm\ atomicwindowsx86.inline.hpp(对应于windows操作系统,X86指令集)。下面是对应于intel x86处理器的源代码的片段:

// Adding a lock prefix to an instruction on MP machine
// VC++ doesn't like the lock prefix to be on a single line
// so we can't insert a label after the lock prefix.
// By emitting a lock prefix, we can define a label after it.
#define LOCK_IF_MP(mp) __asm cmp mp, 0 \
__asm je L0 \
__asm _emit 0xF0 \
__asm L0: inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value)
{
// alternative for InterlockedCompareExchange
int mp = os::is_MP();
__asm {
mov edx, dest
mov ecx, exchange_value
mov eax, compare_value
LOCK_IF_MP(mp) // 这里需要先进行判断是否为多核处理器
cmpxchg dword ptr [edx], ecx // 如果是多核处理器就会在这行指令前加Lock标记
}
}
 

2019.8.15补充:好的~我又回来了,不得不说写这篇文章当时属实浅尝辄止,现在需要解释一下这段汇编代码

  int mp = os::is_MP();

os::is_MP()会返回当前JVM运行所在机器是否为多核CPU,当然返回1代表true,0代表false

然后是一段内嵌汇编,C/C++支持内嵌汇编,大家知道这个特性就好,我来通俗易懂的解释一下这段汇编的大体意思。

  __asm {
mov edx, dest # 取Atomic::cmpxchg方法的参数dest内存地址存入寄存器edx
mov ecx, exchange_value # 取Atomic::cmpxchg方法的参数exchange_value内存地址存入寄存器ecx
mov eax, compare_value # 取Atomic::cmpxchg方法的参数compare_value内存地存入寄存器eax
LOCK_IF_MP(mp) # 如果是多核处理器,就在下一行汇编代码前加上lock前缀
cmpxchg dword ptr [edx], ecx # 比较ecx和eax的中内存地址的中存的变量值,如果相等就写入edx内存地址中,否则不
}
 

x86汇编指令cmpxchg本身保证了原子性,其实就是cpu的CAS操作的实现,那么问题来了,为什么保证了原子性还需要在多核处理器中加上lock前缀呢?

答案是:多核处理器中不能保证可见性,lock前缀可以保证这行汇编中所有值的可见性,这个问题的原因是多核CPU中缓存导致的(x86中罪魁祸首是store buffer的存在)。

这样通过lock前缀保障多核处理器的可见性,然后通过cmpxchg指令完成CPU上原子性的CAS操作,完美解决问题!

多说一句,这只是x86中的实现方式,对于其他平台,还是有不同的方式实现,这点希望读者一定要搞清楚。

这段汇编代码看不懂也没关系,但其大意是使用CPU的锁机制,确保了整个CAS操作的原子性。关于CPU中的锁机制和CPU的原子操作 ——CPU中的原子操作

3.concurrent包中CAS的应用

由于java的CAS同时具有 volatile 读和volatile写的内存语义,因此Java线程之间的通信现在有了下面四种方式:

  1. A线程写volatile变量,随后B线程读这个volatile变量。
  1. A线程写volatile变量,随后B线程用CAS更新这个volatile变量。
  1. A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量。
  1. A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量。

注:volatile 关键字保证了变量的可见性,根据JAVA内存模型,每一个线程都有自己的栈内存,不同线程的栈内存里的变量有可能因为栈内的操作而不同,而 CPU又是直接操作栈中的数据并保存在自己的缓存中,所以多核CPU就出现了很大的问题,而volatile修饰的变量,保证了CPU各个核心不会从栈内存中和 缓存中读数据,而是直接从堆内存中读数据,而且写操作会直接写回堆内存中,从而保证了多线程间共享变量的可见性和局部顺序性(但不保证原子性),关于volatile——Java并发编程:volatile关键字解析

Java的CAS操作可以实现现代CPU上硬件级别的原子指令(不是依靠JVM或者操作系统的锁机制),而同时volatile关键字又保证了线程间共享变量的可见性和指令的顺序性,因此凭借这两种手段,就可以实现不依靠操作系统实现的锁机制来保证并发时共享变量的一致性。

如果我们仔细分析concurrent包的源代码实现,会发现一个通用化的实现模式:

  1. 首先,声明共享变量为volatile;
  2. 然后,使用CAS的原子条件更新来实现线程之间的同步;
  3. 同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。

4.小结

其实本来还有更多的基础要讲一讲,但是这一篇博客不能太长了,关于JVM内存结构,JMM模型,还有volatile关键字和Java中原生的的同步锁.,这些以后希望能补全,当然也是我自己再次学习的过程记录下来。


参考资料:

  1. JAVA CAS原理深度分析——超多干货
  2. Java并发编程:volatile关键字解析

Java并发--Java中的CAS操作和实现原理的更多相关文章

  1. java高并发系列 - 第21天:java中的CAS操作,java并发的基石

    这是java高并发系列第21篇文章. 本文主要内容 从网站计数器实现中一步步引出CAS操作 介绍java中的CAS及CAS可能存在的问题 悲观锁和乐观锁的一些介绍及数据库乐观锁的一个常见示例 使用ja ...

  2. Java并发编程(您不知道的线程池操作), 最受欢迎的 8 位 Java 大师,Java并发包中的同步队列SynchronousQueue实现原理

    Java_并发编程培训 java并发程序设计教程 JUC Exchanger 一.概述 Exchanger 可以在对中对元素进行配对和交换的线程的同步点.每个线程将条目上的某个方法呈现给 exchan ...

  3. Java并发编程中的若干核心技术,向高手进阶!

    来源:http://www.jianshu.com/p/5f499f8212e7 引言 本文试图从一个更高的视角来总结Java语言中的并发编程内容,希望阅读完本文之后,可以收获一些内容,至少应该知道在 ...

  4. Java并发编程中的相关注解

    引自:http://www.cnblogs.com/phoebus0501/archive/2011/02/21/1960077.html Java并发编程中,用到了一些专门为并发编程准备的 Anno ...

  5. Java并发编程中的设计模式解析(二)一个单例的七种写法

    Java单例模式是最常见的设计模式之一,广泛应用于各种框架.中间件和应用开发中.单例模式实现起来比较简单,基本是每个Java工程师都能信手拈来的,本文将结合多线程.类的加载等知识,系统地介绍一下单例模 ...

  6. Go并发编程之美-CAS操作

    摘要: 一.前言 go语言类似Java JUC包也提供了一些列用于多线程之间进行同步的措施,比如低级的同步措施有 锁.CAS.原子变量操作类.相比Java来说go提供了独特的基于通道的同步措施.本节我 ...

  7. Java并发编程(七)ConcurrentLinkedQueue的实现原理和源码分析

    相关文章 Java并发编程(一)线程定义.状态和属性 Java并发编程(二)同步 Java并发编程(三)volatile域 Java并发编程(四)Java内存模型 Java并发编程(五)Concurr ...

  8. Java并发编程系列-(8) JMM和底层实现原理

    8. JMM和底层实现原理 8.1 线程间的通信与同步 线程之间的通信 线程的通信是指线程之间以何种机制来交换信息.在编程中,线程之间的通信机制有两种,共享内存和消息传递. 在共享内存的并发模型里,线 ...

  9. Java并发--Java线程面试题 Top 50

    原文链接:http://www.importnew.com/12773.html 不管你是新程序员还是老手,你一定在面试中遇到过有关线程的问题.Java语言一个重要的特点就是内置了对并发的支持,让Ja ...

随机推荐

  1. ACM-后序遍历(简单方法和正规方法)

    1.后序遍历简单方法 /**二叉树遍历一般有三种方法:前序,中序,后序.*其中前序遍历u顺序为:根->左子树->右子树,在此定义一种新的遍历方法:根->右子树->左子u树*使用 ...

  2. MySQL实战45讲学习笔记:第四十四讲

    一.引子 这是我们专栏的最后一篇答疑文章,今天我们来说说一些好问题. 在我看来,能够帮我们扩展一个逻辑的边界的问题,就是好问题.因为通过解决这样的问题,能够加深我们对这个逻辑的理解,或者帮我们关联到另 ...

  3. MySQL实战45讲学习笔记:第三十三讲

    一.引子 我经常会被问到这样一个问题:我的主机内存只有 100G,现在要对一个 200G 的大表做全表扫描,会不会把数据库主机的内存用光了? 这个问题确实值得担心,被系统 OOM(out of mem ...

  4. QPushButton 一组中凸显选中的一个,且只能选中一个。

    QButtonGroup * buttonGroup = new QButtonGroup(this); buttonGroup->setExclusive(true); ui->push ...

  5. 内核发送uevent的API,用户空间解析uevent(转)

    #include <stdio.h>#include <string.h>#include <sys/types.h>#include <unistd.h&g ...

  6. ​为什么我会选择走 Java 这条路?

    ​本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 https://github.com/h2pl/Java-Tutorial 喜欢的话麻烦点 ...

  7. JavaScript对象及初识面向对象

    一.对象 1.1对象是什么 对象是包含相关属性和方法的集合体 1.2什么是面向对象 面向对象仅仅是一个概念或者编程思想 通过一种叫做原型的方式来实现面向对象编程 二.创建对象 2.1自定义对象 2.1 ...

  8. python 中in 的 用法

    1.   作用为 成员运算符   在字符串内操作,如果字符串包含相关字符 则返回True,如果不包含则返回False   当然处理不单单是只有单个字符,多个连续的字符也是可以处理的 # 单个字符 a= ...

  9. 图解Hyperf框架:Hyperf 的初始化

  10. 使用LocalDateTime计算两个时间的差

    LocalDateTime now = LocalDateTime.now();System.out.println("计算两个时间的差:");LocalDateTime end ...