從 Atomic 到 CAS,竟然衍生出這麼多 20k+ 面試題

‍ 面試官:“CAS 知道嗎,如何實現?講一講AtomicInteger,為什麼要用 CAS 而不是 synchronized?CAS 底層原理,談談你對 UnSafe 的理解?CAS 有什麼缺點嗎?AtomicInteger 的ABA問題,能說一下嗎,原子更新引用知道嗎?如何規避 ABA 問題?”

前言

Java 內存模型要保證可見性,原子性和有序性。

在 JDK 5之前 Java 語言是靠 synchronized 關鍵字保證同步的,但 synchronized 是一種獨佔鎖,是一種悲觀鎖, 會導致其它所有需要鎖的線程掛起,等待持有鎖的線程釋放鎖 ,效率不是很高。

Java 虛擬機又提供了一個輕量級的同步機制——volatile(

面試必問的 volatile,你真的理解了嗎

但是 volatile 算是乞丐版的 synchronized,並不能保證原子性 ,所以,又增加了java.util.concurrent.atomic包, 這個包下提供了一系列原子類。

1. Atomic 原子類

Atomic 原子類可以保證多線程環境下,當某個線程在執行 atomic 的方法時,不會被其他線程打斷,而別的線程就像自旋鎖一樣,一直等到該方法執行完成,才由 JVM 從等待隊列中選擇一個線程執行。Atomic 類在軟件層面上是非阻塞的,它的原子性其實是在硬件層面上藉助相關的指令來保證的。

從 Atomic 到 CAS,竟然衍生出這麼多 20k+ 面試題

Atomic 包中的類可以分成 4 組:

  1. 基本類型:AtomicBoolean,AtomicInteger,AtomicLong
  2. 數組類型:tomicIntegerArray,AtomicLongArray,AtomicReferenceArray
  3. 引用類型:AtomicReference,AtomicMarkableReference,AtomicStampedReference
  4. 對象的屬性修改類型 :AtomicIntegerFieldUpdater,AtomicLongFieldUpdater,AtomicReferenceFieldUpdater
  5. JDK1.8新增:DoubleAccumulator、LongAccumulator、DoubleAdder、LongAdder、Striped64

以 AtomicInteger 為例瞭解常用方法

方法描述get()直接返回值addAndGet(int)增加指定的數據後返回增加後的數據,相當於 i++getAndAdd(int)增加指定的數據,返回變化前的數據,相當於 ++igetAndIncrement()增加1,返回增加前的數據getAndDecrement()減少1,返回減少前的數據getAndSet(int)設置指定的數據,返回設置前的數據decrementAndGet()減少1,返回減少後的值incrementAndGet()增加1,返回增加後的值floatValue()轉化為浮點數返回intValue()轉化為int 類型返回set(int)設置為給定值lazySet(int)僅僅當get時才會set http://ifeve.com/juc-atomic-class-lazyset-que/compareAndSet(int, int)嘗試新增後對比,若增加成功則返回true否則返回false

Coding~~~

<code>public class CASDemo {
public static void main(String[] args) {
System.out.println(num.compareAndSet(6, 7) + "\\t + current num:" + num);
System.out.println(num.compareAndSet(6, 7) + "\\t current num:" + num);
}
}
/<code>
<code>true\t + current num:7
false\t current num:7
/<code>

執行兩次結果卻不同,Why?

compareAndSet() 嘗試新增後對比,若增加成功則返回true否則返回false。其實就是比較並交換,判斷用當前值和期望值(第一個參數),是否一致,如果一致,修改為更新值(第二個參數),這就是大名鼎鼎的 CAS。

2. CAS 是什麼

2.1 CAS 算法

  • CAS:全稱 Compare and swap,即比較並交換,它是一條 CPU 同步原語。是一種硬件對併發的支持,針對多處理器操作而設計的一種特殊指令,用於管理對共享數據的併發訪問。
  • CAS 是一種無鎖的非阻塞算法的實現。
  • CAS 包含了 3 個操作數:
    • 需要讀寫的內存值 V
    • 舊的預期值 A
    • 要修改的更新值 B
  • 當且僅當 V 的值等於 A 時,CAS 通過原子方式用新值 B 來更新 V 的 值,否則不會執行任何操作(他的功能是判斷內存某個位置的值是否為預期值,如果是則更改為新的值,這個過程是原子的。)

CAS 併發原語體現在 Java 語言中的 sum.misc.Unsafe 類中的各個方法。調用 Unsafe 類中的 CAS 方法, JVM 會幫助我們實現出 CAS 彙編指令。這是一種完全依賴於硬件的功能,通過它實現了原子操作。再次強調,由於 CAS是一種系統原語,原語屬於操作系統用於範疇,是由若干條指令組成的,用於完成某個功能的一個過程,並且原語的執行必須是連續的在執行過程中不允許被中斷,CAS 是一條 CPU 的原子指令,不會造成數據不一致問題。

我們常用的 java.util.concurrent 包就建立在CAS之上。

2.2 用 CAS 分析 AtomicInteger 類

查看 AtomicInteger 代碼如下,可以看到該類下的方法大部分是 調用了 Unsafe

從 Atomic 到 CAS,竟然衍生出這麼多 20k+ 面試題

2.2.1 UnSafe 類

是 CAS 的核心類,由於 Java 方法無法直接訪問底層系統,需要通過本地(native)方法來訪問,UnSafe 相當於一個後門,基於該類可以直接操作特定內存的數據。UnSafe 類存在與 sum.misc 包中,其內部方法可以像 C 語言的指針一樣直接操作內存,因為 Java 中 CAS 操作的執行依賴於 UnSafe 類的方法。

UnSafe 類中的所有方法都是 native 修飾的,也就是說該類中的方法都是直接調用操作系統底層資源執行相應任務。

<code>public final class Unsafe {
private static final Unsafe theUnsafe;
\t// ......
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
public native int getInt(Object var1, long var2);
public native void putInt(Object var1, long var2, int var4);
public native Object getObject(Object var1, long var2);
public native void putObject(Object var1, long var2, Object var4);

public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
// ......
}
/<code>

Unsafe 類為一單例實現,提供靜態方法 getUnsafe 獲取 Unsafe 實例,當且僅當調用 getUnsafe 方法的類為引導類加載器所加載時才合法,否則拋出 SecurityException 異常

2.2.2 valueOffset

AtomicInteger 中的變量 valueOffset 表示該變量值在內存中的偏移地址,因為 UnSafe 就是根據內存偏移地址獲取數據。

<code>public final int getAndIncrement() {
\treturn unsafe.getAndAddInt(this, valueOffset, 1);
}
/<code>

2.2.3 volatile int value

變量 value 用 volatile 修飾,保證了多線程之間的內存可見性。

2.2.4 舉個栗子

我們用線程安全的 ++i 舉例,也就是 AtomicInteger 中的 getAndAdd

逐層看 Unsafe 類中的 getAndAdd() 的源碼如下

<code>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));
return var5;
}
/<code>

