Java并发/多线程-CAS原理分析
什么是CAS
CAS 即 compare and swap,比较并交换。
CAS是一种原子操作,同时 CAS 使用乐观锁机制。
J.U.C中的很多功能都是建立在 CAS 之上,各种原子类,其底层都用 CAS来实现原子操作。用来解决并发时的安全问题。
并发安全问题
举一个典型的例子i++
public class AddTest {
  public volatile int i;
  public void add() {
    i++;
  }
}
通过javap -c AddTest可以看到add 方法的字节码指令:
public void add();
    Code:
       0: aload_0
       1: dup
       2: getfield      #2                  // Field i:I
       5: iconst_1
       6: iadd
       7: putfield      #2                  // Field i:I
      10: return
i++被拆分成了多个指令:
- 执行getfield拿到原始内存值;
- 执行iadd进行加 1 操作;
- 执行putfield写把累加后的值写回内存。
假设一种情况:
- 当线程 1执行到iadd时,由于还没有执行putfield,这时候并不会刷新主内存区中的值。
- 此时线程 2进入开始运行,刚刚将主内存区的值拷贝到私有内存区。
- 线程 1正好执行- putfield,更新主内存区的值,那么此时- 线程 2的副本就是旧的了。错误就出现了。
如何解决?
最简单的,在 add 方法加上 synchronized 。
public class AddTest {
  public volatile int i;
  public synchronized void add() {
    i++;
  }
}
虽然简单,并且解决了问题,但是性能表现并不好。
最优的解法应该是使用JDK自带的CAS方案,如上例子,使用AtomicInteger类
public class AddIntTest {
  public AtomicInteger i;
  public void add() {
    i.getAndIncrement();
  }
}
底层原理
CAS 的原理并不复杂:
- 三个参数,一个当前内存值 V、预期值 A、更新值 B
- 当且仅当预期值 A 和内存值 V 相同时,将内存值修改为 B 并返回 true
- 否则什么都不做,并返回 false
拿 AtomicInteger 类分析,先来看看源码:
我这里的环境是Java11,如果是Java8这里一些内部的一些命名有些许不同。
public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;
    /*
     * This class intended to be implemented using VarHandles, but there
     * are unresolved cyclic startup dependencies.
     */
    private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();
    private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");
    private volatile int value;
		//...
}
Unsafe 类,该类对一般开发而言,少有用到。
Unsafe 类底层是用 C/C++ 实现的,所以它的方式都是被 native 关键字修饰过的。
它可以提供硬件级别的原子操作,如获取某个属性在内存中的位置、修改对象的字段值。
关键点:
- AtomicInteger类存储的值在- value字段中,而- value字段被- volatile
- 在静态代码块中,并且获取了 - Unsafe实例,获取了- value字段在内存中的偏移量- VALUE
接下回到刚刚的例子:
如上,getAndIncrement() 方法底层利用 CAS 技术保证了并发安全。
public final int getAndIncrement() {
  return U.getAndAddInt(this, VALUE, 1);
}
getAndAddInt() 方法:
public final int getAndAddInt(Object o, long offset, int delta) {
  int v;
  do {
    v = getIntVolatile(o, offset);
  } while (!weakCompareAndSetInt(o, offset, v, v + delta));
  return v;
}
v 通过 getIntVolatile(o, offset)方法获取,其目的是获取 o 在 offset 偏移量的值,其中 o 就是 AtomicInteger 类存储的值,即value, offset 内存偏移量的值,即 VALUE。
重点,weakCompareAndSetInt 就是实现 CAS 的核心方法
- 如果  o和v相等,就证明没有其他线程改变过这个变量,那么就把v值更新为v + delta,其中delta是更新的增量值。
- 反之 CAS 就一直采用自旋的方式继续进行操作,这一步也是一个原子操作。
分析:
- 设定 AtomicInteger的原始值为 A,线程 1和线程 2各自持有一份副本,值都是 A。
- 线程 1通过- getIntVolatile(o, offset)拿到 value 值 A,这时- 线程 1被挂起。
- 线程 2也通过- getIntVolatile(o, offset)方法获取到 value 值 A,并执行- weakCompareAndSetInt方法比较内存值也为 A,成功修改内存值为 B。
- 这时线程 1恢复执行weakCompareAndSetInt方法比较,发现自己手里的值 A 和内存的值 B 不一致,说明该值已经被其它线程提前修改过了。
- 线程 1重新执行- getIntVolatile(o, offset)再次获取 value 值,因为变量 value 被 volatile 修饰,具有可见性,线程A继续执行- weakCompareAndSetInt进行比较替换,直到成功
CAS需要注意的问题
使用限制
CAS是由CPU支持的原子操作,其原子性是在硬件层面进行保证的,在Java中普通用户无法直接使用,只能借助atomic包下的原子类使用,灵活性受限。
但是CAS只能保证单个变量操作的原子性,当涉及到多个变量时,CAS无能为力。
原子性也不一定能保证线程安全,如在Java中需要与volatile配合来保证线程安全。
ABA 问题
概念
CAS 有一个问题,举例子如下:
- 线程 1从内存位置 V 取出 A
- 这时候线程 2也从内存位置 V 取出 A
- 此时线程 1处于挂起状态,线程 2将位置 V 的值改成 B,最后再改成 A
- 这时候线程 1再执行,发现位置 V 的值没有变化,符合期望继续执行。
此时虽然线程 1还是成功了,但是这并不符合我们真实的期望,等于线程 2狸猫换太子把线程 1耍了。
这就是所谓的ABA问题
解决方案
引入原子引用,带版本号的原子操作。
把我们的每一次操作都带上一个版本号,这样就可以避免ABA问题的发生。既乐观锁的思想。
- 内存中的值每发生一次变化,版本号都更新。 
- 在进行CAS操作时,比较内存中的值的同时,也会比较版本号,只有当二者都没有变化时,才能执行成功。 
- Java中的 - AtomicStampedReference类便是使用版本号来解决ABA问题的。
高竞争下的开销问题
- 在并发冲突概率大的高竞争环境下,如果CAS一直失败,会一直重试,CPU开销较大。 
- 针对这个问题的一个思路是引入退出机制,如重试次数超过一定阈值后失败退出。 
- 更重要的是避免在高竞争环境下使用乐观锁。 
Java并发/多线程-CAS原理分析的更多相关文章
- java并发 - 自底向上的原理分析
		[TOC] 事先声明,我只是java并发的新手,这篇文章也只是我阅读<java并发编程的艺术>一书(内容主要涉及前3章)的一些总结和感悟.希望大家能多多讨论,对于错误的地方还请指出. 0. ... 
