CAS都不了解,你还怎么看J.U.C
前言
说到CAS(CompareAndSwap),不得不先说一说悲观锁和乐观锁,因为CAS是乐观锁思想的一种实现。
悲观锁:总是很悲观的认为,每次拿数据都会有其他线程并发执行,所以每次都会进行加锁,用完之后释放锁,其他的线程才能拿到锁,进而拿到资源进行操作。java中的synchronized和ReentrantLock等独占锁就是悲观锁思想的实现。
乐观锁:总是很乐观认为,自己拿到数据操作的时候,没有其他线程来并发操作,等自己操作结束要更新数据时,判断自己对数据操作的期间有没有其他线程进行操作,如果有,则进行重试,直到操作变更成功。乐观锁常使用CAS和版本号机制来实现。java中java.util.atomic包下的原子类都是基于CAS实现的。
一、什么是CAS
CAS指CompareAndSwap,顾名思义,先比较后交换。比较什么?交换什么呢?
CAS中有三个变量:内存地址V,期待值A, 更新值B。
当且仅当内存地址V对应的值与期待值A时相等时,将内存地址V对应的值更换为B。
二、atomic包
有了悲观锁,乐观锁的知识,让我们走进java.util.atomic包,看一看java中CAS的实现。
这就是java.util.atomic包下的类,我们着重看AtomicInteger源码(其他的都是一样的思想实现的)
然后思考CAS有什么弊端?如何解决弊端?有什么优缺点?
2.1、走进AtomicInteger源码
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// 使用Unsafe.compareAndSwapInt进行原子更新操作
private static final Unsafe unsafe = Unsafe.getUnsafe();
//value对应的存储地址偏移量
private static final long valueOffset;
static {
try {
//使用反射及unsafe.objectFieldOffset拿到value字段的内存地址偏移量,这个值是固定不变的
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
//volatile修饰的共享变量
private volatile int value;
//..........
}
上面的代码其实就是为了初始化内存值对应的内存地址偏移量valueOffset,方便后续执行CAS操作时使用。因为这个值一旦初始化,就不会更改,所以使用static final 修饰。
我们可以看到value使用了volatile修饰,上一篇9龙详细详解了JMM,其中也说了volatile的语义,不了解的小伙伴可以先去看一看。
我们都知道如果进行value++操作,并发下是不安全的。上一篇中我们也通过例子证明了volatile只能保证可见性,不能保证原子性。因为value++本身不是原子操作,value++分了三步,先拿到value的值,进行+1,再赋值回value。
2.2、compareAndSwapXxx
我们先看一看AtomicInteger提供的CAS操作。
/**
* 原子地将value设置为update,如果valueOffset对应的值与expect相等时
*
* @param expect 期待值
* @param update 更新值
* @return 如果更新成功,返回true;在valueOffset对应的值与expect不相等时返回false
*/
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
我们已经知道CAS的原理,那来看看下面的测试。你知道输出的结果是多少吗?评论区给出你的答案吧。
public class AtomicIntegerTest {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger();
atomicInteger.compareAndSet(0, 1);
atomicInteger.compareAndSet(2, 1);
atomicInteger.compareAndSet(1, 3);
atomicInteger.compareAndSet(2, 4);
System.out.println(atomicInteger.get());
}
}
Unsafe提供了三个原子更新的方法。
关于Unsafe类,因为java不支持直接操作底层硬件资源,如分配内存等。如果你使用unsafe开辟的内存,是不被JVM垃圾回收管理,需要自己管理,容易造成内存泄漏等。
2.3、AtomicInteger的原子自增方法
我们上面说了,value++不是原子操作,不能在并发下使用。我们来看看AtomicInteger提供的原子++操作。
/**
* 原子地对value进行+1操作
*
* @return 返回更新后的值
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
/**
* unsafe提供的方法
* var1 更改的目标对象
* var2 目标对象的共享字段对应的内存地址偏移量valueOffset
* var4 需要在原value上增加的值
* @return 返回未更新前的值
*/
public final int getAndAddInt(Object var1, long var2, int var4) {
//期待值
int var5;
do {
//获取valueOffset对应的value的值,支持volatile load
var5 = this.getIntVolatile(var1, var2);
//如果原子更新失败,则一直重试,直到成功。
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
我们看到CAS只能原子的更新一个值,如果我们要原子更新多个值,CAS可以做到吗?答案是可以的。
2.4、AtomicReference
如果要原子地更新多个值,就需要使用AtomicReference。其使用的是compareAndSwapObject方法。可以将多个值封装到一个对象中,原子地更换对象来实现原子更新多个值。
public class MultiValue {
private int value1;
private long value2;
private Integer value3;
public MultiValue(int value1, long value2, Integer value3) {
this.value1 = value1;
this.value2 = value2;
this.value3 = value3;
}
}
public class AtomicReferenceTest {
public static void main(String[] args) {
MultiValue multiValue1 = new MultiValue(1, 1, 1);
MultiValue multiValue2 = new MultiValue(2, 2, 2);
MultiValue multiValue3 = new MultiValue(3, 3, 3);
AtomicReference<MultiValue> atomicReference = new AtomicReference<>();
//因为构造AtomicReference时,没有使用有参构造函数,所以value默认值是null
atomicReference.compareAndSet(null, multiValue1);
System.out.println(atomicReference.get());
atomicReference.compareAndSet(multiValue1, multiValue2);
System.out.println(atomicReference.get());
atomicReference.compareAndSet(multiValue2, multiValue3);
System.out.println(atomicReference.get());
}
}
//输出结果
//MultiValue{value1=1, value2=1, value3=1}
//MultiValue{value1=2, value2=2, value3=2}
//MultiValue{value1=3, value2=3, value3=3}
我们再看一看AtomicReference的compareAndSet方法。
注意:这里的比较都是使用==而非equals方法。所以最好封装的MultiValue不要提供set方法。
public final boolean compareAndSet(V expect, V update) {
return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}
2.5、CAS的ABA问题
假设你的账户上有100块钱,你要给女票转50块钱。
我们使用CAS进行原子更新账户余额。由于某种原因,你第一次点击转账出现错误,你以为没有发起转账请求,这时候你又点击了一次。系统开启了两个线程进行转账操作,第一个线程进行CAS比较,发现你的账户上预期是100块钱,实际也有100块钱,这时候转走了50,需要设置为100 - 50 = 50 元,这时账户余额为50
第一个线程操作成功了,第二个线程由于某种原因阻塞住了;这时候,你的家人又给你转了50块钱,并且转账成功。那你账户上现在又是100块钱;
太巧了,第二个线程被唤醒了,发现你的账户是100块钱,跟预期的100是相等的,这时候又CAS为50。大兄弟,哭惨了,你算算,正确的场景你要有多少钱?这就是CAS存在的ABA问题。
public class AtomicIntegerABA {
private static AtomicInteger atomicInteger = new AtomicInteger(100);
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
//线程1
executorService.execute(() -> {
System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get());
atomicInteger.compareAndSet(100, 50);
System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get());
});
//线程2
executorService.execute(() -> {
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get());
atomicInteger.compareAndSet(50, 100);
System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get());
});
//线程3
executorService.execute(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get());
atomicInteger.compareAndSet(100, 50);
System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get());
});
executorService.shutdown();
}
}
//输出结果
//pool-1-thread-1 - 100
//pool-1-thread-1 - 50
//pool-1-thread-2 - 50
//pool-1-thread-2 - 100
//pool-1-thread-3 - 100
//pool-1-thread-3 - 50
大家心想,靠,这不是坑吗?那还用。。。。。。。。。。。。。。冷静,冷静。你能想到的问题,jdk都能想到。atomic包提供了一个AtomicStampedReference
2.6、AtomicStampedReference
看名字是不是跟AtomicReference很像啊,其实就是在AtomicReference上加上了一个版本号,每次操作都对版本号进行自增,那每次CAS不仅要比较value,还要比较stamp,当且仅当两者都相等,才能够进行更新。
public AtomicStampedReference(V initialRef, int initialStamp) {
pair = Pair.of(initialRef, initialStamp);
}
//定义了内部静态内部类Pair,将构造函数初始化的值与版本号构造一个Pair对象。
private static class Pair<T> {
final T reference;
final int stamp;
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static <T> Pair<T> of(T reference, int stamp) {
return new Pair<T>(reference, stamp);
}
}
//所以我们之前的value就对应为现在的pair
private volatile Pair<V> pair;
让我们来看一看它的CAS方法。
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
//只有在旧值与旧版本号都相同的时候才会更新为新值,新版本号
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
private boolean casPair(Pair<V> cmp, Pair<V> val) {
return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}
还是上面转账的例子,我们使用AtomicStampedReference来看看是否解决了呢。
public class AtomicStampedReferenceABA {
/**
* 初始化账户中有100块钱,版本号对应0
*/
private static AtomicStampedReference<Integer> atomicInteger = new AtomicStampedReference<>(100, 0);
public static void main(String[] args) {
ExecutorService executorService = Executors.newFixedThreadPool(3);
int[] result = new int[1];
//线程1
executorService.execute(() -> {
System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result));
//将100更新为50,版本号+1
atomicInteger.compareAndSet(100, 50, 0, 1);
System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result));
});
//线程2
executorService.execute(() -> {
try {
TimeUnit.MILLISECONDS.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result));
//将50更新为100,版本号+1
atomicInteger.compareAndSet(50, 100, 1, 2);
System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result));
});
//线程3
executorService.execute(() -> {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result));
//此线程还是以为没有其他线程进行过更改,所以旧版本号还是0
atomicInteger.compareAndSet(100, 50, 0, 1);
System.out.println(Thread.currentThread().getName() + " - " + atomicInteger.get(result));
});
executorService.shutdown();
}
}
//输出结果
//pool-1-thread-1 - 100
//pool-1-thread-1 - 50
//pool-1-thread-2 - 50
//pool-1-thread-2 - 100
//pool-1-thread-3 - 100
//pool-1-thread-3 - 100
妈妈再也不用担心我的钱少了。
三、总结
本篇详细讲解了CAS的原理,CAS可以进行原子更新一个值(包括对象),主要用于读多写少的场景,如原子自增操作,如果多线程调用,在CAS失败之后,会死循环一直重试,直到更新成功。这种情况是很耗CPU资源的,虽然没有锁,但循环的自旋可能比锁的代价还高。同时存在ABA问题,但AtomicStampedReference通过加入版本号机制已经解决。其实对于atomic包,jdk1.8新增的LongAdder,效率比AtomicLong高,9龙还未涉足,以后肯定会品一品。J.U.C(java.util.concurrent)包中大量使用了CAS,ConcurrentHashMap也使用到,如果不了解CAS,怎么入手J.U.C呢。
各位看官,如果觉得9龙的文章对你有帮助,求点赞,求关注。如果转载请注明出处。
参考链接:
CAS都不了解,你还怎么看J.U.C的更多相关文章
- 如何让HTTPS站点评级达到A+? 还得看这篇HTTPS安全优化配置最佳实践指南
0x00 前言简述 SSL/TLS 简单说明 描述: 当下越来越多的网站管理员为企业站点或自己的站点进行了SSL/TLS配置, SSL/TLS 是一种简单易懂的技术,它很容易部署及运行,但要对其进行安 ...
- 你连Bug都抓不住,还谈什么参与感?
林子大了什么鸟都有,APP市场也是这样.举个例子,有段时期图片社交井喷式发展,各类图片社交APP一时充斥着市场.各种或重视图片加工或主打社交元素的APP“来得快去得快”.“你方唱罢我登场”,这些短命A ...
- 安卓开发视频教程!想找工作的你还不看这份资料就晚了!Android校招面试指南
前言 准备面试其实已经准备了挺久了,当时打算面试准备了差不多以后,跟公司谈谈涨薪的事情,谈不拢的话,就年后直接找其他的公司.谁想到婚假还没休完,老板就在公司宣布了撤出上海的决定,愿意去深圳的就去,不愿 ...
- java OOM还在看log日志,兄弟你错的的很严重,正确方式是分析dump文件
目录 OOM异常--intsmaze 正确姿势dump文件分析--intsmaze 正确的姿势--intsmaze dump丢失打印--intsmaze 哪些内存溢出会产生dump文件--intsma ...
- 教你判断一个APP页面是原生的还是H5页面 。(还没看)
来源:https://www.25xt.com/appdesign/11851.html 刚好是周末,无意之间学堂君在收集相关资料的时候,发现有部分童鞋在问<如何判断一个APP页面是不是H5页面 ...
- 树莓派安装OSMC打造家庭影院,还可以看优酷和CCTV
1.OSMC是什么? OSMC是树莓派官方推荐的影音系统,是一款开源的操作系统,是Openelec的升级版,同样是基于Kodi的开源项目.OSMC,使用它可以将树莓派打造成一款全功能的家庭影院系统,它 ...
- Longest Palindromic Substring (最长回文字符串)——两种方法还没看,仍需认真看看
Given a string S, find the longest palindromic substring in S. You may assume that the maximum lengt ...
- [CSS七分钟系列]都1902年了,还不知道用margin:auto给flex容器内元素分组?
最近看到几篇博文讲解margin:auto在flex容器中的使用,可惜的是大多讲解都浮于页面表现,没深究其中的作用机理,本文在此浅薄对其表现机理做简单探讨. 引子 日常业务迭代过程中,flex已经是前 ...
- 扩展、接管MVC都不会,还说会Spring Boot?
持续原创输出,点击上方蓝字关注我 目录 前言 Spring Boot 版本 如何扩展MVC? 如何自定义一个拦截器? 什么都不配置为什么依然能运行MVC相关的功能? 如何全面接管MVC?[不推荐] 为 ...
随机推荐
- centos7 yum 安装 mysql5.7
1.查看系统中是否已安装 MySQL 服务,以下提供两种方式: rpm -qa | grep mysqlyum list installed | grep mysql 2.如果已安装则删除 MySQL ...
- Python基础学习(一)之Python的概述与环境安装
Python介绍 Python语言介绍 Python是一门高级的.面向对象的.解释性.脚本语言. 高级语言:贴近开发者,对应底层语言,底层语言贴近机器:java.C#.php .ruby 面向对象对应 ...
- I/O流操作
dmesg :将printk输出的信息重新输出一遍(信息在缓存区中).
- python经典面试算法题1.1:如何实现链表的逆序
本题目摘自<Python程序员面试算法宝典>,我会每天做一道这本书上的题目,并分享出来,统一放在我博客内,收集在一个分类中. 1.1 如何实现链表的逆序 [腾讯笔试题] 难度系数:⭐⭐⭐ ...
- Subline Text3最新激活方法解决 That license key doesn't appear to be valid.
第一步: 管理员身份登录系统 第二步: 进入到 C:\Windows\System32\drivers\etc (这个路径可以复制,都是一样的) 第三步: 右键hosts这个文件(打开方式 - 选择记 ...
- css3mask淡出文字
css3mask淡出文字<pre>.p0_11wrap { width: 0.299rem; height: 7.68rem; overflow: hidden; background: ...
- javax.persistence.PersistenceException: org.hibernate.exception.GenericJDBCException: ResultSet is from UPDATE. No Data.
Java jpa调用存储过程,抛出异常如下: javax.persistence.PersistenceException: org.hibernate.exception.GenericJDBCEx ...
- nyoj 72-Financial Management (求和 ÷ 12.0)
72-Financial Management 内存限制:64MB 时间限制:3000ms 特判: No 通过数:7 提交数:12 难度:1 题目描述: Larry graduated this ye ...
- PostgreSQL空间数据库创建备份恢复(PostGIS vs ArcGIS)
梯子 PostGIS创建备份恢复ArcGIS创建备份恢复 PostGIS 创建 安装就不必介绍了,windows下使用安装工具Application Stack Builder,选择空间扩展PostG ...
- cn_windows虚拟机配置
1.打开“VMware”,点击“主页”,点“创建新的虚拟机”: 2.会弹出一个“新建虚拟机向导”,类型选择“典型”,点击“下一步”: 3.选择“稍后安装操作系统”,点击“下一步”: 4.选择“Micr ...