解毒

可以看到 getAndAddInt 方法有 4 個參數

  • val1:AtomicInteger 對象本身
  • var2:該對象值的引用地址,內存偏移量
  • var4:需要變動的數量,即 ++i 的 i
  • var5:用var1, var2 找出的主內存中真實的值(通過內存偏移量)

this.compareAndSwapInt 用該對象當前的值與 var5 比較,如果相同,更新 var5 + var4 並且返回 true,如果不同,繼續取值然後再比較,直到更新完成。

這一操作沒有加鎖,反覆執行,既保證了一致性,又保證了併發性

假設線程A和線程B兩個線程同時執行 getAndAddInt 操作(分別跑在不同CPU上):

  1. AtomicInteger 裡面的 value 原始值為 3,即主內存中 AtomicInteger 的 value 為 3,根據 JMM 模型,線程A和線程B各自持有一份值為 3 的 value 的副本分別到各自的工作內存;
  2. 線程A通過 getIntVolatile(var1,var2) 拿到 value 值3,這時線程A被掛起;
  3. 線程B也通過 getIntVolatile(var1,var2) 方法獲取到 value 值 3,此時剛好線程B沒有被掛起並執行compareAndSwapInt 方法比較內存值為 3,成功修改內存值為 4,線程B結束,一切正常
  4. 這時線程A恢復,執行compareAndSwapInt() 方法比較,發現自己手裡的3和主內存的值4不一致,說明該值已經被其他線程搶先一步修改過了,那線程A本次修改失敗,重新讀取;
  5. 線程A重新獲取value值,因為變量value 被 volatile 修飾,所以其他線程對它的修改,線程A總是能夠看到,線程A繼續執行compareAndSwapInt進行比較替換,直到成功

2.2.5 使用 UnSafe 類

那如若想使用這個類,該如何獲取其實例?有如下兩個可行方案

  1. 從getUnsafe 方法的使用限制條件出發,通過Java命令行命令 -Xbootclasspath/a 把調用 Unsafe 相關方法的類A所在 jar 包路徑追加到默認的 bootstrap 路徑中,使得A被引導類加載器加載,從而通過Unsafe.getUnsafe方法安全的獲取 Unsafe 實例。
