一罪佳、什么是原子操作逛漫?如何實現(xiàn)原子操作?
CAS:Compare And Swap,比較并且交換赘艳。隸屬于樂觀鎖機制酌毡。
什么是原子操作?
假設(shè)現(xiàn)在有A蕾管,B兩個操作枷踏,如果某個線程執(zhí)行A操作,當另外一個線程執(zhí)行B操作的時候掰曾,要么這個B全部執(zhí)行完旭蠕,要么這個B完全不執(zhí)行,那么對于A、B來講掏熬,他們彼此就是原子的佑稠。
在數(shù)據(jù)庫層面,這種操作就是事務(wù)操作旗芬,嚴格意義上來說事務(wù)操作也是屬于原子操作的一種舌胶。
如何實現(xiàn)原子操作
可以利用synchronize關(guān)鍵字,但是會引發(fā)一系列問題:
- 1.synchronize是阻塞式的疮丛,一個線程擁有鎖后幔嫂,其他的線程必須等待
- 2.等待中的線程優(yōu)先級很高,但是遲遲拿不到鎖怎么辦这刷?
- 3.等待中的線程競爭很激烈,但是拿到鎖的線程遲遲不釋放鎖怎么辦娩井?
解決辦法CAS
CAS可以完美地解決上述的問題暇屋,進而更完美地實現(xiàn)原子操作,它利用了現(xiàn)代處理器都支持的CAS指令洞辣,這個指令是CPU級別的指令咐刨。
CAS包含的要素
1.內(nèi)存地址v:修改的對象或者變量的內(nèi)存地址
2.期望值A(chǔ):
3.新值B
當我去改這個內(nèi)存地址上所對應(yīng)的對象或者變量的時候,我期望在我改的時候扬霜,這個值是多少定鸟,如果是A,我就把他改成B著瓶,如果不是A联予,那我就不能改。將B值替換為A值材原。
即比較---->交換
沸久。
用java語言來講,這個操作需要兩個語句余蟹,一個是比較卷胯,一個是交換。
而在CPU層面威酒,只要你執(zhí)行了這個指令窑睁,我可以保證別的指令都被阻塞,只有這一個CAS指令操作完了才允許別的指令進行操作葵孤。
在JDK層面來講担钮,用到了循環(huán)(自旋、死循環(huán))尤仍,直到成功為止裳朋,原理如下:
這種思想就是樂觀鎖。
用一句話來概括CAS如何實現(xiàn)線程安全?
CAS在語言層面不作處理鲤嫡,我們把它交給了CPU和內(nèi)存送挑,利用CPU的能力實現(xiàn)硬件層面阻塞,進而實現(xiàn)CAS的線程安全暖眼。
二惕耕、CAS引起的問題
1.ABA問題
下面的兩種情況下會出現(xiàn)ABA問題。
1.A最開始的內(nèi)存地址是X诫肠,然后失效了司澎,又分配了B,恰好內(nèi)存地址是X栋豫,這時候通過CAS操作挤安,卻設(shè)置成功了
這種情況在帶有GC的語言中,這種情況是不可能發(fā)生的丧鸯,為什么呢蛤铜?拿JAVA舉例,在執(zhí)行CAS操作時丛肢,A围肥,B對象肯定生命周期內(nèi),GC不可能將其釋放蜂怎,那么A指向的內(nèi)存是不會被釋放的穆刻,B也就不可能分配到與A相同的內(nèi)存地址,CAS失敗杠步。若在無GC的氢伟,A對象已經(jīng)被釋放了,那么B被分配了A的內(nèi)存幽歼,CAS成功腐芍。
2.線程1準備用CAS將變量的值由A替換為B,在此之前试躏,線程2將變量的值由A替換為C猪勇,又由C替換為A,然后線程1執(zhí)行CAS時發(fā)現(xiàn)變量的值仍然為A颠蕴,所以CAS成功泣刹。但實際上這時的現(xiàn)場已經(jīng)和最初不同了,盡管CAS成功犀被,但可能存在潛藏的問題椅您。比如:
現(xiàn)有一個用單向鏈表實現(xiàn)的堆棧,棧頂為A寡键,這時線程T1已經(jīng)知道A.next為B掀泳,然后希望用CAS將棧頂替換為B:head.compareAndSet(A,B);在T1執(zhí)行上面這條指令之前,線程T2介入,將A员舵、B出棧脑沿,再pushD、C马僻、A庄拇。而對象B此時處于游離狀態(tài):此時輪到線程T1執(zhí)行CAS操作,檢測發(fā)現(xiàn)棧頂仍為A韭邓,所以CAS成功措近,棧頂變?yōu)锽,但實際上B.next為null女淑,其中堆棧中只有B一個元素瞭郑,C和D組成的鏈表不再存在于堆棧中,平白無故就把C鸭你、D丟掉了屈张。
以上就是由于ABA問題帶來的隱患,各種樂觀鎖的實現(xiàn)中通常都會用版本戳version來對記錄或?qū)ο髽擞浳荆苊獠l(fā)操作帶來的問題袜茧,在Java中菜拓,AtomicStampedReference<E>也實現(xiàn)了這個作用瓣窄,它通過包裝[E,Integer]的元組來對對象標記版本戳stamp,從而避免ABA問題纳鼎,例如下面的代碼分別用AtomicInteger和AtomicStampedReference來對初始值為100的原子整型變量進行更新俺夕,AtomicInteger會成功執(zhí)行CAS操作,而加上版本戳的AtomicStampedReference對于ABA問題會執(zhí)行CAS失敗贱鄙。
package concur.lock;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABA {
private static AtomicInteger atomicInt = new AtomicInteger(100);
private static AtomicStampedReference<Integer> atomicStampedRef =
new AtomicStampedReference<Integer>(100, 0);
public static void main(String[] args) throws InterruptedException {
Thread intT1 = new Thread(new Runnable() {
@Override
public void run() {
atomicInt.compareAndSet(100, 101);
atomicInt.compareAndSet(101, 100);
}
});
Thread intT2 = new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
boolean c3 = atomicInt.compareAndSet(100, 101);
System.out.println(c3); //true
}
});
intT1.start();
intT2.start();
intT1.join();
intT2.join();
Thread refT1 = new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
atomicStampedRef.compareAndSet(100, 101,
atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1);
atomicStampedRef.compareAndSet(101, 100,
atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1);
}
});
Thread refT2 = new Thread(new Runnable() {
@Override
public void run() {
int stamp = atomicStampedRef.getStamp();
System.out.println("before sleep : stamp = " + stamp); // stamp = 0
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("after sleep : stamp = " + atomicStampedRef.getStamp());//stamp = 1
boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp+1);
System.out.println(c3); //false
}
});
refT1.start();
refT2.start();
}
}
如何解決劝贸?
增加版本號,也就是說在每個變量前面都要加一個版本號逗宁,每次修改的時候都對其版本+1映九。其實在大多數(shù)開發(fā)過程中,我們是不關(guān)心ABA問題的瞎颗。但是ABA問題在一線互聯(lián)網(wǎng)公司的面試中是經(jīng)常問到的件甥。
- 1.ABA問題的解決思路是使用版本號,每次變量更新的時候版本號加1哼拔,那么A->B->A就會變成1A->2B->3A
- 2.從jdk1.5開始引有,jdk的Atomic包里就提供了兩個類來解決ABA問題,一個是
AtomicStampedReference
,另一個是AtomicMarkableReference
倦逐,AtomicStampedReference這個類中的compareAndSet方法的作用就是首先檢查當前引用是否等于預期引用譬正,并且檢查當前標志是否等于預期標志,如果全部相等,則以原子方式將該引用和該標志的值更新為指定的新值曾我。
AtomicStampedReference
和AtomicMarkableReference
的區(qū)別
AtomicStampedReference帶了版本號粉怕,關(guān)心被修改過幾次,AtomicMarkableReference只關(guān)心有沒有人修改過您单。
2.開銷問題
自旋CAS如果長時間不成功斋荞,會給CPU帶來非常大的執(zhí)行開銷。如果jvm能支持處理器提供的pause指令虐秦,那么效率會有一定的提升平酿。pause指令有兩個作用:
第一,它可以延遲流水線執(zhí)行指令(de-pipeline)悦陋,使CPU不會消耗過多的執(zhí)行資源蜈彼,延遲的時間取決于具體實現(xiàn)的版本,在一些處理器上延遲時間是零俺驶。
第二幸逆,它可以避免在退出循環(huán)的時候因內(nèi)存順序沖突(Memory Order Violation)而引起CPU流水線被清空(CPU Pipeline Flush),從而提高CPU的執(zhí)行效率暮现。
3.只能保證一個變量的原子操作
當對一個共享變量執(zhí)行操作時还绘,我們可以使用循環(huán)CAS的方式來保證原子操作,但是多個共享變量操作時栖袋,循環(huán)CAS就無法保證操作的原子性拍顷,這個時候就可以用鎖。還有一個方法塘幅,就是把多個共享變量合并成一個共享變量來操作昔案。比如,有兩個共享變量i=2,j=a合并一下ij=2a电媳,然后用CAS來操作ij踏揣。從java1.5開始,JDK提供了AtomicReference
類來保證引用對象之間的原子性匾乓,就可以把多個變量放在一個對象里來進行CAS操作捞稿。
三、原子操作類的使用
jdk中相關(guān)原子操作類的使用
- 更新基本類型類:AtomicBoolean,AtomicInteger拼缝,AtomicLong
- 更新數(shù)組類:AtomicIntegerArray娱局,AtomicLongArray,AtomicReferenceArray
- 更新引用類:AtomicReference珍促,AtomicMarkableReference铃辖,AtomicStampeReference
- 原子更新字段類:AtomicReferenceFiledUpdater,AtomicIntegerFiledUpdater,AtomicLongFiledUpdater
舉例:
import java.util.concurrent.atomic.AtomicInteger;
/**
*類說明:演示基本類型的原子操作類
*/
public class UseAtomicInt {
static AtomicInteger ai = new AtomicInteger(10);
public static void main(String[] args) {
//返回的是我自增以前的值
int i = ai.getAndIncrement(); // i++
//返回自增以后的值
int b = ai.incrementAndGet();// ++i
System.out.println(i +"------"+ b);
//ai.compareAndSet();
int fianl = ai.addAndGet(24);
System.out.println("加了24之后的值為:"+fianl);
}
}
運行結(jié)果:
import java.util.concurrent.atomic.AtomicIntegerArray;
/**
*類說明: 演示原子操作數(shù)組
*/
public class AtomicArray {
static int[] value = new int[] { 1, 2 };
static AtomicIntegerArray ai = new AtomicIntegerArray(value);
public static void main(String[] args) {
ai.getAndSet(0, 3);
System.out.println(ai.get(0));
System.out.println(value[0]);//原數(shù)組不會變化
}
}
運行結(jié)果:
注意:
原子操作只會操作原子類的值,不會操作原數(shù)組猪叙,原子操作類的值再怎么變也不會影響原數(shù)組的值
運用原子操作類修改兩個變量的值
import java.util.concurrent.atomic.AtomicReference;
/**
*類說明:演示引用類型的原子操作類
*/
public class UseAtomicReference {
static AtomicReference<UserInfo> atomicUserRef;
public static void main(String[] args) {
UserInfo user = new UserInfo("Mark", 15);//要修改的實體的實例
atomicUserRef = new AtomicReference(user);
UserInfo updateUser = new UserInfo("Bill",17);
atomicUserRef.compareAndSet(user,updateUser);
System.out.println(atomicUserRef.get());
System.out.println(user);
}
//定義一個實體類
static class UserInfo {
private volatile String name;
private int age;
public UserInfo(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
@Override
public String toString() {
return "UserInfo{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
}
運行結(jié)果:
這是運用AtomicReference修改兩個變量的值娇斩,本質(zhì)上是包裝成一個變量仁卷,對這一個變量進行修改。