- java并发多线程显式锁Condition条件简介分析与监视器 多线程下篇(四)
		Lock接口提供了方法Condition newCondition();用于获取对应锁的条件,可以在这个条件对象上调用监视器方法 可以理解为,原本借助于synchronized关键字以及锁对象,配备了 ... 
- 原子类java.util.concurrent.atomic.*原理分析
		原子类java.util.concurrent.atomic.*原理分析 在并发编程下,原子操作类的应用可以说是无处不在的.为解决线程安全的读写提供了很大的便利. 原子类保证原子的两个关键的点就是:可 ... 
- java 并发多线程 锁的分类概念介绍 多线程下篇(二)
		接下来对锁的概念再次进行深入的介绍 之前反复的提到锁,通常的理解就是,锁---互斥---同步---阻塞 其实这是常用的独占锁(排它锁)的概念,也是一种简单粗暴的解决方案 抗战电影中,经常出现为了阻止日 ... 
- JAVA常用数据结构及原理分析
		JAVA常用数据结构及原理分析 http://www.2cto.com/kf/201506/412305.html 前不久面试官让我说一下怎么理解java数据结构框架,之前也看过部分源码,balaba ... 
- java 并发多线程显式锁概念简介 什么是显式锁 多线程下篇(一)
		目前对于同步,仅仅介绍了一个关键字synchronized,可以用于保证线程同步的原子性.可见性.有序性 对于synchronized关键字,对于静态方法默认是以该类的class对象作为锁,对于实例方 ... 
- (6)Java数据结构-- 转:JAVA常用数据结构及原理分析
		JAVA常用数据结构及原理分析 http://www.2cto.com/kf/201506/412305.html 前不久面试官让我说一下怎么理解java数据结构框架,之前也看过部分源码,balab ... 
- Java并发-volatile的原理及用法
		Java并发-volatile的原理及用法 volatile属性:可见性.保证有序性.不保证原子性.一.volatile可见性 在Java的内存中所有的变量都存在主内存中,每个线程有单独CPU缓存内存 ... 
- Java NIO使用及原理分析 (四)
		在上一篇文章中介绍了关于缓冲区的一些细节内容,现在终于可以进入NIO中最有意思的部分非阻塞I/O.通常在进行同步I/O操作时,如果读取数据,代码会阻塞直至有 可供读取的数据.同样,写入调用将会阻塞直至 ... 
随机推荐
- 使用OpenSSL自建一个HTTPS服务
			1. 理论知识 1.1 什么是https 传统的 HTTP 协议以明文方式进行通信,不提供任何方式的数据加密,很容易被中间攻击者破解通信内容或者伪装成服务器与客户端通信,在安全性上存在很大问题. HT ... 
- 得物(毒)APP,8位抽奖码需求,这不就是产品给我留的数学作业!
			作者:小傅哥 博客:https://bugstack.cn Github:https://github.com/fuzhengwei/CodeGuide/wiki 沉淀.分享.成长,让自己和他人都能有 ... 
- AWT02-ContainerAPI
			1.体系 Object -Component -Container Window:窗口容器 Frame:创建窗口 Dialog:创建对话框 Panel:内嵌容器 Applet ScrollPane:含 ... 
- Object.assign  之后 点对象  找不到
			export function CopyObject(val) { return JSON.parse(JSON.stringify(val)); } 
- js上 二十、综合案例
			二十.综合案例 题目一: **1. ** 数组随机 描述,写randomArray函数,传递一个数组,传递一个数值,返回一个指定个数的随机的新数组,不允许有重复数据 用例: randomArray([ ... 
- 项目1_001_涉及知识点(Django任务追踪平台)
- 顶会两篇论文连发,华为云医疗AI低调中崭露头角
			摘要:2020年国际医学图像计算和计算机辅助干预会议(MICCAI 2020),论文接收结果已经公布.华为云医疗AI团队和华中科技大学合作的2篇研究成果入选. 同时两篇研究成果被行业顶会收录,华为云医 ... 
- asp.net webapi关闭https配置
			将s去掉就行 
- 前端可视化开发--liveload
			在前端开发中,我们会频繁的修改html.css.js,然后刷新页面,开效果,再调整,再刷新,不知不觉会浪费掉我们很多时间.有没有什么方法,我在编辑器里面改了代码以后,只要保存,浏览器就能实时刷新.经过 ... 
- Json串的字段如果和类中字段不一致,如何映射、转换?
			Json串是我们现在经常会遇到的一种描述对象的字符串格式.在用Java语言开发的功能中,也经常需要做Json串与Java对象之间的转换. fastjson就是经常用来做Json串与Java对象之间的转 ... 