<code>java -Xbootclasspath/a: ${path}   // 其中path為調用Unsafe相關方法的類所在jar包路徑
/<code>
  1. 通過反射技術暴力獲取 Unsafe 對象
<code>   private static Unsafe reflectGetUnsafe() {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe) field.get(null);
} catch (Exception e) {
log.error(e.getMessage(), e);
return null;
}
}
/<code>

美團技術團隊有一篇介紹Unsafe 類的文章:Java魔法類:Unsafe應用解析

3. CAS 缺點

  • 循環時間長,開銷很大。CAS算法需要不斷地自旋來讀取最新的內存值,長時間讀取不到就會造成不必要的CPU開銷。do while 如果CAS失敗,會一直進行嘗試,如果CAS長時間一直不成功,可能會給CPU帶來很大的開銷
  • 只能保證一個共享變量的原子操作。當對一個共享變量執行操作時,我們可以使用循環CAS的方式來保證原子操作,但是,對多個共享變量操作時,循環CAS就無法保證操作的原子性,這個時候就可以用鎖來保證原子性。
  • ABA 問題

ABA 問題

ABA 問題是什麼?是如何產生的?

CAS算法實現一個重要前提是需要取出內存中某時刻的數據並在當下時刻比較並替換,那麼在這個時間差類會導致數據的變化。

比如線程1從內存位置 V 中取出A,這時線程2也從內存中取出A,並且線程2進行了一些操作將值變成了B,然後線程2又將V位置的數據變成A,這個時候線程1進行CAS操作發現內存中仍然是A,線程1就會誤認為它沒有被修改過,這個漏洞就是CAS操作的"ABA"問題。

儘管線程1的CAS操作成功,但是不代表這個過程就是沒有問題的。

原子引用

我們平時操作的不止是基本數據類型,大多數情況其實是類對象,Atomic 提供的引用類型 AtomicReference 通過泛型可以支持對對象的原子操作

<code>public class AtomicRefrenceDemo {
public static void main(String[] args) {
User tom = new User("tom",18);
User jim = new User("jim",20);
AtomicReference<user> user = new AtomicReference<>();
user.set(tom);
System.out.println(user.compareAndSet(tom, jim)+"\\t"+user.get().toString());
System.out.println(user.compareAndSet(tom, jim)+"\\t"+user.get().toString());
}
}
class User{
String name;
int age;
public User(String tom, int i) {
}
}
/<user>/<code>

除了AtomicReference ,Atomic 還提供了AtomicStampedReference、AtomicMarkableReference

解決ABA 問題

各種樂觀鎖的實現中通常都會用版本戳 version 來對記錄或對象標記,避免併發操作帶來的問題

在Java中,AtomicStampedReference 也實現了這個作用,它通過包裝[E,int]的元組來對對象標記版本戳stamp,從而避免ABA問題

<code>public class AtomicStampedReferenceDemo {
static AtomicStampedReference<string> asf = new AtomicStampedReference<>("A", 1);
public static void main(String[] args) {
new Thread(() -> {
String value = asf.getReference();
System.out.println("Thread1 current value: " + asf.getReference() + ", stamp: " + asf.getStamp());
asf.compareAndSet(value, "B", asf.getStamp(), asf.getStamp() + 1);
System.out.println("Thread1:" + value + "——>" + asf.getReference() + ",stamp:" + asf.getStamp());
value = asf.getReference();
asf.compareAndSet(asf.getReference(), "A", asf.getStamp(), asf.getStamp() + 1);
System.out.println("Thread1:" + value + "——>" + asf.getReference() + ",stamp:" + asf.getStamp());
}).start();
new Thread(() -> {
String value = asf.getReference();
int stamp = asf.getStamp();
System.out.println("Thread2 current value: " + asf.getReference() + ", stamp: " + stamp);
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Thread2: " + value + "——>" + "B" + ",stamp:" + stamp + 1);
boolean flag = asf.compareAndSet(value, "B", stamp, stamp + 1);
if (flag) {
System.out.println("Thread2 update from " + value + " to B");
} else {
System.out.println("Thread2 update fail");
}
}).start();
}
}
/<string>/<code>
<code>Thread1 current value: A, stamp: 1
Thread2 current value: A, stamp: 1
Thread1:A——>B,stamp:2
Thread1:B——>A,stamp:3
Thread2: A——>B,stamp:11
Thread2 update fail
/<code>


分享到:


相關文章: