并发编程的基石——CAS机制
本博客系列是学习并发编程过程中的记录总结。由于文章比较多,写的时间也比较散,所以我整理了个目录贴(传送门),方便查阅。
Java中提供了很多原子操作类来保证共享变量操作的原子性。这些原子操作的底层原理都是使用了CAS机制。在使用一门技术之前,了解这个技术的底层原理是非常重要的,所以本篇博客就先来讲讲什么是CAS机制,CAS机制存在的一些问题以及在Java中怎么使用CAS机制。
其实Java并发框架的基石一共有两块,一块是本文介绍的CAS,另一块就是AQS,后续也会写博客介绍。
什么是CAS机制
CAS机制是一种数据更新的方式。在具体讲什么是CAS机制之前,我们先来聊下在多线程环境下,对共享变量进行数据更新的两种模式:悲观锁模式和乐观锁模式。
悲观锁更新的方式认为:在更新数据的时候大概率会有其他线程去争夺共享资源,所以悲观锁的做法是:第一个获取资源的线程会将资源锁定起来,其他没争夺到资源的线程只能进入阻塞队列,等第一个获取资源的线程释放锁之后,这些线程才能有机会重新争夺资源。synchronized就是java中悲观锁的典型实现,synchronized使用起来非常简单方便,但是会使没争抢到资源的线程进入阻塞状态,线程在阻塞状态和Runnable状态之间切换效率较低(比较慢)。比如你的更新操作其实是非常快的,这种情况下你还用synchronized将其他线程都锁住了,线程从Blocked状态切换回Runnable华的时间可能比你的更新操作的时间还要长。
乐观锁更新方式认为:在更新数据的时候其他线程争抢这个共享变量的概率非常小,所以更新数据的时候不会对共享数据加锁。但是在正式更新数据之前会检查数据是否被其他线程改变过,如果未被其他线程改变过就将共享变量更新成最新值,如果发现共享变量已经被其他线程更新过了,就重试,直到成功为止。CAS机制就是乐观锁的典型实现。
CAS,是Compare and Swap的简称,在这个机制中有三个核心的参数:
- 主内存中存放的共享变量的值:V(一般情况下这个V是内存的地址值,通过这个地址可以获得内存中的值)
- 工作内存中共享变量的副本值,也叫预期值:A
- 需要将共享变量更新到的最新值:B
如上图中,主存中保存V值,线程中要使用V值要先从主存中读取V值到线程的工作内存A中,然后计算后变成B值,最后再把B值写回到内存V值中。多个线程共用V值都是如此操作。CAS的核心是在将B值写入到V之前要比较A值和V值是否相同,如果不相同证明此时V值已经被其他线程改变,重新将V值赋给A,并重新计算得到B,如果相同,则将B值赋给V。
值得注意的是CAS机制中的这步步骤是原子性的(从指令层面提供的原子操作),所以CAS机制可以解决多线程并发编程对共享变量读写的原子性问题。
CAS机制优缺点
缺点
1. ABA问题
ABA问题:CAS在操作的时候会检查变量的值是否被更改过,如果没有则更新值,但是带来一个问题,最开始的值是A,接着变成B,最后又变成了A。经过检查这个值确实没有修改过,因为最后的值还是A,但是实际上这个值确实已经被修改过了。为了解决这个问题,在每次进行操作的时候加上一个版本号,每次操作的就是两个值,一个版本号和某个值,A——>B——>A问题就变成了1A——>2B——>3A。在jdk中提供了AtomicStampedReference类解决ABA问题,用Pair这个内部类实现,包含两个属性,分别代表版本号和引用,在compareAndSet中先对当前引用进行检查,再对版本号标志进行检查,只有全部相等才更新值。
2. 可能会消耗较高的CPU
看起来CAS比锁的效率高,从阻塞机制变成了非阻塞机制,减少了线程之间等待的时间。每个方法不能绝对的比另一个好,在线程之间竞争程度大的时候,如果使用CAS,每次都有很多的线程在竞争,也就是说CAS机制不能更新成功。这种情况下CAS机制会一直重试,这样就会比较耗费CPU。因此可以看出,如果线程之间竞争程度小,使用CAS是一个很好的选择;但是如果竞争很大,使用锁可能是个更好的选择。在并发量非常高的环境中,如果仍然想通过原子类来更新的话,可以使用AtomicLong的替代类:LongAdder。
3. 不能保证代码块的原子性
Java中的CAS机制只能保证共享变量操作的原子性,而不能保证代码块的原子性。
优点
- 可以保证变量操作的原子性;
- 并发量不是很高的情况下,使用CAS机制比使用锁机制效率更高;
- 在线程对共享资源占用时间较短的情况下,使用CAS机制效率也会较高。
Java提供的CAS操作类--Unsafe类
从Java5开始引入了对CAS机制的底层的支持,在这之前需要开发人员编写相关的代码才可以实现CAS。在原子变量类Atomic中(例如AtomicInteger、AtomicLong)可以看到CAS操作的代码,在这里的代码都是调用了底层(核心代码调用native修饰的方法)的实现方法。在AtomicInteger源码中可以看getAndSet方法和compareAndSet方法之间的关系,compareAndSet方法调用了底层的实现,该方法可以实现与一个volatile变量的读取和写入相同的效果。在前面说到了volatile不支持例如i++这样的复合操作,在Atomic中提供了实现该操作的方法。JVM对CAS的支持通过这些原子类(Atomic***)暴露出来,供我们使用。
而Atomic系类的类底层调用的是Unsafe类的API,Unsafe类提供了一系列的compareAndSwap*方法,下面就简单介绍下Unsafe类的API:
long objectFieldOffset(Field field)方法:返回指定的变量在所属类中的内存偏移地址,该偏移地址仅仅在该Unsafe函数中访问指定字段时使用。如下代码使用Unsafe类获取变量value在AtomicLong对象中的内存偏移。
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
int arrayBaseOffset(Class arrayClass)方法:获取数组中第一个元素的地址。
int arrayIndexScale(Class arrayClass)方法:获取数组中一个元素占用的字节。
boolean compareAndSwapLong(Object obj, long offset, long expect, long update)方法:比较对象obj中偏移量为offset的变量的值是否与expect相等,相等则使用update值更新,然后返回true,否则返回false,这次处理器提供的一个原子性指令。
public native long getLongvolatile(Object obj, long offset)方法:获取对象obj中偏移量为offset的变量对应volatile语义的值。
void putLongvolatile(Object obj, long offset, long value)方法:设置obj对象中offset偏移的类型为long的field的值为value,支持volatile语义。
void putOrderedLong(Object obj, long offset, long value)方法:设置obj对象中offset偏移地址对应的long型field的值为value。这是一个有延迟的putLongvolatile方法,并且不保证值修改对其他线程立刻可见。只有在变量使用volatile修饰并且预计会被意外修改时才使用该方法。
void park(boolean isAbsolute, long time)方法:阻塞当前线程,其中参数isAbsolute等于false且time等于0表示一直阻塞。time大于0表示等待指定的time后阻塞线程会被唤醒,这个time是个相对值,是个增量值,也就是相对当前时间累加time后当前线程就会被唤醒。如果isAbsolute等于true,并且time大于0,则表示阻塞的线程到指定的时间点后会被唤醒,这里time是个绝对时间,是将某个时间点换算为ms后的值。另外,当其他线程调用了当前阻塞线程的interrupt方法而中断了当前线程时,当前线程也会返回,而当其他线程调用了unPark方法并且把当前线程作为参数时当前线程也会返回。
void unpark(Object thread)方法:唤醒调用park后阻塞的线程。
下面是JDK8新增的函数,这里只列出Long类型操作。
long getAndSetLong(Object obj, long offset, long update)方法:获取对象obj中偏移量为offset的变量volatile语义的当前值,并设置变量volatile语义的值为update。
//这个方法只是封装了compareAndSwapLong的使用,不需要自己写重试机制
public final long getAndSetLong(Object var1, long var2, long var4) {
long var6;
do {
var6 = this.getLongVolatile(var1, var2);
} while(!this.compareAndSwapLong(var1, var2, var6, var4)); return var6;
}
long getAndAddLong(Object obj, long offset, long addValue)方法:获取对象obj中偏移量为offset的变量volatile语义的当前值,并设置变量值为原始值+addValue,原理和上面的方法类似。
Unsafe类的使用
public class UnsafeDemo {
static final Unsafe unsafe;
static final long offSet;
static long count;
static {
try {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
unsafe= (Unsafe)theUnsafe.get(null);
offSet = unsafe.objectFieldOffset(UnsafeDemo.class.getDeclaredField("offSet"));
}catch (Exception e){
}
}
public static void main(String[] args) {
UnsafeDemo unsafeDemo = new UnsafeDemo();
boolean b = unsafe.compareAndSwapInt(unsafeDemo, offSet, 0, 1);
System.out.println(b);
}
}
CAS使用场景
- 使用一个变量统计网站的访问量;
- Atomic类操作;
- 数据库乐观锁更新。
参考
- https://www.cnblogs.com/myopensource/p/8177074.html
- https://baijiahao.baidu.com/s?id=1647620168550407271&wfr=spider&for=pc
- https://www.jianshu.com/p/f714c440d0cb
并发编程的基石——CAS机制的更多相关文章
- 【C# 线程】并发编程的基石——CAS机制
其实Java并发框架的基石一共有两块,一块是本文介绍的CAS,另一块就是AQS,后续也会写博客介绍. 什么是CAS机制 CAS机制是一种数据更新的方式.在具体讲什么是CAS机制之前,我们先来聊下在多线 ...
- Java并发编程:Concurrent锁机制解析
Java并发编程:Concurrent锁机制解析 */--> code {color: #FF0000} pre.src {background-color: #002b36; color: # ...
- 并发编程的基石——AQS类
本博客系列是学习并发编程过程中的记录总结.由于文章比较多,写的时间也比较散,所以我整理了个目录贴(传送门),方便查阅. 并发编程系列博客传送门 本文参考了[Java多线程进阶(六)-- J.U.C之l ...
- 超强图文|并发编程【等待/通知机制】就是这个feel~
你有一个思想,我有一个思想,我们交换后,一个人就有两个思想 If you can NOT explain it simply, you do NOT understand it well enough ...
- Java并发编程(06):Lock机制下API用法详解
本文源码:GitHub·点这里 || GitEE·点这里 一.Lock体系结构 1.基础接口简介 Lock加锁相关结构中涉及两个使用广泛的基础API:ReentrantLock类和Condition接 ...
- Java并发编程基础-ReentrantLock的机制
同步锁: 我们知道,锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能够防止多个线程同时访问共享资源,在Lock接口出现之前,Java应用程序只能依靠synchronized关键字来实现同步锁 ...
- 简论数据库乐观悲观锁与并发编程中的CAS
为了防止无良网站的爬虫抓取文章,特此标识,转载请注明文章出处.LaplaceDemon/ShiJiaqi. http://www.cnblogs.com/shijiaqi1066/p/5783205. ...
- Go并发编程之美-CAS操作
摘要: 一.前言 go语言类似Java JUC包也提供了一些列用于多线程之间进行同步的措施,比如低级的同步措施有 锁.CAS.原子变量操作类.相比Java来说go提供了独特的基于通道的同步措施.本节我 ...
- 【并发编程】Binder运行机制的流程图
Binder工作在Linux层面,属于一个驱动,只是这个驱动不需要硬件,或者说其操作的硬件是基于一小段内存.从线程的角度来讲,Binder驱动代码运行在内核态,客户端程序调用Binder是通过系统调用 ...
随机推荐
- ashx不能折叠代码,没有智能提示
visual studio 2013有时候会遇到这个问题.没安装任何第三方插件,创建的是web网站项目.ashx文件忽然就没有intelligent智能提示了. 可以试试: 关闭visual stud ...
- Jquery FormData文件异步上传 快速指南
网站中文件的异步上传是个比较麻烦的问题,不过现在通过jquery 可以很容易的解决这个问题: 使用jquery2.1版本,较老版本不支持异步文件上传功能: 表单代码: <form id=&quo ...
- springmvc 返回json数据给前台jsp页面展示
spring mvc返回json字符串的方式 方案一:使用@ResponseBody 注解返回响应体 直接将返回值序列化json 优点:不需要自己再处理 步骤一:在spring- ...
- yeoman&bower
一.Yeoman 在nodejs环境下安装: npm install -g yo 然后安装所需要的generator,generator是npm包,命名为generator-xyz,比如安装angul ...
- javascript 宽度和高度
宽度和高度 对于编写css代码时,宽度和高度就是width和height 但是在JavaScript中,还有其他的宽度和高度,比如offsetWidth,offsetHeight,clientX,cl ...
- poj 3278(hdu 2717) Catch That Cow(bfs)
Catch That Cow Time Limit: 5000/2000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)To ...
- springboot activiti工作流简单示例
最近一直研究springboot,根据工作需求,工作流需要作为一个单独的微服务工程来提供给其他服务调用,现在简单的写下工作流(使用的activiti)微服务的搭建与简单使用 jdk:1.8 数据库:m ...
- PHP两个变量值互换(不用第三变量)
<?php /** * 双方变量为数字或者字符串时 * 使用list()和array()方法可以达到交换变量值得目的 */ $a = "This is A"; // a ...
- 怎么让FOXMAIL关了以后在右下角自动收取邮件
1.缩小到任务栏:打开foxmail,在工具-系统设置-常规,选项中有一项最小化时在任务栏显示,勾选上即可.2.要自动收取邮件,选中邮件账户,右键打开菜单,属性-接收邮件,右边勾选上“每隔*分钟自动收 ...
- H3C 单路径网络中环路产生过程(1)