并发编程之 CAS 的原理

前言
在并发编程中,锁是消耗性能的操作,同一时间只能有一个线程进入同步块修改变量的值,比如下面的代码
synchronized void function(int b){
a = a + b;
}
如果不加 synchronized 的话,多线程修改 a 的值就会导致结果不正确,出现线程安全问题。但锁又是要给耗费性能的操作。不论是拿锁,解锁,还是等待锁,阻塞,都是非常耗费性能的。那么能不能不加锁呢?
可以。
什么意思呢?我们看上面的代码,分为几个步骤:
- 读取a
- 将 a 和 b 相加
- 将计算的值赋值给a。
我们知道,这不是一个原子的操作,多线程上面时候会出问题:当两个线程同时访问 a ,都得到了a 的值,并且通知对a 加 1,然后同时将计算的值赋值给a,这样就会导致 a 的值只增加了1,但实际上我们想加 2.
问题出在哪里?第三步,对 a 赋值操作,如果有一种判断,判断 a 已经别的线程修改,你需要重新计算。比如下面这样:
void function(int b) {
int backup = a;
int c = a + b;
compareAndSwap(a, backup, c);
}
void compareAndSwap(int backup ,int c ){
if (a == backup) {
a = c;
}
}
从代码中,我们看到,我们备份了 a 的值,并且对 a 进行计算,如果 a 的值和备份的值一致,说明 a 没有被别的线程更改过,这个时候就可以进行修改了。
这里有个问题:compareAndSwap 方法有多步操作,不是原子的,并且没有使用锁,如何保证线程安全。其实楼主这里只是伪代码。下面就要好好说说什么是 CAS (compareAndSwap);
1. 什么是 CAS
CAS (compareAndSwap),中文叫比较交换,一种无锁原子算法。过程是这样:它包含 3 个参数 CAS(V,E,N),V表示要更新变量的值,E表示预期值,N表示新值。仅当 V值等于E值时,才会将V的值设为N,如果V值和E值不同,则说明已经有其他线程做两个更新,则当前线程则什么都不做。最后,CAS 返回当前V的真实值。CAS 操作时抱着乐观的态度进行的,它总是认为自己可以成功完成操作。
当多个线程同时使用CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。失败的线程不会挂起,仅是被告知失败,并且允许再次尝试,当然也允许实现的线程放弃操作。基于这样的原理,CAS 操作即使没有锁,也可以发现其他线程对当前线程的干扰。
与锁相比,使用CAS会使程序看起来更加复杂一些,但由于其非阻塞的,它对死锁问题天生免疫,并且,线程间的相互影响也非常小。更为重要的是,使用无锁的方式完全没有锁竞争带来的系统开销,也没有线程间频繁调度带来的开销,因此,他要比基于锁的方式拥有更优越的性能。
简单的说,CAS 需要你额外给出一个期望值,也就是你认为这个变量现在应该是什么样子的。如果变量不是你想象的那样,哪说明它已经被别人修改过了。你就需要重新读取,再次尝试修改就好了。
那么这个CAS 是如何实现的呢?也就是说,比较和交换实际上是两个操作,如何变成一个原子操作呢?
2. CAS 底层原理
这样归功于硬件指令集的发展,实际上,我们可以使用同步将这两个操作变成原子的,但是这么做就没有意义了。所以我们只能靠硬件来完成,硬件保证一个从语义上看起来需要多次操作的行为只通过一条处理器指令就能完成。这类指令常用的有:
- 测试并设置(Tetst-and-Set)
- 获取并增加(Fetch-and-Increment)
- 交换(Swap)
- 比较并交换(Compare-and-Swap)
- 加载链接/条件存储(Load-Linked/Store-Conditional)
其中,前面的3条是20世纪时,大部分处理器已经有了,后面的2条是现代处理器新增的。而且这两条指令的目的和功能是类似的,在IA64,x86 指令集中有 cmpxchg 指令完成 CAS 功能,在 sparc-TSO 也有 casa 指令实现,而在 ARM 和 PowerPC 架构下,则需要使用一对 ldrex/strex 指令来完成 LL/SC 的功能。
CPU 实现原子指令有2种方式:
通过总线锁定来保证原子性。
总线锁定其实就是处理器使用了总线锁,所谓总线锁就是使用处理器提供的一个 LOCK# 信号,当一个处理器咋总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。但是该方法成本太大。因此有了下面的方式。通过缓存锁定来保证原子性。
所谓 缓存锁定 是指内存区域如果被缓存在处理器的缓存行中,并且在Lock 操作期间被锁定,那么当他执行锁操作写回到内存时,处理器不在总线上声言 LOCK# 信号,而时修改内部的内存地址,并允许他的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改两个以上处理器缓存的内存区域数据(这里和 volatile 的可见性原理相同),当其他处理器回写已被锁定的缓存行的数据时,会使缓存行无效。
注意:有两种情况下处理器不会使用缓存锁定。
- 当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行时,则处理器会调用总线锁定。
- 有些处理器不支持缓存锁定,对于 Intel 486 和 Pentium 处理器,就是锁定的内存区域在处理器的缓存行也会调用总线锁定。
3. Java 如何实现原子操作
java 在 1.5 版本中提供了 java.util.concurrent.atomic 包,该包下所有的类都是原子操作:

