CAS(比較並交換)一個小demo
import java.util.concurrent.atomic.AtomicInteger;
public class CasDemo {
public static void main(String[] args) {
//默認初始值爲5,也就是主存中的值爲5
AtomicInteger atomicInteger = new AtomicInteger(5);
//compareAndSet期望並交換,期望是第一個參數,期望拿走內存時和放回時實際內存值相同,更新值是更新到多少
System.out.println(atomicInteger.compareAndSet(5, 2048));
System.out.println("cas number:"+atomicInteger.get());
System.out.println(atomicInteger.compareAndSet(5, 2222));
System.out.println("cas number:"+atomicInteger.get());
}
}
第一次CAS是因爲初始化AtomicInteger是5,所以交換成功(volatile關鍵字保證內存值實時同步可見)
第二次CAS中內存的值已被修改爲2048所以無法修改返回false
下面是原子類CAS的源碼
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
第一個參數爲期望值也就是期望內存中的值爲多少第二個參數爲更新到多少
compareAndSwapInt方法源碼,在Unsafe類中
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
其爲native本地方法,不由Java實現
方法的作用就是,Object var1爲當前對象,long var2爲當前對象內存偏移量,var4爲要進行的操作,var5爲當前主內存地址的值
CAS底層原理
直接是對地址進行操作所以不需要加sync鎖就可以保證安全性,CAS憑什麼能保證原子性,靠的就是底層的unsafe類,Unsafe類是操作系統級別的操作,直接調用的是操作系統底層資源執行相應的任務
由原語所決定,原語默認是連續性的,不存在被中斷 ,也就是說CAS是一條CPU原子指令,不會造成所謂的數據不一致問題
CAS自旋鎖
加sync同一個時間段只能由一個線程來訪問
一致性得到了保障但是併發性下降
cas一直循環,既提高了一致性又保證了併發性
以下就是CAS的應用:
AtomicInteger中的自增操作getAndIncrement(),其中
this是當前對象
ValueOffset是當前對象內存偏移量
i是加一的操作
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
Unsafe類中getAndAddInt()方法中的CAS自旋鎖
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;
}
第一個var5就是保存當前地址下的值(也就是主物理內存裏的值) var5=this.getIntVolatile(var1,var2);(相當於先獲取值在自增1(自增1的1就是var4))
whlie裏面意思就是
比較並交換先檢測var1這個對象和var2這個內存偏移量上的值是否與剛剛獲取的var5相同,若相同則加var4也就是加1
var5是期望值
假如var1和var2的內存地址上的值跟期望值var5不相同,則while循環中this.compareAndSwapIntI(var1,var2,var5,var5 + var4)爲假然後!號取反然後繼續do內循環,再去找此內存地址上的值,再做比較,直到成功(也就是var5的值和var1var2對應的值相同時,這也是自旋鎖的自旋之處)爲止(這點會一直循環嗎,只要不成功???疑惑)
疑惑解析!!!
do裏面的var5會更新,因爲此對象地址值var1var2已被volatile修飾,根據jmm內存可見所以就繼續this.compareAndSwapIntI(var1,var2,var5,var5 + var4),然後直成功爲止因爲可能有很多線程在掛起時搶先執行
但是依然會造成一直匹配不到值的循環操作,這也是CAS的一個缺點
第二個缺點就是隻能對一個對象進行原子操作,並不能對一塊代碼進行原子操作
第三個缺點就是接下來要講的ABA問題
ABA問題的產生:
ABA問題就是加入主存中的值爲A
(1)兩個線程都會去主存裏拿到一份值A
(2)t1線程由於執行時間短,兩秒,所以執行了又A->B->A的操作
(3)由於JMM原理,主存中也是同樣執行了這樣的操作
(4)線程t2在執行完之後看到主存中的值還是A就進行交換,但其實已經被修改過了,又改回來了
解決:
加上一個時間戳,每改變一次,自增一次,相當於版本號
使用原子引用類AtomicStampedReference
public AtomicStampedReference(V initialRef, int initialStamp) {
pair = Pair.of(initialRef, initialStamp);
}
參數一還是對象,參數二則是一個初始版本號
然後每次改變主存中的值就會改變版本號,這樣就有效 避免了ABA問題的發生
解決ABA問題demo如下
package com.wsx.aba;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicStampedReference;
public class SloveAba {
static AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100,1);
public static void main(String[] args) {
System.out.println("ABA問題的產生");
new Thread(()->{
atomicReference.compareAndSet(100,101);
atomicReference.compareAndSet(101,100);
System.out.println(Thread.currentThread().getName()+"\t"+atomicReference.get());
},"t1").start();
new Thread(()->{
try {
//爲了讓t1完成一次ABA操作
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicReference.compareAndSet(100,2019);
System.out.println(Thread.currentThread().getName()+"\t"+atomicReference.get());
},"t2").start();
//主線程睡眠等待上限操作完成
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("ABA問題的解決");
new Thread(()->{
int stamp = atomicStampedReference.getStamp();
//這裏睡一秒是爲了t3線程和t4線程的起點一致,版本號都爲1
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"\t最新版本號"+atomicStampedReference.getStamp());
//ABA帶版本號操作
atomicStampedReference.compareAndSet(100,101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t最新版本號"+atomicStampedReference.getStamp());
atomicStampedReference.compareAndSet(101,100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1);
System.out.println(Thread.currentThread().getName()+"\t最新版本號"+atomicStampedReference.getStamp());
},"t3").start();
new Thread(()->{
int stamp = atomicStampedReference.getStamp();
//這裏睡兩秒是爲了讓t3線程完成ABA操作
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
//這裏的stamp是默認沒被修改的版本號,如果被其他線程修改過那麼這裏版本號就開始落後,然後修改失敗
boolean result = atomicStampedReference.compareAndSet(100, 2019, stamp, stamp + 1);
System.out.println(Thread.currentThread().getName()+"\t最新版本號"+atomicStampedReference.getStamp());
System.out.println(Thread.currentThread().getName()+"\t是否改動成功"+result);
System.out.println(Thread.currentThread().getName()+"\t最新值"+atomicStampedReference.getReference());
},"t4").start();
}
}