聊聊并发(六)——CAS算法
一、原子类
1、CAS算法
强烈建议读者看这篇之前,先看这篇 初识JUC 的前两节,对原子性,原子变量,内存可见性有一个初步认识。
CAS(Compare and Swap)是一种硬件对并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问,是硬件对于并发操作共享数据的支持。它是一个原子性的操作,对应到CPU指令为cmpxchg。它是一条CPU并发原语。
CAS包含了3个操作数:内存值V,比较值A,更新值B。当且仅当V == A时,V = B,否则不执行任何操作。
CAS算法:当多个线程并发的对主存中的数据进行修改的时候。有且只有一个线程会成功,其他的都会失败(同时操作,只是会失败而已,并不会被锁之类的)。
CAS是一种无锁的非阻塞算法,是乐观锁的一种实现。不存在上下文切换的问题。
CAS比普通同步锁效率高,原因:CAS算法当这一次不成功的时候,它下一次不会阻塞,也就是它不会放弃CPU的执行权,它可以立即再次尝试,再去更新。
通俗的说:我要将变量 i 由 2 修改为 3。当内存中 i == 2,且修改成功,才为成功。若内存中 i 由于其他线程的操作已经不是 2 了,那此次我的修改视为失败。
2、简单使用
JDK 1.5 以后java.util.concurrent.atomic包下提供了常用的原子变量。它支持单个变量上的无锁线程安全编程。这些原子变量具备以下特点:volatile的内存可见性;CAS算法保证数据的原子性。
atomic包描述:图片来源API文档
代码示例:原子变量使用
1 public class Main {
2 public static void main(String[] args) {
3 AtomicInteger integer = new AtomicInteger(2);
4
5 boolean b = integer.compareAndSet(3, 5);
6 System.out.println(b);
7 System.out.println(integer.get());
8
9 b = integer.compareAndSet(2, 10);
10 System.out.println(b);
11 System.out.println(integer.get());
12
13 // 等价于 i++
14 integer.getAndIncrement();
15
16 // 等价于 ++i
17 integer.incrementAndGet();
18 }
19 }
20
21 // 结果
22 false
23 2
24 true
25 10
分析:很简单,设置初始值为 2。
①由 3 修改成5,而设置初始值内存值为2,所以修改失败,返回false。
②由 2 修改成10,初始值内存值为2,所以修改成功,返回true。
3、源码分析
这些原子变量底层就是通过CAS算法来保证数据的原子性。
源码示例:AtomicInteger 类
1 public class AtomicInteger extends Number implements java.io.Serializable {
2 private static final long serialVersionUID = 6214790243416807050L;
3
4 // setup to use Unsafe.compareAndSwapInt for updates
5 private static final Unsafe unsafe = Unsafe.getUnsafe();
6 private static final long valueOffset;
7
8 // 获取value在内存的地址偏移量
9 static {
10 try {
11 valueOffset = unsafe.objectFieldOffset
12 (AtomicInteger.class.getDeclaredField("value"));
13 } catch (Exception ex) { throw new Error(ex); }
14 }
15
16 private volatile int value;
17
18 public AtomicInteger(int initialValue) {
19 value = initialValue;
20 }
21
22 public AtomicInteger() {
23 }
24
25 public final int get() {
26 return value;
27 }
28
29 public final void set(int newValue) {
30 value = newValue;
31 }
32
33 public final boolean compareAndSet(int expect, int update) {
34 return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
35 }
36
37 public final int getAndIncrement() {
38 return unsafe.getAndAddInt(this, valueOffset, 1);
39 }
40
41 public final int incrementAndGet() {
42 return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
43 }
44
45 }
说明:public final boolean compareAndSet(int expect, int update)
变量valueOffset:通过静态代码块获取变量value在内存中的偏移地址。
变量value:用volatile修饰,这里体现了"多线程之间的内存可见性"。
this:即 AtomicInteger 对象本身。
很容易理解:就是将当前对象 this 的变量value,由期望值 expect 修改为 update。
源码示例:Unsafe 类
1 public final class Unsafe {
2
3 public native void throwException(Throwable var1);
4
5 public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
6
7 public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
8
9 public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
10
11 public native int getIntVolatile(Object var1, long var2);
12
13
14 public final int getAndAddInt(Object var1, long var2, int var4) {
15 int var5;
16 do {
17 // 获取对象var1的变量var2的内存值
18 var5 = this.getIntVolatile(var1, var2);
19 } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
20
21 return var5;
22 }
23
24 }
Unsafe是CAS的核心类,其所有方法都是native修饰的。也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务,是由C/C++编写的本地方法。CAS算法的实现,也是由Unsafe类通过调用本地方法直接操作特定内存数据来实现的。
getAndIncrement()方法能够在多线程环境保证变量的原子性自增。但源码中,并没有加synchronized或者lock锁,那么,它是如何保证的呢?其实很简单:
先获取一次变量的内存值,然后通过CAS算法进行比较更新。失败了就一直不停的重试,是一个循环的过程,这个过程也称作自旋。
这就是为什么 AtomicInteger 的自增操作具备原子性。
1 private AtomicInteger i = new AtomicInteger();
2 public int getI() {
3 return i.getAndIncrement();
4 }
4、CAS的缺点
(1)ABA问题。
(2)循环时间变长:高并发情况下,使用CAS可能会存在一些线程一直循环修改不成功,导致循环时间变长,这会给CPU带来很大的执行开销。由于AtomicInteger中的变量是volatile的,为了保证内存可见性,需要保证缓存一致性,通过总线传输数据,当有大量的CAS循环时,会产生总线风暴。
(3)只能保证一个变量的原子操作:如果需要保证多个变量操作的原子性,是做不到的。对于这种情况只能使用synchronized或者juc包中的Lock工具。
二、ABA问题
1、介绍
代码示例:演示ABA问题
1 // 原子引用类演示ABA问题
2 public class ABATest {
3 public static void main(String[] args) throws InterruptedException {
4 AtomicReference<String> reference = new AtomicReference<>("A");
5
6 // 线程 t1 由A修改B,又由B修改A
7 new Thread(() -> {
8 System.out.println(reference.compareAndSet("A", "B") + ". " + Thread.currentThread().getName() + " value is:" + reference.get());
9 System.out.println(reference.compareAndSet("B", "A") + ". " + Thread.currentThread().getName() + " value is:" + reference.get());
10 }, "t1").start();
11
12
13 new Thread(() -> {
14 // 让t1线程完成ABA操作
15 try {
16 Thread.sleep(500);
17 } catch (InterruptedException e) {
18 e.printStackTrace();
19 }
20 System.out.println(reference.compareAndSet("A", "C") + ". " + Thread.currentThread().getName() + " value is:" + reference.get());
21
22 }, "t2").start();
23
24 Thread.sleep(1000);
25
26 System.out.println(reference.get());
27 }
28 }
29
30 // 结果
31 true. t1 value is:B
32 true. t1 value is:A
33 true. t2 value is:C
34 C
如何理解ABA问题?
可能你会觉得,线程 t2 不就是要将"A"改为"C"嘛,虽然中间变化了,但对 t2 也没影响呀!
比如:你的银行卡里有10w,中间你领了工资1w,然后,又被扣除还了房贷1w,此时,你的银行卡里还是10w。虽然结果没变,但余额已经不是原来的余额了。而且,你一定在意中间你的钱去哪里了,所以是不一样的。
再比如:对于公司财务来说,可能某一时刻,账户是100w,你偷偷挪用了公款20w,后来又悄悄补上了。虽然结果没变,但中间的记账明细,其实我们是关心的,因为这个时候你已经犯法了。
2、解决
带时间戳的原子引用:Java提供了AtomicStampedReference来解决ABA问题。其实其实就是加了版本号,每一次的修改,版本号都 +1。比对的是 内存值 + 版本号 是否一致。
代码示例:解决ABA问题
1 public class ABATest {
2 public static void main(String[] args) throws InterruptedException {
3
4 AtomicStampedReference<String> reference = new AtomicStampedReference<>("A", 1);
5 final int stamp = reference.getStamp();
6
7 // 线程 t1 由A修改B,又由B修改A
8 new Thread(() -> {
9 System.out.println(reference.compareAndSet("A", "B", stamp, stamp + 1) + ". " + Thread.currentThread().getName() + " value is:" + reference.getReference());
10 System.out.println(reference.compareAndSet("B", "A", reference.getStamp(), reference.getStamp() + 1) + ". " + Thread.currentThread().getName() + " value is:" + reference.getReference());
11 }, "t1").start();
12
13
14 new Thread(() -> {
15 // 让t1线程完成ABA操作
16 try {
17 Thread.sleep(500);
18 } catch (InterruptedException e) {
19 e.printStackTrace();
20 }
21 System.out.println(reference.compareAndSet("A", "C", stamp, stamp + 1) + ". " + Thread.currentThread().getName() + " value is:" + reference.getReference());
22
23 }, "t2").start();
24
25 Thread.sleep(1000);
26
27 System.out.println(reference.getReference());
28 }
29 }
30
31 // 结果
32 true. t1 value is:B
33 true. t1 value is:A
34 false. t2 value is:A // t2并没有修改成功
35 A
compareAndSet()方法的 4 个参数:
expectedReference:表示期望的引用值
newReference:表示要修改后的新引用值
expectedStamp:表示期望的戳(版本号)
newStamp:表示修改后新的戳(版本号)
3、源码分析
1 public class AtomicStampedReference<V> {
2
3 private static class Pair<T> {
4 final T reference;
5 final int stamp;
6 private Pair(T reference, int stamp) {
7 this.reference = reference;
8 this.stamp = stamp;
9 }
10 static <T> Pair<T> of(T reference, int stamp) {
11 return new Pair<T>(reference, stamp);
12 }
13 }
14
15 public boolean compareAndSet(V expectedReference,
16 V newReference,
17 int expectedStamp,
18 int newStamp) {
19 Pair<V> current = pair;
20 return
21 expectedReference == current.reference &&
22 expectedStamp == current.stamp &&
23 ((newReference == current.reference &&
24 newStamp == current.stamp) ||
25 casPair(current, Pair.of(newReference, newStamp)));
26 }
27
28 private boolean casPair(Pair<V> cmp, Pair<V> val) {
29 return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
30 }
31 }
很简单,维护了一对Pair,里面除了引用reference,还有一个int类型的戳(版本号)。比较更新的时候,两个变量都要比较。
三、LongAdder
1、介绍
《阿里巴巴Java开发手册》推荐使用LongAdder。
AtomicLong,本质上是多个线程同时操作同一个目标资源,有且只有一个线程执行成功,其他线程都会失败,不断重试(自旋),自旋会成为瓶颈。
而LongAdder的思想就是把要操作的目标资源[分散]到数组Cell中,每个线程对自己的Cell变量的value进行原子操作,大大降低了失败的次数。
这就是为什么在高并发场景下,推荐使用LongAdder的原因。
参考文档:https://www.matools.com/api/java8
《阿里巴巴Java开发手册》百度网盘:https://pan.baidu.com/s/1aWT3v7Efq6wU3GgHOqm-CA 密码: uxm8
聊聊并发(六)——CAS算法的更多相关文章
- 并发策略-CAS算法
对于并发控制而言,我们平时用的锁(synchronized,Lock)是一种悲观的策略.它总是假设每一次临界区操作会产生冲突,因此,必须对每次操作都小心翼翼.如果多个线程同时访问临界区资源,就宁可牺牲 ...
- 聊聊并发(六)——ConcurrentLinkedQueue的实现原理分析
1. 引言 在并发编程中我们有时候需要使用线程安全的队列.如果我们要实现一个线程安全的队列有两种实现方式:一种是使用阻塞算法,另一种是使用非阻塞算法.使用阻塞算法的队列可以用一个锁(入队和出队用同一把 ...
- (转载)java高并发:CAS无锁原理及广泛应用
java高并发:CAS无锁原理及广泛应用 版权声明:本文为博主原创文章,未经博主允许不得转载,转载请注明出处. 博主博客地址是 http://blog.csdn.net/liubenlong007 ...
- CAS 算法与 Java 原子类
乐观锁 一般而言,在并发情况下我们必须通过一定的手段来保证数据的准确性,如果没有做好并发控制,就可能导致脏读.幻读和不可重复度等一系列问题.乐观锁是人们为了应付并发问题而提出的一种思想,具体的实现则有 ...
- 聊聊并发(一)——初始JUC
一.volatile 1.介绍 JDK 5.0 提供了java.util.concurrent包,在此包中增加了并发编程中很常用的使用工具类,用于定义类似于线程的自定义子系统,包括线程池.异步IO和轻 ...
- 三、原子变量与CAS算法
原子变量:jdk1.5 后 java.util.concurrent.atomic 包下提供了常用的原子变量: - AtomicBoolean - AtomicInteger - AtomicLong ...
- Java多线程系列——原子类的实现(CAS算法)
1.什么是CAS? CAS:Compare and Swap,即比较再交换. jdk5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronou ...
- 原子变量与CAS算法(二)
一.锁机制存在的问题 (1)在多线程竞争下,加锁.释放锁会导致比较多的上下文切换和调度延时,引起性能问题. (2)一个线程持有锁会导致其它所有需要此锁的线程挂起. (3)如果一个优先级高的线程等待一个 ...
- (一)juc线程高级特性——volatile / CAS算法 / ConcurrentHashMap
1. volatile 关键字与内存可见性 原文地址: https://www.cnblogs.com/zjfjava/category/979088.html 内存可见性(Memory Visibi ...
随机推荐
- CORS+XSS的漏洞利用payload
之前有人问我有没有CORS+XSS的利用姿势,翻了一下国内貌似都没有利用姿势于是就写了这篇文章!!! 首先找到一个反射xss,然后使用xss加载javascript代码达到跨域劫持目的payload如 ...
- Boost Started on Windows
Boost 官网指南 Boost C++ Libraries Boost Getting Started on Windows - 1.77.0 ① 下载 Boost.7z包 下载 .7z包 boos ...
- ubuntu修改软件源的方法
最快方法--替换法 刚安装好的ubutun,打开source.list后,用vim替换的方法将所有的us提付出替换为 cn,然后保存退出,更新即可. # vim /etc/apt/source.lis ...
- Java:抽象类和接口小记
Java:抽象类和接口小记 对 Java 中的 抽象类和接口,做一个微不足道的小小小小记 抽象类:使用 abstract 修饰,子类用 extends 继承: 接口:使用 interface 修饰,采 ...
- the Agiles Scrum Meeting 5
会议时间:2020.4.13 20:00 1.每个人的工作 今天已完成的工作 增量组:完成了增量开发的基础工作,初步完成了自动评测机制 issues:增量组:准备评测机制,增加仓库自动创建和管理 完善 ...
- [BZOJ4399]魔法少女LJJ----------线段树进阶
感谢线段树进阶,给了我重新做人的机会.---------------某不知名OIer,Keen_z Description 题目描述 在森林中见过会动的树,在沙漠中见过会动的仙人掌过后,魔法少女LJJ ...
- 你知道如何从单片机过渡到嵌入式linux需要经历那些吗?(这个亲身体验有效)
就现在的行业发展来看只会单片机已经不吃香了并且在薪资待遇方面来看的话单片机的收入限制性太强可能工作很多年之后发现没有了成长空间,因此逐渐转到嵌入式Linux这个方向是越来越多的人的一个选择,那么接触了 ...
- 企业级BI为什么这么难做?
本人长期在银行内从事数据线相关工作,亲眼目睹过多个企业级BI(非部门级BI)产品从上线试用.全行推广.然后衰败没落,再替换到下一个BI产品重复此过程.企业内没有任何一个BI产品即能长期运行,又能赢得非 ...
- 与 Python 之父聊天:更快的 Python!
Python猫注: 在今年 5 月的 Python 语言峰会上,Guido van Rossum 作了一场<Making CPython Faster>的分享(材料在此),宣告他加入了激动 ...
- cf12D Ball(MAP,排序,贪心思想)
题意: N位女士一起聚在一个舞厅.每位女士有三个特征值B,I,R.分别代表美貌,智慧,富有. 对于一位女士而言,如果存在一个女士的B,I,R都分别大于她自己的B,I,R.则她自己会自杀. 统计总共有多 ...