CAS (多线程保证原子性)
面试必问的CAS,你懂了吗?
概述
CAS(Compare-and-Swap),即比较并替换,是一种实现并发算法时常用到的技术,Java并发包中的很多类都使用了CAS技术。CAS也是现在面试经常问的问题,本文将深入的介绍CAS的原理。
案例
介绍CAS之前,我们先来看一个例子。
上面这个例子在volatile关键字详解文中用过,我们知道,运行完这段代码之后,并不会获得期望的结果,而且会发现每次运行程序,输出的结果都不一样,都是一个小于200000的数字。
通过分析字节码我们知道,这是因为volatile只能保证可见性,无法保证原子性,而自增操作并不是一个原子操作(如下图所示),在并发的情况下,putstatic指令可能把较小的race值同步回主内存之中,导致我们每次都无法获得想要的结果。那么,应该怎么解决这个问题了?
解决方法:
首先我们想到的是用synchronized来修饰increase方法。
使用synchronized修饰后,increase方法变成了一个原子操作,因此是肯定能得到正确的结果。但是,我们知道,每次自增都进行加锁,性能可能会稍微差了点,有更好的方案吗?
答案当然是有的,这个时候我们可以使用Java并发包原子操作类(Atomic开头),例如以下代码。
我们将例子中的代码稍做修改:race改成使用AtomicInteger定义,“race++”改成使用“race.getAndIncrement()”,AtomicInteger.getAndIncrement()是原子操作,因此我们可以确保每次都可以获得正确的结果,并且在性能上有不错的提升(针对本例子,在JDK1.8.0_151下运行)。
通过方法调用,我们可以发现,getAndIncrement方法调用getAndAddInt方法,最后调用的是compareAndSwapInt方法,即本文的主角CAS,接下来我们开始介绍CAS。
getAndAddInt方法解析:拿到内存位置的最新值v,使用CAS尝试修将内存位置的值修改为目标值v+delta,如果修改失败,则获取该内存位置的新值v,然后继续尝试,直至修改成功。
CAS是什么?
CAS是英文单词CompareAndSwap的缩写,中文意思是:比较并替换。CAS需要有3个操作数:内存地址V,旧的预期值A,即将要更新的目标值B。
CAS指令执行时,当且仅当内存地址V的值与预期值A相等时,将内存地址V的值修改为B,否则就什么都不做。整个比较并替换的操作是一个原子操作。
源码分析
上面源码分析时,提到最后调用了compareAndSwapInt方法,接着继续深入探讨该方法,该方法在Unsafe中对应的源码如下。
可以看到调用了“Atomic::cmpxchg”方法,“Atomic::cmpxchg”方法在linux_x86和windows_x86的实现如下。
linux_x86的实现:
windows_x86的实现:
Atomic::cmpxchg方法解析:
mp是“os::is_MP()”的返回结果,“os::is_MP()”是一个内联函数,用来判断当前系统是否为多处理器。
- 如果当前系统是多处理器,该函数返回1。
- 否则,返回0。
LOCK_IF_MP(mp)会根据mp的值来决定是否为cmpxchg指令添加lock前缀。
- 如果通过mp判断当前系统是多处理器(即mp值为1),则为cmpxchg指令添加lock前缀。
- 否则,不加lock前缀。
这是一种优化手段,认为单处理器的环境没有必要添加lock前缀,只有在多核情况下才会添加lock前缀,因为lock会导致性能下降。cmpxchg是汇编指令,作用是比较并交换操作数。
intel手册对lock前缀的说明如下:
- 确保对内存的读-改-写操作原子执行。在Pentium及Pentium之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其他处理器暂时无法通过总线访问内存。很显然,这会带来昂贵的开销。从Pentium
4,Intel Xeon及P6处理器开始,intel在原有总线锁的基础上做了一个很有意义的优化:如果要访问的内存区域(area of
memory)在lock前缀指令执行期间已经在处理器内部的缓存中被锁定(即包含该内存区域的缓存行当前处于独占或以修改状态),并且该内存区域被完全包含在单个缓存行(cacheline)中,那么处理器将直接执行该指令。由于在指令执行期间该缓存行会一直被锁定,其它处理器无法读/写该指令要访问的内存区域,因此能保证指令执行的原子性。这个操作过程叫做缓存锁定(cache
locking),缓存锁定将大大降低lock前缀指令的执行开销,但是当多处理器之间的竞争程度很高或者指令访问的内存地址未对齐时,仍然会锁住总线。
- 禁止该指令与之前和之后的读和写指令重排序。
- 把写缓冲区中的所有数据刷新到内存中。
上面的第1点保证了CAS操作是一个原子操作,第2点和第3点所具有的内存屏障效果,保证了CAS同时具有volatile读和volatile写的内存语义。
CAS的缺点:
CAS虽然很高效的解决了原子操作问题,但是CAS仍然存在三大问题。
- 循环时间长开销很大。
- 只能保证一个共享变量的原子操作。
- ABA问题。
循环时间长开销很大:
我们可以看到getAndAddInt方法执行时,如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。
只能保证一个共享变量的原子操作:
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
什么是ABA问题?ABA问题怎么解决?
如果内存地址V初次读取的值是A,并且在准备赋值的时候检查到它的值仍然为A,那我们就能说它的值没有被其他线程改变过了吗?
如果在这段期间它的值曾经被改成了B,后来又被改回为A,那CAS操作就会误认为它从来没有被改变过。这个漏洞称为CAS操作的“ABA”问题。Java并发包为了解决这个问题,提供了一个带有标记的原子引用类“AtomicStampedReference”,它可以通过控制变量值的版本来保证CAS的正确性。因此,在使用CAS前要考虑清楚“ABA”问题是否会影响程序并发的正确性,如果需要解决ABA问题,改用传统的互斥同步可能会比原子类更高效。
CAS (多线程保证原子性)的更多相关文章
- Java CAS同步机制 原理详解(为什么并发环境下的COUNT自增操作不安全): Atomic原子类底层用的不是传统意义的锁机制,而是无锁化的CAS机制,通过CAS机制保证多线程修改一个数值的安全性。
精彩理解: https://www.jianshu.com/p/21be831e851e ; https://blog.csdn.net/heyutao007/article/details/19 ...
- JUC 并发编程--05, Volatile关键字特性: 可见性, 不保证原子性,禁止指令重排, 代码证明过程. CAS了解么 , ABA怎么解决, 手写自旋锁和死锁
问: 了解volatile关键字么? 答: 他是java 的关键字, 保证可见性, 不保证原子性, 禁止指令重排 问: 你说的这三个特性, 能写代码证明么? 答: .... 问: 听说过 CAS么 他 ...
- 为什么volatile不能保证原子性而Atomic可以?
在上篇<非阻塞同步算法与CAS(Compare and Swap)无锁算法>中讲到在Java中long赋值不是原子操作,因为先写32位,再写后32位,分两步操作,而AtomicLong赋值 ...
- volatile之一--volatile不能保证原子性
Java语言是支持多线程的,为了解决线程并发的问题,在语言内部引入了 同步块 和 volatile 关键字机制在java线程并发处理中,有一个关键字volatile的使用目前存在很大的混淆,以为使用这 ...
- 为什么volatile不能保证原子性?
为什么volatile能替代简单的锁,却不能保证原子性?这里面涉及volatile,是java中的一个我觉得这个词在Java规范中从未被解释清楚的神奇关键词,在Sun的JDK官方文档是这样形容vola ...
- Volatile不保证原子性(二)
Volatile不保证原子性 前言 通过前面对JMM的介绍,我们知道,各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存进行操作后在写回到主内存中的. 这就可能存在一个线程AAA修改 ...
- 为什么volatile能保证有序性不能保证原子性
对于内存模型的三大特性:有序性.原子性.可见性. 大家都知道volatile能保证可见性和有序性但是不能保证原子性,但是为什么呢? 一.原子性.有序性.可见性 1.原子性: (1)原子的意思代表着-- ...
- volatile实现可见性但不保证原子性
volatile实现可见性但不保证原子性 volatile关键字: 能够保证volatile变量的可见性 不能保证volatile变量复合操作的原子性 volatile如何实现内存可见性: 深入来说: ...
- 【转】为什么volatile不能保证原子性而Atomic可以?
直接上好文链接!!! 为什么volatile不能保证原子性而Atomic可以?
随机推荐
- python之路--关于线程的一些方法
一 . 线程的两种创建方式 from threading import Thread # 第一种创建方式 def f1(n): print('%s号线程任务'%n) def f2(n): print( ...
- 如何使用 Yum Repository 安装指定版本的 MySQL
自从从使用 debian 系的 apt-get 转到使用 yum 工具之后一直不是很习惯,也没有去看过很多工具包安装的时候到底影响到了哪些文件等.这次借这次社区版 MySQL 安装来一并梳理一下. 首 ...
- Golang的面向对象实践method
最近在系统的学习go的语法,一切都弄好了之后准备弄个im项目出来玩.在这个过程中会把看到有趣的写法和语法啥的拿出来分析一下. 我一直以为go语言对面向对象没有支持,但是后面看到了类似类的概念,meth ...
- MySQL系列:视图基本操作(3)
1. 视图简介 1.1 视图定义 视图是一种虚拟的表,是从数据库中一个或多个表中导出来的表. 视图可以从已存在的视图的基础上定义. 数据库中只存放视图的定义,并没有存放视图中的数据,数据存放在原来的表 ...
- CSS3 flexbox 布局 ---- flex 容器属性介绍
flexbox布局是CSS3中新增的属性,它可以很轻松地帮我们解决掉一些常见的布局问题,比如导航栏. 我们用普通的方法写导航栏,通常会在ul, li 结构写好后,让li 元素左浮动,然后再给ul 清浮 ...
- Nginx 假如reload或reopen时发生错误如何解决
配置Nginx 如果reload 或 quit发生不存在文件的时候 重新编译下即可 ./nginx -c /usr/local/webserver/nginx/conf/nginx.conf //重 ...
- JAVA优先级队列元素输出顺序测试
package code.test; import java.util.Comparator; import java.util.Iterator; import java.util.Priority ...
- BZOJ 2049 洞穴勘测
LCT判断联通性 没什么特别的..还是一个普通的板子题,把LCT当并查集用了,只不过LCT灵活一些,还可以断边 话说自从昨天被维修数列那题榨干之后我现在写splay都不用动脑子了,,机械式的码spla ...
- IDEA+Maven+多个Module模块(创建多模块SpringBoot整合项目)
最近在学习springboot,先从创建项目开始,一般项目都是一个项目下会有多个模块,这里先创建一个最简单的实例,一个项目下有一个springboot模块项目提供web服务,引用另一个java项目(相 ...
- bzoj1559 [JSOI2009]密码
题目链接:[JSOI2009]密码 我们先看第一问:输出方案数 我们把所有给出来的串丢到AC自动机里面去,然后在建出来的\(trie\)图上跑dp 由于\(n\leq 10\)我们很自然的就想到了状压 ...