多線程模型:

image.png
每個線程都有自己的獨立內(nèi)存空間,當線程需要操作主內(nèi)存中的數(shù)據(jù),需要先拷貝一份數(shù)據(jù)到自己的內(nèi)存空間,然后進行修改,再刷回主內(nèi)存
CAS:
CompareAndSwap,比較然后替換,多線程解決數(shù)據(jù)錯亂的方案,如上圖,試想,i=10表示10本書,t1,t2線程同時進行銷售書,如果某一時間,t1和t2線程同時將i=10拷貝到自己的數(shù)據(jù)空間,t1將書減1,i=9再刷回主內(nèi)存,即現(xiàn)在書的數(shù)目已經(jīng)為9了,然后t2線程將的工作內(nèi)存還為10它也進行減1然后刷回主內(nèi)存,這樣主內(nèi)存的i值還是為9,這樣就導(dǎo)致了已經(jīng)賣了兩本書,但是書的數(shù)目卻只減少了1,CAS便是在t2線程進行減了操作后,需要刷回主內(nèi)存的時候,將取到的值與主內(nèi)存的值作比較,相同再進行設(shè)置,不同則設(shè)置失敗。
ABA:
有了CAS可以保證操作數(shù)目一致,但是也出現(xiàn)一個問題,例如,t1線程先將書的數(shù)目減了1然后別人退還一本書,書的數(shù)目又加了1,也就是數(shù)的數(shù)目經(jīng)歷了10->9->10,然后t2線程操作時,發(fā)現(xiàn)主內(nèi)存還是為10,所以覺得沒問題就進行操作了,可能這個例子不能很好的描述問題,但是在某些案例中,會給人一種貍貓換太子的感覺,貼一個圖

