并发编程之 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这样的操作在并发情况下是否会有问题 ...
随机推荐
- yum-阿里源配置
原文:https://opsx.alibaba.com/mirrorCentOS 1.备份 mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/ ...
- C# 简述Action与function
Action 与 Func是.NET类库中增加的内置委托,以便更加简洁方便的使用委托. 最初使用委托时,均需要先定义委托类型,然后定义一个符合委托类型签名的函数,在调用前,需声明并创建委托对象,将指定 ...
- 解决SHAREJPOINT 跨域问题
目前仅支持IE7/8不支持IE11和谷歌 对于跨域情况,目前找到如果jquery是get获取方式,可以配置web.config相关属性,具体powershell命令如下: Add-PSSnapin M ...
- [翻译]Elasticsearch重要文章之四:监控每个节点(jvm部分)
http://zhaoyanblog.com/archives/753.html 操作系统和进程部分 操作系统和进程部分的含义是很清楚的,这里不会描述的很详细.他们列出了基本的资源统计,例如CPU和负 ...
- Web 前端 注意知识点
一. 前端使用技巧: 1. button的用法.在使用按钮时可以自由在内设置style属性,来改变形态.可以给予type=sbumit提交属性. 2. 各种使用符号: # <!--小于 大于 ...
- 【sping揭秘】3、Spring容器中bean默认是保持一个实例
Spring容器中bean默认是保持一个实例 这里做一个测试,基础代码 package cn.cutter.start.provider; import org.springframework.con ...
- 【tomcat】servlet原理及其生命周期
1.什么是servlet? Servlet(Servlet Applet),全称Java Servlet,是用Java编写的服务器端程序.而这些Servlet都要实现Servlet这个接口.其主要功能 ...
- vue教程3-06 vue路由嵌套(多层路由),路由其他信息
多层嵌套: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF ...
- 匿名类、包、权限修饰符_DAY10
1:内部类(理解) (1)把类定义在一个类的内部. (2)特点: A:内部类可以直接使用外部类的成员,包括私有. B:外部类要使用内部类成员,必须创建对象使用. 例子: public c ...
- Linux发邮件
一.mail命令 1.配置 vim /etc/mail.rc 文件尾增加以下内容 set from=1968089885@qq.com smtp="smtp.qq.com"set ...