如何使用呢?看代码
public static void main(String[] args) throws InterruptedException {
AtomicInteger integer = new AtomicInteger();
System.out.println(integer.get());
Thread[] threads = new Thread[1000];
for (int j = 0; j < 1000; j++) {
threads[j] = new Thread(() ->
integer.incrementAndGet()
);
threads[j].start();
}
for (int j = 0; j < 1000; j++) {
threads[j].join();
}
System.out.println(integer.get());
}
}
上面的代码,我们启动了1000个线程对 AtomicInteger 变量做了自增操作。结果是我们预期的1000,表示没有发生同步问题。
我们看看他的内部实现,我们找到该类的 compareAndSet 方法,也就是比较并且设置。我们看看该方法实现:

该方法调用了 unsafe 类的 compareAndSwapInt 方法,有几个参数,一个是该变量的内存地址,一个是期望值,一个是更新值,一个是对象自身。完全符合我们之前CAS 的定义。那么 ,这个 unsafe 又是什么呢?
该类在 rt.jar 包中,但不在我们熟悉的 java 包下,而是 sun.misc 包下。并且都是 class 文件,注释都没有,符合他的名字:不安全。
我们能构造他吗?不能,除非反射。
我们看看他的源码:


getUnsafe 方法中,会检查调用 getUnsafe 方法的类,如果这个类的 ClassLoader 不为null ,就直接抛出异常,什么情况下会为null呢?当类加载器是 Bootstrap 加载器的时候,Bootstrap 加载器是没有对象的,也就是说,加载这个类极有可能是 rt.jar 下的。
而在最新的 Java 9 当中,该类已经被隐藏。因为该类使用了指针。但指针的缺点就是不安全。
4. CAS 的缺点
CAS 看起来非常的吊,但是,他仍然有缺点,最著名的就是 ABA 问题,假设一个变量 A ,修改为 B之后又修改为 A,CAS 的机制是无法察觉的,但实际上已经被修改过了。如果在基本类型上是没有问题的,但是如果是引用类型呢?这个对象中有多个变量,我怎么知道有没有被改过?聪明的你一定想到了,加个版本号啊。每次修改就检查版本号,如果版本号变了,说明改过,就算你还是 A,也不行。
在 java.util.concurrent.atomic 包中,就有 AtomicReference 来保证引用的原子性,但楼主觉得有点鸡肋,不如使用同步加互斥,可能会更加高效。
总结
今天我们从各种角度理解了CAS 的原理,该算法特别的重要,从CPU 都特别的设计一条指令来实现可见一斑。而JDK的源码中,到处都 unSafe 的 CAS 算法,可以说,如果没有CAS ,就没有 1.5 的并发容器。好,今天就到这里。
good luck !!!
并发编程之 CAS 的原理的更多相关文章
- Java并发编程之CAS第一篇-什么是CAS
Java并发编程之CAS第一篇-什么是CAS 通过前面几篇的学习,我们对并发编程两个高频知识点了解了其中的一个—volatitl.从这一篇文章开始,我们将要学习另一个知识点—CAS.本篇是<凯哥 ...
- Java并发编程之CAS二源码追根溯源
Java并发编程之CAS二源码追根溯源 在上一篇文章中,我们知道了什么是CAS以及CAS的执行流程,在本篇文章中,我们将跟着源码一步一步的查看CAS最底层实现原理. 本篇是<凯哥(凯哥Java: ...
- Java并发编程之CAS第三篇-CAS的缺点及解决办法
Java并发编程之CAS第三篇-CAS的缺点 通过前两篇的文章介绍,我们知道了CAS是什么以及查看源码了解CAS原理.那么在多线程并发环境中,的缺点是什么呢?这篇文章我们就来讨论讨论 本篇是<凯 ...
- Java并发编程之CAS
CAS(Compare and swap)比较和替换是设计并发算法时用到的一种技术.简单来说,比较和替换是使用一个期望值和一个变量的当前值进行比较,如果当前变量的值与我们期望的值相等,就使用一个新值替 ...
- 并发编程之CAS(二)
更多Android架构进阶视频学习请点击:https://space.bilibili.com/474380680本篇文章将从以下几个内容来阐述CAS: [CAS原理] [CAS带来的ABA问题] 一 ...
- 并发编程之 Exchanger 源码分析
前言 JUC 包中除了 CountDownLatch, CyclicBarrier, Semaphore, 还有一个重要的工具,只不过相对而言使用的不多,什么呢? Exchange -- 交换器.用于 ...
- 并发编程之J.U.C的第二篇
并发编程之J.U.C的第二篇 3.2 StampedLock 4. Semaphore Semaphore原理 5. CountdownLatch 6. CyclicBarrier 7.线程安全集合类 ...
- 并发编程之J.U.C的第一篇
并发编程之J.U.C AQS 原理 ReentrantLock 原理 1. 非公平锁实现原理 2)可重入原理 3. 可打断原理 5) 条件变量实现原理 3. 读写锁 3.1 ReentrantRead ...
- 并发编程之:Atomic
大家好,我是小黑,一个在互联网苟且偷生的农民工. 在开始讲今天的内容之前,先问一个问题,使用int类型做加减操作是不是线程安全的呢?比如 i++ ,++i,i=i+1这样的操作在并发情况下是否会有问题 ...
随机推荐
- 编写高质量iOS与OS X代码的52个有效方法
第一章重点: 第一条:OC的起源 OC由smalltalk语言演化而来的语言为消息结构(messaging structure)语言,其运行时所因执行的的代码由运行环境来决定:函数调用(functio ...
- Delphi中Unicode转中文
function UnicodeToChinese(inputstr: string): string; var i: Integer; index: Integer; temp, top, last ...
- 探索基于.NET下实现一句话木马之ashx篇
0x01 前言 在渗透测试的时候各种PHP版的一句话木马已经琳琅满目,而.NET平台下的一句话木马则百年不变,最常见的当属下面这句 笔者感觉有必要挖坑一下.NET平台里的一句话木马,经过一番摸索填坑终 ...
- SignalR 设计理念(一)
SignalR 设计理念(一) 实现客户端和服务器端的实时通讯. 问题阐述 客户端提供的方法不确定! 客户端的方法参数不确定! 不同的名称和参数要分别调用指定的方法! 调用客户端方法时,忽略大小写! ...
- C#项目 学生选课系统 C#窗口 Winform项目 项目源码及使用说明
这是一个学生选课信息管理系统,使用VS2010+SQL2008编写,VS2017正常使用. 项目源码下载地址 https://gitee.com/whuanle/xkgl 笔者录了两个视频,打开项目源 ...
- C#后台代码获取程序集资源文件
资源会被打包在程序集内部. 选择这种生成方式后,该资源文件会被嵌入到该应用的程序集中,就是说打开生成的应用程序目录是看不到这个文件的. 可以用相对于当前的XAML文件的相对Uri访问,<Imag ...
- wpf 的依赖属性只能在loaded 事件之后才能取到
wpf 的依赖属性只能在loaded 事件之后才能取到,在构造函数的 InitializeComponent(); 之后取不到 wpf 的依赖属性只能在loaded 事件之后才能取到,在构造函数的 ...
- Asp.Net Mvc异步上传文件的方式
今天试了下mvc自带的ajax,发现上传文件时后端action接收不到文件, Request.Files和HttpPostedFileBase都接收不到.....后来搜索了下才知道mvc自带的Ajax ...
- 背水一战 Windows 10 (40) - 控件(导航类): AppBar, CommandBar
[源码下载] 背水一战 Windows 10 (40) - 控件(导航类): AppBar, CommandBar 作者:webabcd 介绍背水一战 Windows 10 之 控件(导航类) App ...
- TmsTimeUtils 时间戳
package com.sprucetec.tms.utils; import java.math.BigDecimal;import java.text.DateFormat;import java ...