Skip to content

Commit 311e335

Browse files
committed
📝CAS
1 parent 2345ca6 commit 311e335

1 file changed

Lines changed: 180 additions & 78 deletions

File tree

docs/java/JUC/CAS.md

Lines changed: 180 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
# 从 Atomic 到 CAS
1+
# 从 Atomic 到 CAS ,竟然衍生出这么多 20k+ 面试题
22

33
> CAS 知道吗,如何实现?
44
>
5-
> 讲一讲AtomicInteger,为什么要用CAS而不是synchronized
5+
> 讲一讲AtomicInteger,为什么要用 CAS 而不是 synchronized
66
>
7-
> CAS底层原理,谈谈你对 UnSafe 的理解?
7+
> CAS 底层原理,谈谈你对 UnSafe 的理解?
88
>
99
> AtomicInteger 的ABA问题,能说一下吗,原子更新引用知道吗?
1010
>
@@ -24,20 +24,21 @@ Java 虚拟机又提供了一个轻量级的同步机制——volatile([面试
2424

2525

2626

27-
## Atomic 原子类
27+
## 1. Atomic 原子类
2828

29-
Atomic 原子类可以保证多线程环境下,当某个线程在执行 atomic 的方法时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由 JVM 从等待队列中选择一个线程执行。Atomic类在软件层面上是非阻塞的,它的原子性其实是在硬件层面上借助相关的指令来保证的。
29+
Atomic 原子类可以保证多线程环境下,当某个线程在执行 atomic 的方法时,不会被其他线程打断,而别的线程就像自旋锁一样,一直等到该方法执行完成,才由 JVM 从等待队列中选择一个线程执行。Atomic 类在软件层面上是非阻塞的,它的原子性其实是在硬件层面上借助相关的指令来保证的。
3030

3131
![](https://tva1.sinaimg.cn/large/00831rSTly1gdc9ufejhyj30mg0n8n0f.jpg)
3232

3333

3434

35-
Atomic包中的类可以分成4组
35+
Atomic 包中的类可以分成 4 组
3636

3737
1. 基本类型:AtomicBoolean,AtomicInteger,AtomicLong
3838
2. 数组类型:tomicIntegerArray,AtomicLongArray,AtomicReferenceArray
3939
3. 引用类型:AtomicReference,AtomicMarkableReference,AtomicStampedReference
4040
4. 对象的属性修改类型 :AtomicIntegerFieldUpdater,AtomicLongFieldUpdater,AtomicReferenceFieldUpdater
41+
5. JDK1.8新增:DoubleAccumulator、LongAccumulator、DoubleAdder、LongAdder、Striped64
4142

4243

4344

@@ -46,8 +47,8 @@ Atomic包中的类可以分成4组:
4647
| 方法 | 描述 |
4748
| ----------------------- | ------------------------------------------------------------ |
4849
| get() | 直接返回值 |
49-
| addAndGet(int) | 增加指定的数据后返回增加后的数据 |
50-
| getAndAdd(int) | 增加指定的数据,返回变化前的数据 |
50+
| addAndGet(int) | 增加指定的数据后返回增加后的数据,相当于 i++ |
51+
| getAndAdd(int) | 增加指定的数据,返回变化前的数据,相当于 ++i |
5152
| getAndIncrement() | 增加1,返回增加前的数据 |
5253
| getAndDecrement() | 减少1,返回减少前的数据 |
5354
| getAndSet(int) | 设置指定的数据,返回设置前的数据 |
@@ -77,61 +78,92 @@ false current num:7
7778

7879
执行两次结果却不同,Why?
7980

80-
`compareAndSet()` 比较并交换,判断用当前值和期望值(第一个参数),是否一致,如果一致,修改为更新值(第二个参数),这就是大名鼎鼎的 CAS
81+
`compareAndSet()` 尝试新增后对比,若增加成功则返回true否则返回false。其实就是比较并交换,判断用当前值和期望值(第一个参数),是否一致,如果一致,修改为更新值(第二个参数),这就是大名鼎鼎的 CAS
8182

8283

8384

84-
## CAS 是什么
85+
## 2. CAS 是什么
8586

86-
CAS: 全称 `Compare and swap`,它是一条 **CPU 并发原语**
87+
### 2.1 CAS 算法
8788

88-
他的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。
89+
- CAS:全称 `Compare and swap`,即**比较并交换**,它是一条 **CPU 同步原语**。 是一种硬件对并发的支持,针对多处理器操作而设计的一种特殊指令,用于管理对共享数据的并发访问。
90+
- CAS 是一种无锁的非阻塞算法的实现。
91+
- CAS 包含了 3 个操作数:
92+
- 需要读写的内存值 V
93+
- 旧的预期值 A
94+
- 要修改的更新值 B
95+
- 当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V 的 值,否则不会执行任何操作(他的功能是判断内存某个位置的值是否为预期值,如果是则更改为新的值,这个过程是原子的。)
8996

90-
CAS 并发原语体现在 Java 语言中的 sum.misc.Unsafe 类中的各个方法。调用Unsafe 类中的CAS 方法, JVM 会帮助我们实现出 CAS 汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用于范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的在执行过程中不允许被中断,CAS是一条CPU的原子指令,不会造成数据不一致问题。
97+
CAS 并发原语体现在 Java 语言中的 `sum.misc.Unsafe` 类中的各个方法。调用 Unsafe 类中的 CAS 方法, JVM 会帮助我们实现出 CAS 汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于 CAS是一种系统原语,**原语属于操作系统用于范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的****在执行过程中不允许被中断**,CAS 是一条 CPU 的原子指令,不会造成数据不一致问题。
9198

99+
我们常用的 `java.util.concurrent` 包就建立在CAS之上。
92100

93101

94-
i++ 在多线程环境下用 AtomicInteger 的 getAndIncrement() 为什么是线程安全的
95102

96-
```java
97-
public class CASDemo {
103+
### 2.2 用 CAS 分析 AtomicInteger 类
98104

99-
public static void main(String[] args) {
100-
101-
AtomicInteger num = new AtomicInteger(6);
102-
System.out.println(num.compareAndSet(6, 7) + "\t + current num:" + num);
103-
System.out.println(num.compareAndSet(6, 8) + "\t current num:" + num);
104-
}
105-
}
106-
```
105+
查看 AtomicInteger 代码如下,可以看到该类下的方法大部分是 调用了 **Unsafe**
107106

108107
![](https://tva1.sinaimg.cn/large/00831rSTly1gd7dmmn1erj312q0lin1t.jpg)
109108

110-
#### 1. UnSafe
109+
#### 2.2.1 UnSafe
111110

112-
是CAS的核心类,由于 Java 方法无法直接访问底层系统,需要通过本地(native)方法来访问,UnSafe 相当于一个后门,基于该类可以直接操作特定内存的数据。UnSafe 类存在与 sum.misc 包中,其内部方法可以像 C 的指针一样直接操作内存,因为Java 中CAS 操作的执行依赖于 UnSafe 类的方法。
111+
是 CAS 的核心类,由于 Java 方法无法直接访问底层系统,需要通过本地(native)方法来访问,UnSafe 相当于一个后门,基于该类可以直接操作特定内存的数据。UnSafe 类存在与 `sum.misc` 包中,其内部方法可以像 C 语言的指针一样直接操作内存,因为 Java 中 CAS 操作的执行依赖于 UnSafe 类的方法。
113112

114113
UnSafe 类中的所有方法都是 native 修饰的,也就是说该类中的方法都是直接调用操作系统底层资源执行相应任务。
115114

116-
#### 2. valueOffset
115+
```java
116+
public final class Unsafe {
117+
private static final Unsafe theUnsafe;
118+
// ......
119+
@CallerSensitive
120+
public static Unsafe getUnsafe() {
121+
Class var0 = Reflection.getCallerClass();
122+
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
123+
throw new SecurityException("Unsafe");
124+
} else {
125+
return theUnsafe;
126+
}
127+
}
128+
129+
public native int getInt(Object var1, long var2);
130+
131+
public native void putInt(Object var1, long var2, int var4);
117132

118-
变量 valueOffset 表示该变量值在内存中的偏移地址,因为UnSafe 就是根据内存偏移地址获取数据
133+
public native Object getObject(Object var1, long var2);
134+
135+
public native void putObject(Object var1, long var2, Object var4);
136+
137+
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
138+
139+
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
140+
// ......
141+
}
142+
```
143+
144+
Unsafe 类为一单例实现,提供静态方法 getUnsafe 获取 Unsafe 实例,当且仅当调用 getUnsafe 方法的类为引导类加载器所加载时才合法,否则抛出 SecurityException 异常
145+
146+
#### 2.2.2 valueOffset
147+
148+
AtomicInteger 中的变量 valueOffset 表示该变量值在内存中的偏移地址,因为 UnSafe 就是根据内存偏移地址获取数据。
119149

120150
```java
121151
public final int getAndIncrement() {
122152
return unsafe.getAndAddInt(this, valueOffset, 1);
123153
}
124154
```
125155

126-
#### 3.变量 value用volatile 修饰,保证了多线程之间的内存可见性
156+
#### 2.2.3 volatile int value
127157

158+
变量 value 用 volatile 修饰,保证了多线程之间的内存可见性。
128159

129160

130-
## CAS 是什么
131161

162+
#### 2.2.4 举个栗子
132163

164+
我们用线程安全的 ++i 举例,也就是 AtomicInteger 中的 getAndAdd
133165

134-
逐层看getAndIncrement() 的源码如下
166+
逐层看 Unsafe 类中的 getAndAdd() 的源码如下
135167

136168
```java
137169
public final int getAndAddInt(Object var1, long var2, int var4) {
@@ -144,39 +176,62 @@ public final int getAndAddInt(Object var1, long var2, int var4) {
144176
}
145177
```
146178

147-
val1 AtomicInteger对象本身
179+
**解毒**
148180

149-
var2 该对象值得引用地址
181+
可以看到 getAndAddInt 方法有 4 个参数
150182

151-
var4 需要变动的数量
183+
- val1:AtomicInteger 对象本身
152184

153-
var5 是用过var1 var2 找出的主内存中真实的值(通过内存偏移量)
185+
- var2:该对象值的引用地址,内存偏移量
186+
- var4: 需要变动的数量,即 ++i 的 i
187+
- var5:是用过var1, var2 找出的主内存中真实的值(通过内存偏移量)
154188

155-
用该对象当前的值与var5 比较,如果相同,更新var5 + var4 并且返回true
189+
`this.compareAndSwapInt` 用该对象当前的值与 var5 比较,如果相同,更新 var5 + var4 并且返回 true,如果不同,继续取值然后再比较,直到更新完成。
156190

157-
如果不同,继续取值然后再比较,直到更新完成
191+
这一操作没有加锁,反复执行,**既保证了一致性,又保证了并发性**
158192

159-
没有加锁,反复执行,既保证了一致性,又保证了并发性
160193

161194

195+
假设线程A和线程B两个线程同时执行 getAndAddInt 操作(分别跑在不同CPU上):
162196

163-
假设线程A和线程B两个线程同时执行getAndAddInt操作(分别泡在不同CPU上):
164-
165-
1. AtomicInteger 里面的value原始值为3,即主内存中AtomicInteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存;
166-
2. 线程A通过getIntVolatile(var1,var2)拿到value值3,这时线程A被挂起;
167-
3. 线程B也通过getIntVolatile(var1,var2)方法获取到value值3,此时刚好线程B没有被挂起并执行compareAndSwapInt方法比较内存值为3,成功修改内存值为4,线程B结束,一切正常
197+
1. AtomicInteger 里面的 value 原始值为 3,即主内存中 AtomicInteger 的 value 为 3,根据 JMM 模型,线程A和线程B各自持有一份值为 3 的 value 的副本分别到各自的工作内存;
198+
2. 线程A通过 getIntVolatile(var1,var2) 拿到 value 值3,这时线程A被挂起;
199+
3. 线程B也通过 getIntVolatile(var1,var2) 方法获取到 value 值 3,此时刚好线程B没有被挂起并执行compareAndSwapInt 方法比较内存值为 3,成功修改内存值为 4,线程B结束,一切正常
168200
4. 这时线程A恢复,执行compareAndSwapInt() 方法比较,发现自己手里的3和主内存的值4不一致,说明该值已经被其他线程抢先一步修改过了,那线程A本次修改失败,重新读取;
169-
5. 线程A重新获取value值,因为变量value被volatile修饰,所以其他线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功
201+
5. 线程A重新获取value值,因为变量value 被 volatile 修饰,所以其他线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功
202+
203+
170204

205+
#### 2.2.5 使用 UnSafe 类
171206

207+
那如若想使用这个类,该如何获取其实例?有如下两个可行方案
172208

209+
1.`getUnsafe` 方法的使用限制条件出发,通过Java命令行命令 `-Xbootclasspath/a` 把调用 Unsafe 相关方法的类A所在 jar 包路径追加到默认的 bootstrap 路径中,使得A被引导类加载器加载,从而通过`Unsafe.getUnsafe `方法安全的获取 Unsafe 实例。
173210

211+
```
212+
java -Xbootclasspath/a: ${path} // 其中path为调用Unsafe相关方法的类所在jar包路径
213+
```
174214

175-
![](https://tva1.sinaimg.cn/large/00831rSTly1gd7gj60r2wj31eu0kg15t.jpg)
215+
2. 通过反射技术暴力获取 Unsafe 对象
176216

217+
```java
218+
private static Unsafe reflectGetUnsafe() {
219+
try {
220+
Field field = Unsafe.class.getDeclaredField("theUnsafe");
221+
field.setAccessible(true);
222+
return (Unsafe) field.get(null);
223+
} catch (Exception e) {
224+
log.error(e.getMessage(), e);
225+
return null;
226+
}
227+
}
228+
```
177229

230+
美团技术团队有一篇介绍Unsafe 类的文章:[Java魔法类:Unsafe应用解析](https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html)
178231

179-
## CAS 缺点
232+
233+
234+
## 3. CAS 缺点
180235

181236
- 循环时间长,开销很大。CAS算法需要不断地自旋来读取最新的内存值,长时间读取不到就会造成不必要的CPU开销。
182237

@@ -185,62 +240,109 @@ var5 是用过var1 var2 找出的主内存中真实的值(通过内存偏移
185240
- 只能保证一个共享变量的原子操作
186241

187242
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是,对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
243+
244+
- ABA 问题
188245

189-
### ABA 问题
246+
190247

191-
aba问题如何产生的
248+
### ABA 问题
192249

193-
CAS会导致"ABA问题"
250+
ABA 问题是什么?是如何产生的?
194251

195252
CAS算法实现一个重要前提是需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。
196253

197-
比如说一个线程one从内存位置V中取出A,这时候另一个线程two也从内存中取出A,并且线程two进行了一些操作将值变成了B,然后线程two又将V位置的数据变成A,这个时候线程one进行CAS操作发现内存中仍然是A,然后线程one操作成功
254+
比如线程1从内存位置 V 中取出A,这时线程2也从内存中取出A,并且线程2进行了一些操作将值变成了B,然后线程2又将V位置的数据变成A,这个时候线程1进行CAS操作发现内存中仍然是A,线程1就会误认为它没有被修改过,这个漏洞就是CAS操作的"ABA"问题
198255

199-
**尽管线程one的CAS操作成功,但是不代表这个过程就是没有问题的。**
256+
**尽管线程1的CAS操作成功,但是不代表这个过程就是没有问题的。**
200257

201258

202259

203-
### 原子引用
260+
#### 原子引用
204261

205-
AtomicReference
262+
我们平时操作的不止是基本数据类型,大多数情况其实是类对象,Atomic 提供的引用类型 AtomicReference 通过泛型可以支持对对象的原子操作
206263

264+
```java
265+
public class AtomicRefrenceDemo {
207266

267+
public static void main(String[] args) {
208268

209-
解决ABA问题, 新增一种机制,修改版本号(类似时间戳)
269+
User tom = new User("tom",18);
270+
User jim = new User("jim",20);
210271

211-
### 2. 原子变量 CAS算法
272+
AtomicReference<User> user = new AtomicReference<>();
273+
user.set(tom);
212274

213-
#### CAS 算法
275+
System.out.println(user.compareAndSet(tom, jim)+"\t"+user.get().toString());
276+
System.out.println(user.compareAndSet(tom, jim)+"\t"+user.get().toString());
214277

215-
- CAS (Compare-And-Swap) 是一种硬件对并发的支持,针对多处理器 操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问。
216-
- CAS 是一种无锁的非阻塞算法的实现。
217-
- CAS 包含了 3 个操作数:
218-
- 需要读写的内存值 V
219-
- 旧的预期值 A
220-
- 要修改的更新值 B
221-
- 当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V 的 值,否则不会执行任何操作。
278+
}
279+
}
222280

281+
class User{
282+
String name;
283+
int age;
284+
public User(String tom, int i) {
285+
}
286+
}
287+
```
223288

289+
除了AtomicReference ,Atomic 还提供了AtomicStampedReference、AtomicMarkableReference
224290

225-
#### 原子变量
226291

227-
- 类的小工具包,支持在单个变量上解除锁的线程安全编程。事实上,此包中的类可 将 volatile 值、字段和数组元素的概念扩展到那些也提供原子条件更新操作的类。
228-
- 类 AtomicBoolean、AtomicInteger、AtomicLong 和 AtomicReference 的实例各自提供对 相应类型单个变量的访问和更新。每个类也为该类型提供适当的实用工具方法。
229-
- AtomicIntegerArray、AtomicLongArray 和 AtomicReferenceArray 类进一步扩展了原子操 作,对这些类型的数组提供了支持。这些类在为其数组元素提供 volatile 访问语义方 面也引人注目,这对于普通数组来说是不受支持的。
230-
- 核心方法:boolean compareAndSet(expectedValue, updateValue)
231-
- java.util.concurrent.atomic 包下提供了一些原子操作的常用类:
232-
- AtomicBoolean 、AtomicInteger 、AtomicLong 、 AtomicReference
233-
- AtomicIntegerArray 、AtomicLongArray
234-
- AtomicMarkableReference
235-
- AtomicReferenceArray
236-
- AtomicStampedReference
237292

293+
### 解决ABA 问题
238294

295+
各种乐观锁的实现中通常都会用版本戳 version 来对记录或对象标记,避免并发操作带来的问题
239296

240-
## 原理
297+
在Java中,AtomicStampedReference\<V> 也实现了这个作用,它通过包装[E,int]的元组来对对象标记版本戳stamp,从而避免ABA问题
241298

242-
自旋锁 UnSafe类
299+
```java
300+
public class AtomicStampedReferenceDemo {
301+
302+
static AtomicStampedReference<String> asf = new AtomicStampedReference<>("A", 1);
303+
304+
public static void main(String[] args) {
243305

244-
## Java的Atomic类分析
306+
new Thread(() -> {
307+
String value = asf.getReference();
308+
System.out.println("Thread1 current value: " + asf.getReference() + ", stamp: " + asf.getStamp());
309+
310+
asf.compareAndSet(value, "B", asf.getStamp(), asf.getStamp() + 1);
311+
System.out.println("Thread1: " + value + "——>" + asf.getReference() + ",stamp:" + asf.getStamp());
312+
value = asf.getReference();
313+
asf.compareAndSet(asf.getReference(), "A", asf.getStamp(), asf.getStamp() + 1);
314+
System.out.println("Thread1: " + value + "——>" + asf.getReference() + ",stamp:" + asf.getStamp());
315+
}).start();
316+
317+
new Thread(() -> {
318+
String value = asf.getReference();
319+
320+
int stamp = asf.getStamp();
321+
System.out.println("Thread2 current value: " + asf.getReference() + ", stamp: " + stamp);
322+
323+
try {
324+
TimeUnit.SECONDS.sleep(3);
325+
} catch (InterruptedException e) {
326+
e.printStackTrace();
327+
}
328+
System.out.println("Thread2: " + value + "——>" + "B" + ",stamp:" + stamp + 1);
329+
boolean flag = asf.compareAndSet(value, "B", stamp, stamp + 1);
330+
if (flag) {
331+
System.out.println("Thread2 update from " + value + " to B");
332+
} else {
333+
System.out.println("Thread2 update fail");
334+
}
335+
}).start();
336+
}
337+
}
338+
```
339+
340+
```
341+
Thread1 current value: A, stamp: 1
342+
Thread2 current value: A, stamp: 1
343+
Thread1: A——>B,stamp:2
344+
Thread1: B——>A,stamp:3
345+
Thread2: A——>B,stamp:11
346+
Thread2 update fail
347+
```
245348

246-
https://blog.csdn.net/goodlixueyong/article/details/51339689

0 commit comments

Comments
 (0)