从 Atomic 到 CAS,竟然衍生出这么多 20k+ 面试题

‍ 面试官:“CAS 知道吗,如何实现?讲一讲AtomicInteger,为什么要用 CAS 而不是 synchronized?CAS 底层原理,谈谈你对 UnSafe 的理解?CAS 有什么缺点吗?Atom p s 1 {icInteger 的ABA问题,能说一下吗,原子更新引用知道吗?如何规避 ABA 问题?”

前言

Java 内存模型要保证可见性,原子性和有序性。

在 JDK 5之前 Java 语言是靠 synchronizO 6 H =e! ; ) W ld 关键字保证同步的,但 synchronized 是一种独占锁,是一种悲观锁, 会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁 ,效率不是很高。

Java 虚拟机又提供了一个轻量级的同步机制——volatile(

面试必问的 volatile,你真的理解了吗

但是 volatile 算是乞丐版的 synchronized,并不能保证原子性 ,所以,又增加了java.util.concurrent.atomic包, 这_ c c % I个包下提供了一系列原子类U i / h Q

1. Atomic 原子1 R b J N j a p }

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

从 Atomic 到 CAS,竟然衍生出这么多 20k+ 面试题

Atomic 包中的类可以分成 4 组:

  1. 基本类型:AtomicBoolean,D u %AtomicInteger,AtomicLong
  2. 数组类型:ton r # Z P UmicIntegerArraV Z % ( 5 f U y,AtomicLongArray,AtomicReferenceArray
  3. 引用类型:AtomS 5 ( i ) 8icReference,AtomicMarkable0 g H e ?Reference,AtomicStampedReference
  4. 对象的属性修改类型 :AtomicIntegerFieldUpdater,AtomicLongFieldUpdater,AtomicReferenceFieldUpdater
  5. JDK1.8新增:DoubleAccumulator、LongAccumulator、Doub5 a V eleAdder、R X g vLongAdder、Striped64

以 AtomicInteger 为例了解常用方法

方法描述get()直接返回值addAnd0 a ( 7 Y :Get(int)增加指定的数据后返回增加后的数据,相当于 i++getAndAdd(int)增加指定的数据,返回变化前的数据,相当于 ++igetAndIncrement()增加1,返回增加前的数据getAnd, u /Decrement()减少1,返回减少前的数据getAndSet(int)设置指定的数据,返回设置前的数据decrementAndGet()减少1,返回减少后的值incrementAndGet()增加1,返回增加后的值w K t 8floatValue()转化为浮点数返回intValue()转化为int 类型返回set(int)设置为给定值] x d $ Q y _ `lazySet(int)仅仅当get时才会set h9 ( 9 u x Cttp://ifeve.com/juc-atomic-class-lazyset-que/compareAndSet(int, int)尝试新增后对比,若增加成功则返回true否则返回false

Coding~~~

public class CASDt 2 c Iemo {
publicb U ` d ] u . z F static void main(String[] args) {
System.out.println(num.compareAndSet(6, 7) + \"\\t + current num:\" + num);
System.out.println(num.coq R kmpareAndSet(6, 7R s ? V l + Y) + \"\\t current num:\" + num);
}
}
truer x T } + : f = +	 + current num:7
false current num:7

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

compareAndSet() 尝试新增后对比J o Y 0 g m % K *,若增_ 4 p I &加成功则返回true否则返回false。其实就是比较并交换,判断用当前值和期望值(第一个参数),是否一致,如果一致,修改为更新值(第二个参数),这就是大名鼎鼎的 CAS。

2. CAS 是什么

2.1 CAS 算法

  • CAS:全称 Compare and swap,即比较并交换,它是一条 CPU 同步原+ | _ r w $ ] 8。是一种硬件对并发的支持,针对多处理器操作而设计的一种特殊指令,用于管理对共享数据的并发访问。
  • CAS 是一种无锁的非阻塞算法的实现。
  • CAD N V US 包含了 3 个操作数:
    • 需要读写的内存值 V
    • 旧的预期值 A
    • 要修改的更新值 B
  • 当且仅当 V 的值等于 A 时,CAS 通过原子方式用新值 B 来更新 V 的 值,否则不会执行任何操q 1 : $ W y p / 6作(他的功能是判断内存] a i P Y o z g某个位置的$ x 3 e o Q P %值是否为预期值,_ 1 x N 8 L O s ,如果是则更改为新的值,这个过程是原子的。)