image.png
圖片來自:https://baijiahao.baidu.com/s?id=1648077822185803003&wfr=spider&for=pc
一個小偷,把別人家的錢偷了之后又還了回來,還是原來的錢嗎,你老婆出軌之后又回來,還是原來的老婆嗎?ABA問題也一樣,如果不好好解決就會帶來大量的問題。最常見的就是資金問題,也就是別人如果挪用了你的錢,在你發(fā)現(xiàn)之前又還了回來。但是別人卻已經(jīng)觸犯了法律。
解決方案:
AtomicStampedReference
主要思想:通過加版本控制來進行修改,代碼如下:
public class AtomicStampedReferenceTest {
// 初始化,值為100,版本為1
static AtomicStampedReference<Integer> integerReference =
new AtomicStampedReference<>(100,1);
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(()->{
int stamp = integerReference.getStamp();
System.out.println("t1第一次版本:"+stamp);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1第一次修改成功?:"+integerReference.compareAndSet(100, 101,
stamp, stamp + 1));
stamp = integerReference.getStamp();
System.out.println("t1第二次版本:"+stamp);
System.out.println("t1第一次修改成功?:"+integerReference.compareAndSet(101,100,
stamp,stamp+1));
},"t1");
Thread t2 = new Thread(() -> {
int stamp = integerReference.getStamp();
System.out.println("t2第一次版本:"+stamp);
// 睡眠3秒保證t1完成一次ABA操作
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2修改成功?:"+integerReference.compareAndSet(100,500,
stamp,stamp+1));
System.out.println("t2:期望版本是:"+stamp+"--當前版本是:"+integerReference.getStamp());
},"t2");
t1.start();
t2.start();
t1.join();
t2.join();
}
}
- 代碼解析:
- 首先t1,t2線程同時拿到對象的最初版本1,值為100
2.t2 線程等待3秒,保證t1線程將值修改為101,然后再將值修改為100,但是版本已經(jīng)從,1->2->3
3.t2線程根據(jù)3秒前拿到的版本進行修改,即使數(shù)據(jù)值都為100,但是3秒前的版本是1,現(xiàn)在的版本為3,所以修改失敗
打印輸出:
- 首先t1,t2線程同時拿到對象的最初版本1,值為100
t1第一次版本:1
t2第一次版本:1
t1第一次修改成功?:true
t1第二次版本:2
t1第一次修改成功?:true
t2修改成功?:false
期望版本是:1--當前版本是:3
問題:
一切看上去好像都沒什么問題,但是當我們把初始值改成大于127的數(shù)值,代碼如下,修改的地方已經(jīng)標注,其實只是將里面的100全部替換為400
public class AtomicStampedReferenceTest {
static AtomicStampedReference<Integer> integerReference =
// 將100改成400
new AtomicStampedReference<>(400,1);
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(()->{
int stamp = integerReference.getStamp();
System.out.println("t1第一次版本:"+stamp);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 將100改成400
System.out.println("t1第一次修改成功?:"+integerReference.compareAndSet(400, 101,
stamp, stamp + 1));
stamp = integerReference.getStamp();
System.out.println("t1第二次版本:"+stamp);
System.out.println("t1第一次修改成功?:"+integerReference.compareAndSet(101,400,
stamp,stamp+1));
},"t1");
Thread t2 = new Thread(() -> {
int stamp = integerReference.getStamp();
System.out.println("t2第一次版本:"+stamp);
// 睡眠3秒保證t1完成一次ABA操作
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//將100改成400
System.out.println("t2修改成功?:"+integerReference.compareAndSet(400,500,
stamp,stamp+1));
System.out.println("t2:期望版本是:"+stamp+"--當前版本是:"+integerReference.getStamp());
},"t2");
t1.start();
t2.start();
t1.join();
t2.join();
}
}
打印輸出
t1第一次版本:1
t2第一次版本:1
t1第一次修改成功?:false
t1第二次版本:1
t1第一次修改成功?:false
t2修改成功?:false
t2:期望版本是:1--當前版本是:1
???怎么會呢?我賣100本書讓我賣,我賣400本書就不讓我賣了?
看看方法
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair<V> current = pair;
return
// 這一行重點,期望值當前值對比,也就是我們傳的100和400
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
- 上面標注的一行為原因所在,這里需要將兩個Integer對象進行對比,相等則true否則false,那為啥我們傳100就可以400就不行,原因就是Integer的緩存,先看下面的代碼與打印輸出
Integer a =127;
Integer b =127;
System.out.println(a==b); // 輸出true
a=128;
b=128;
System.out.println(a==b);// 輸出false
a=-127;
b=-127;
System.out.println(a==b);// 輸出true
a=-128;
b=-128;
System.out.println(a==b);// 輸出true
a=-129;
b=-129;
System.out.println(a==b);// 輸出false
主要原因就是Integer的緩存范圍為:[-128,127],Integer用==進行值比較,什么時候相等,什么時候不等?
,即只要你的數(shù)值在這個范圍內(nèi)使用上面的方法都沒錯,但是超過了,則每次修改就會失敗
解決方案:使用integer來接收當前的對象,在當前對象的基礎(chǔ)上進行修改,即使用100與400的地方都改成通過integerReference.getReference()直接獲取當前對象,如果直接傳400就會導(dǎo)致Integer裝包時創(chuàng)建新的對象,導(dǎo)致數(shù)值雖然相等,但是對象不相等。
public class AtomicStampedReferenceTest {
static AtomicStampedReference<Integer> integerReference =
new AtomicStampedReference<>(400,1);
public static void main(String[] args) throws Exception {
Thread t1 = new Thread(()->{
int stamp = integerReference.getStamp();
System.out.println("t1第一次版本:"+stamp);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t1第一次修改成功?:"+integerReference.compareAndSet(integerReference.getReference(), 101,
stamp, stamp + 1));
stamp = integerReference.getStamp();
System.out.println("t1第二次版本:"+stamp);
System.out.println("t1第一次修改成功?:"+integerReference.compareAndSet(integerReference.getReference(),400,
stamp,stamp+1));
},"t1");
Thread t2 = new Thread(() -> {
int stamp = integerReference.getStamp();
System.out.println("t2第一次版本:"+stamp);
// 睡眠3秒保證t1完成一次ABA操作
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("t2修改成功?:"+integerReference.compareAndSet(integerReference.getReference(),500,
stamp,stamp+1));
System.out.println("t2:期望版本是:"+stamp+"--當前版本是:"+integerReference.getStamp());
},"t2");
t1.start();
t2.start();
t1.join();
t2.join();
}
}