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
37371 . 基本类型:AtomicBoolean,AtomicInteger,AtomicLong
38382 . 数组类型:tomicIntegerArray,AtomicLongArray,AtomicReferenceArray
39393 . 引用类型:AtomicReference,AtomicMarkableReference,AtomicStampedReference
40404 . 对象的属性修改类型 :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
114113UnSafe 类中的所有方法都是 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
121151public 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
137169public 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结束,一切正常
1682004 . 这时线程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
195252CAS算法实现一个重要前提是需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。
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