CAS 并发原语体现在 Java 语} ! C v * { # Y言中的 sum.misc.Unsafe 类中的各个方法。调用 U4 C Vn{ ` p Ssafe 类中的 CAS 方法2 - p, JVM 会帮助我们实现出 CAS 汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调9 H D O & a ],由于 CAS是一种系统原语,原语属于操作系统用于范畴,是由若干条指C y F ; P U令组成的,用于完成某个功5 ? / P .能的一L * X v ? m t ;个过程,并且原语的执行必须是连续的在执L p / J W c V行过程中不允许被中断,CAS 是一条 CPU 的原子指令,不会造成数据不一致问题。

我们常用的7 v % 5 y java.util.concurrent 包就建立在CAS之上。

2.2 用 CAS 分析 AtomicInteger 类

查看 AtomicInteger 代码如下,可以看到该Y k 4 6 - , % 2 `类下的方法大部分是 调用了 Unsafe

从 Atomic 到 CAS,竟然衍生出这么多 20k+ 面试题

2.2.1 UnSafe 类

是 CAS 的核心类,由于 Java 方法无法直接访问底层系统,需要通过本W Z U d [地(native)方法来访问,UnSafe 相当u W W 3于一个后门,基于该类可以直接操作特定内存的数据。UnSafe; X 8 类存在与 sum.misc 包中,其内部方法可以像 C 语言的指针一样直接操作内存,因为 Java 中 CAS 操作的执行依赖于 UnSaV ? Efe 类的b 3 , ! ! [ & c *方法。

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

public final class Unsafe {
private static final Unsafe theUnsafe;
// ......
@CallerSensitive
pu0 H : iblic static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass()D L V Z n Q 7 [;
i{ Z f @ Jf (!VM.isSysh N ~ 9 O 3temDomainLo6 X * ?ader(var0.getClasJ P K TsLoader())) {
throw new SecurityException(\"Unsafe\");
} else {
return theUc 2 U b & gnsafe;I 7 V e
}
}
public9 5 V u J native int getInt(Object var1, long5 I ; ^ t o e var2);
public native void putInt(ObjeO u + g X Hct var1P X 6 [ U, long var2, int var4);
pX = [ublic natiO i f + q 4 S (ve Object getObject(Objq } ~ F g _ {ect var1,P 0 K long var2);
publicb w # l native void putObject(Object var1, long vv v } X lar2, Object var4);

publP N ] 3 9 d lic final native boolean compareAy - 6 V c P 1ndSwapObject(c ) [ + _ P m BObject var1j : k { B, long var2, Object var4, ObW u b V n d J z Qject var6 8 # ! *5);

public final native booleanP x } E compareAndSwapInt(Object var1, long var2, int var4, int var5);
// ......
}

Unsafe 类为一单例实现,提供静态方法 getUnsafe 获取 Unsafe 实例A / 4 Z A c y,当且仅Y ( + t s e J { Z当调用 getUnsafe 方法的. w 7 4 R j 7 Z _类为引导类加载器所加载时才合法,否则抛出 SecurityExceP o 4 z Aption 异常

2.2.2 valueOffset

AtomicInteger 中的变量 valueOffset 表示该/ z ;变量值在内存中的偏移地址,因为 Unv + S WSafe 就是根据内存偏移地址获取数据。

public final int getAndIncr( 0 Z j !ement() {
return u7 S : ] % } Knsafe.getAndAddInt(this, valueOffset, 1);
}

2.2.3 volatile int value

变量 value 用 volatile 修饰,保证了多线j 3 S Y L p程之间的内存可见性。

2.2.4 举个栗子

我们用线程安全的 ++i 举例,也就是 AtomicInteger 中的 getA+ 4 g Q ( c nndAdd

逐层看 Unsafm } w U V We 类中的 getAndAdd() 的源码如下

public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
reS K n /turn var5;
}

h ; * 6 | m k q

可以看到 getAndAddInt 方法有 4 个参数

  • val1:Atomi7 T } + 8 m C H ,cInteger 对象本身
  • var2:该对象值的引用地址,内存偏移量
  • var4:L m r需要变动的数量,即 ++i 的 i
  • var5:用var1, var2 找w K % % ? 7 L n出的主内存中真实的值(通过内存偏移量)

this.compareAndSwapInk @ x } s 4 Tta D ; : 用该对p E o象当前的值与| A S x n P % B k var5 比较,如果相同,更新 var5 + var4 并且返回 true,如果不同,继续取值然后再比较,直到更新完成。

这一操作没有加锁,反复执行,] c & @ K S ^ 4 W保证了一致性,又保证了并发性

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

  1. AtomicInteger 里面的 value 原始值为 3,即主内o % ( P e Z N D存中 AtomicInteger 的 value 为 3,根据 JMM 模型,线程A和线程B各自持有一份值为 3 的 valun O E F u _ 8 Ge 的副本分别到各自的工作内存;
  2. 线程A通过 getIntVolatile({ g S @var1,var2) 拿到 value 值3,这时线程A被挂起;
  3. 线程B也通过 getIntVolatile(vaO a h ? A j y 7r1,var2) 方法获取到 value 值 3,此时刚好线程B没W v [ z有被挂起并执行compareAndSwapInV R e vt 方法比较内存值为 3,成功修改内存值为 4,线程B结束,一切正常
  4. 这时线程A恢复,执行compareAndSwapInt() 方法比较,发现自己手里的3和主内存的值4不一致,说明该值已经被其他线程抢先一步修改过了,那线程A本次修改失败,重新读取;
  5. 线程A重新获取value值,因为变量a W ]vF # 4alue 被 volatil` 0 F Ce 修饰,所以其他_ : U C线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功

2.2.5 使用 UnSafe 类

那如若想使用这个类,该如何获取其实例U V u f / 6 ; y?有如下两个可行方案

  1. 从getUnsa9 + i i * zfe 方法的使用限制条件出发,通过Java命令行命令 -Xbootclasspath/a 把调用 Unsafe 相关方法的类A所在 jar 包路径追加到默认的 bootstrap 路径中,使得A被引导类加载器加载,从而通过Unsafe.getUnsafe方法安全的获取 Unsafe 实例。
java -& f V / b 9 EXbootclasspath/a:L H q x T d ${path}   // 其中path为调7 V X O r X A O用Unsafe相关方法的类所在jar包路径
  1. 通过反射技术暴力获取 Unsafe 对象
   private static Unsafe reflectGetUnsafe() {
try {
Field field = Unsafe.class.getDeclaredFi9 W r / r 1 y O :eld(\"theUnsafe\");
field.setAccessible(true);
return (Unsafe) field.get(null);
} catch/ V c 0 (Exception e) {
log.error(w m & ( u 2e.getMessage(), e);
rC L z U # Y 8 L 1eturn null;
}
}

美团技术6 ] Y团队有一篇介绍Unsafe 类的文章:Java魔法类:Unsafe应用解析

3. CAS 缺点

  • 循环时间长,开销很大。CAS算法需要不断地自旋来读取最新的内存值,长时间读取不到就会造成不必要的CPU开销。do while 如果CAS失败,会一直进行尝试,如果CAS长时间一直不成功,可能会给CPU带来很大的开销
  • 只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是,对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁来保证原子性。
  • ABA 问题

ABA 问题

ABA 问题是什么?是如何产生的?

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

比如r ; i线程1从内存位置 V 中O [ ,取出A,这时线程2也从内o 8 ! 4 / ?存中取出A,并且线程2进行了一些操作将值变成了B,然后线程2又将V位置的数据变成A,这个时候线程1进行CAS操作c | g发现内存中仍然是A,线程1就会误认为它没有被修改过,这个漏洞就是CAS操作的\"ABA\"+ U ,问题。u ? r C y

u G # o管线程= ; c #1的CAS操作成功,但是不代表这个过程就是没有问题的。

原子引用

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

public class AtomicRefrenceDemo {
public static void main(String[] args) {
User tom = new User(\"tom\",18);
User jim = neL ~ ) m d 5 zw User(\"jim\",20);
AtomicReference<User> user = new AtomicReference<>();
user.set(tom);
System.out.println(user.compareAndSet(tom,T o } X [ 0 F 9 : jim)u Z } w W N [+\"\\t\"+user.get().) % x B d ,toString());
System.ouD H T Pt.printlD e t g ^ On(user.compareAndSet(tom, jim)+\"\\t\"G } A p . 9+user.get().toString());
}
}
clg r M G m , 3 Q ass User{
String name;
int ageh ? J | 6 s;
public User(Striu a W w J E )ng tom, int i) {
}
}

除了AtomicReferc + $ence ,AtM l 5omic 还提供了AtomicStampedReference、AtomicMarkableReference

解决ABA 问题

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

在Java1 [ Z . Q P , 1 Y中,AtomicStampe} m (dReferenI o ice<V> 也实现了这个作用,它通过包装[E,int]的元组来对对象标记版本戳stamp,从而避免ABA问题

public class AtomicStampedR1 O XeferenceDemo {
static At` D l | d 2 DomicStampedReference<String> asf = new A_ L / = w u ? AtomicStampedReference<>(\"A\", 1);
public static void main(String[] args) {
new Threa( 2 ~ u W c ? Qd(() -> {
String value = asf.getReference();
System.out.println(\"Thread1 current value: \" + asf.get; J C v + y H |Reference() + \", stamp: \" + asf.getStamp());
asf.compareAndSet(value, \"B\", asf.getStamp(), asf.getStamp() + 1);
System.out.println(\"Thread1:. Z J\" + value + \"——>\" + asf.getReference(( $ x G } 9 B c q) + \",stamp:\" + asf.getStamp()i 8 o o p);
v` } ealue = asf.getReferenceW K ! * q A b ( ^();
asf.compareAndSet(asf.getReference(), \"A\", as5 5 R p r 5 i f.getStamp(), asf.getStamp() + 1);
System.out.println(\"Thread1:\" + value + \"——>\"X % B ? O Y E H + asf.getReK ? D l c 6 )ference() + \",stamp:\" + asf.getStamp(j 9 n u ~ 6));
}).start();
new Thread(() -> {
String value = as? a y 4 * 1 g Hf.getReferer g Q ) ^nce();
int stamp = asf.getStamp();
System.out.println(\"Thread2 current value: \" + asf.getReference() + \", stamp: \" + stamp);
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStav x m J C A 4 P uckTrace()U a w N # O;
}
System.out.println(\"Thread2: \" + value + \"——>\" + \"B\" + \",stamp:\" + stamp +J * N ! 1);. Z 0 t P S - z A
boolean flaM I Q Sg = asf.compareAndSet(va^ * ` ? y f i Glue, \"B\", stamp, stamp + 1);
if (flag) {
System.out.println(\"Thread2 update from \" + value + \" to B\");
} else {
SyP i ] b 5 ystem.out.printa , @ (l! 2 ~ s ` ] J gn(\"Thread2 update fail\");
}
}).start();
}
}
Thread1 currentV 6 : = Y i val$ ~ H o = N Q g !ue: A, stamp: 1
Thread2 current value: A, stamQ X ( 9 Rp: 1
Thread1:A——&Y . c $ M G d : Egt;B,stamp:2
Thc f a N R v F : kread1:B——>A,stamp:3
Thread2/ G x L: A——>B,stamp:11
Thread2 update fail
上一篇

全球疫情最大黑马:患者超中国100000例,股市却再次雪崩

下一篇

多家车企发布2019年业绩报告 市场调整或进一步加剧

你也可能喜欢

  • 暂无相关文章!

发表评论

您的电子邮件地址不会被公开。 必填项已用 * 标注

提示:点击验证后方可评论!

插入图片
返回顶部