CAS 是什麼
-
CAS 的全稱 Compare-And-Swap,它是一條 CPU 併發操作。
-
它的功能是判斷內存某一個位置的值是否爲預期,如果是則更改這個值,這個過程就是原子的。
-
CAS 併發原體現在 JAVA 語言中就是 sun.misc.Unsafe 類中的各個方法。調用 UnSafe 類中的 CAS 方法,JVM 會幫我們實現出 CAS 彙編指令。這是一種完全依賴硬件的功能,通過它實現了原子操作。由於 CAS 是一種系統源語,源語屬於操作系統用語範疇,是由若干條指令組成,用於完成某一個功能的過程,並且原語的執行必須是連續的,在執行的過程中不允許被中斷,也就是說 CAS 是一條原子指令,不會造成所謂的數據不一致的問題。
-
分析一下 getAndAddInt() 這個方法
// unsafe.getAndAddInt public final int getAndAddInt(Object obj, long valueOffset, long expected, int val) { int temp; do { temp = this.getIntVolatile(obj, valueOffset); // 獲取快照值 } while (!this.compareAndSwap(obj, valueOffset, temp, temp + val)); // 如果此時 temp 沒有被修改,就能退出循環,否則重新獲取 return temp; }
CAS 的缺點?
- 循環時間長開銷很大
- 如果 CAS 失敗,會一直嘗試,如果 CAS 長時間一直不成功,可能會給 CPU 帶來很大的開銷(比如線程數很多,每次比較都是失敗,就會一直循環),所以希望是線程數比較小的場景。
- 只能保證一個共享變量的原子操作
- 對於多個共享變量操作時,循環 CAS 就無法保證操作的原子性。
- 引出 ABA 問題
CAS 底層原理?談談對 UnSafe 的理解?
public class CASDemo {
public static void main(String[] args) {
AtomicInteger atomicInteger = new AtomicInteger(666);
// 獲取真實值,並替換爲相應的值
boolean b = atomicInteger.compareAndSet(666, 2019);
System.out.println(b); // true
boolean b1 = atomicInteger.compareAndSet(666, 2020);
System.out.println(b1); // false
atomicInteger.getAndIncrement();
}
}
getAndIncrement()方法
/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
UnSafe 類是什麼?
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
// 獲取下面 value 的地址偏移量
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
// ...
}
- Unsafe 是 CAS 的核心類,由於 Java 方法無法直接訪問底層系統,而需要通過本地(native)方法來訪問, Unsafe 類相當一個後門,基於該類可以直接操作特定內存的數據。Unsafe 類存在於 sun.misc 包中,其內部方法操作可以像 C 指針一樣直接操作內存,因爲 Java 中 CAS 操作執行依賴於 Unsafe 類。
- 變量 vauleOffset,表示該變量值在內存中的偏移量,因爲 Unsafe 就是根據內存偏移量來獲取數據的。
- 變量 value 用 volatile 修飾,保證了多線程之間的內存可見性。
CAS的ABA問題
- ABA 問題是怎麼產生的
public class ABADemo {
private static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
public static void main(String[] args) {
new Thread(() -> {
atomicReference.compareAndSet(100, 101);
atomicReference.compareAndSet(101, 100);
}).start();
new Thread(() -> {
// 保證上面線程先執行
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicReference.compareAndSet(100, 2019);
System.out.println(atomicReference.get()); // 2019
}).start();
}
}
- 原子更新引用
// 原子引用
public class AtomicReferenceDemo {
public static void main(String[] args) {
User cuzz = new User("cuzz", 18);
User faker = new User("faker", 20);
AtomicReference<User> atomicReference = new AtomicReference<>();
atomicReference.set(cuzz);
System.out.println(atomicReference.compareAndSet(cuzz, faker)); // true
System.out.println(atomicReference.get()); // User(userName=faker, age=20)
}
}
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* 時間戳原子引用
**/
public class ABADemo2 {
private static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
public static void main(String[] args) {
new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + " 的版本號爲:" + stamp);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1 );
atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1 );
}).start();
new Thread(() -> {
int stamp = atomicStampedReference.getStamp();
System.out.println(Thread.currentThread().getName() + " 的版本號爲:" + stamp);
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean b = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1);
System.out.println(b); // false
System.out.println(atomicStampedReference.getReference()); // 100
}).start();
}
}
我們先保證兩個線程的初始版本爲一致,後面修改是由於版本不一樣就會修改失敗。