什么是原子操作
原子的意思是說“不能被進(jìn)一步分割的粒子”待侵,而原子操作是說“不可被終端的一個(gè)或多個(gè)系列的操作”丢早。假定有兩個(gè)操作A和B,如果從執(zhí)行A的線程來看,當(dāng)另一個(gè)線程執(zhí)行B時(shí)怨酝,要么將B全部執(zhí)行完傀缩,要么完全不執(zhí)行B,那么A和B對彼此來說是原子的农猬。
java中可以通過鎖赡艰,鎖機(jī)制的方式來實(shí)現(xiàn)原子操作,但是有時(shí)候需要更有效靈活的機(jī)制斤葱,synchronized關(guān)鍵字是基于阻塞的鎖機(jī)制瞄摊,也就是說當(dāng)一個(gè)線程擁有鎖的時(shí)候,訪問同一資源的其它線程需要等待苦掘,直到該線程釋放鎖换帜,因?yàn)閟ynchronized關(guān)鍵字具有排他性,如果有大量的線程來競爭資源鹤啡,那CPU將會花費(fèi)大量的時(shí)間和資源來處理這些競爭惯驼,同時(shí)也會造成死鎖的情況。而且鎖的機(jī)制相當(dāng)于其他輕量級的需求有點(diǎn)過于笨重递瑰,例如計(jì)數(shù)器祟牲,這個(gè)后邊我會介紹兩者之間的性能的比較。
如何實(shí)現(xiàn)原子操作
實(shí)現(xiàn)原子操作還可以使用CAS實(shí)現(xiàn)原子操作抖部,利用了處理器提供的CMPXCHG指令來實(shí)現(xiàn)的说贝,每一個(gè)CAS操作過程都包含三個(gè)運(yùn)算符:一個(gè)內(nèi)存地址V,一個(gè)期望的值A(chǔ)和一個(gè)新值B慎颗,操作的時(shí)候如果這個(gè)地址上存放的值等于這個(gè)期望的值A(chǔ)乡恕,則將地址上的值賦為新值B,否則不做任何操作俯萎。
CAS的基本思路就是傲宜,如果這個(gè)地址上的值和期望的值相等,則給其賦予新值夫啊,否則不做任何事函卒,但是要返回原值是多少。循環(huán)CAS就是在一個(gè)循環(huán)里不斷的做cas操作撇眯,直到成功為止报嵌。下面的代碼實(shí)現(xiàn)了一個(gè)CAS線程安全的計(jì)數(shù)器safeCount。
public class Counter {
private AtomicInteger atomicCount = new AtomicInteger(0);
private int i = 0;
/** cas cafecount **/
private void safeCount() {
for (; ; ) {
int i = atomicCount.get();
boolean suc = atomicCount.compareAndSet(i, ++i);
if (suc) {
break;
}
}
}
public static void main(String[] args) {
Counter cas = new Counter();
List<Thread> ts = new ArrayList<>(500);
long start = System.currentTimeMillis();
for (int j = 0; j < 100; j++) {
Thread t = new Thread(() -> {
for (int i = 0; i < 10000; i++) {
cas.safeCount();
}
});
ts.add(t);
}
for (Thread t : ts) {
t.start();
}
for (Thread t : ts) {
try {
t.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(cas.i);
System.out.println(cas.atomicCount.get());
System.out.println(System.currentTimeMillis() - start);
}
}
CAS是怎么實(shí)現(xiàn)線程的安全呢熊榛?語言層面不做處理锚国,我們將其交給硬件—CPU和內(nèi)存,利用CPU的多處理能力来候,實(shí)現(xiàn)硬件層面的阻塞跷叉,再加上volatile變量的特性(可見性逸雹,有序性)即可實(shí)現(xiàn)基于原子操作的線程安全营搅。
CAS實(shí)現(xiàn)原子性操作的三大問題
在Java并發(fā)包中有一些并發(fā)框架也使用了自旋CAS的方式來實(shí)現(xiàn)原子操作云挟,比如LinkedTransferQueue類的xfer方法。CAS雖然很高的解決了原子操作转质,但是CAS仍然存在三大問題园欣。ABA問題、循環(huán)時(shí)間長開銷大休蟹、以及只能保證一個(gè)共享變量的原子操作沸枯。
ABA問題
因?yàn)镃AS需要在操作值的時(shí)候,檢查值有沒有發(fā)生變化赂弓,如果發(fā)生變化則更新绑榴,但是如果一個(gè)值為A,變成了B盈魁,又變成了A翔怎,那么使用CAS進(jìn)行檢查時(shí)就會發(fā)現(xiàn)它的值沒有發(fā)生變化,但實(shí)際上發(fā)生變化了杨耙。ABA問題的解決思路就是使用版本號赤套,在變量前邊追加版本號,每次變量更新的時(shí)候把版本號加1珊膜,那么A→B→A就會變成1A→2B→3A容握。舉個(gè)通俗點(diǎn)的例子,你倒了一杯水放桌子上车柠,干了點(diǎn)別的事剔氏,然后同事把你水喝了又給你重新倒了一杯水,你回來看水還在竹祷,拿起來就喝介蛉,如果你不管水中間被人喝過,只關(guān)心水還在溶褪,這就是ABA問題币旧。
從java1.5開始,JDK提供了AtomicStampedReference猿妈、AtomicMarkableReference來解決ABA的問題吹菱,通過compareAndSet方法檢查值是否發(fā)生變化以外檢查版本號知否發(fā)生變化。(AtomicStampedReference能夠得到變化的次數(shù)這里下邊會介紹到)
循環(huán)時(shí)間長開銷大
自旋CAS如果長時(shí)間不成功彭则,會給CPU帶來非常大的執(zhí)行開銷鳍刷。
只能保證一個(gè)共享變量的原子操作
當(dāng)對一個(gè)共享變量執(zhí)行操作時(shí),我們可以使用循環(huán)CAS的方式來保證原子操作俯抖,但是對多個(gè)共享變量操作時(shí)输瓜,循環(huán)CAS就無法保證操作的原子性,這個(gè)時(shí)候就可以用鎖。還有一個(gè)取巧的辦法尤揣,就是把多個(gè)共享變量合并成一個(gè)共享變量來操作搔啊。比如,有兩個(gè)共享變量i=2北戏,j=a负芋,合并一下ij=2a,然后用CAS來操作ij嗜愈。從Java 1.5開始旧蛾,JDK提供了AtomicReference類來保證引用對象之間的原子性,就可以把多個(gè)變量放在一個(gè)對象里來進(jìn)行CAS操作蠕嫁。
Jdk中相關(guān)原子操作類的使用
從1.5開始锨天,JDK的并發(fā)包里提供了一些類來支持原子操作,如AtomicBoolean剃毒、AtomicInter绍绘。這些原子包裝類還提供了簡單、性能高效迟赃、線程安全有用的工具方法陪拘,并且
在并發(fā)代碼來說是非常關(guān)鍵的。原子變量將發(fā)生在單個(gè)的變量上纤壁,粒度最細(xì)的情況左刽。原子變量類有很多種,所以Atomic包里一共提供了13個(gè)類酌媒,屬于4種類型的原子更新方式欠痴,分別是原子更新基本類型、原子更新數(shù)組秒咨、原子更新引用和原子更新屬性喇辽。Atomic包里的類基本都是通過Unsafe實(shí)現(xiàn)包裝類。
原子更新基本類型
使用原子方式更新基本類型雨席,Atomic包提供了以下3個(gè)類菩咨。
AtomicBoolean:原子更新布爾值類型
AtomicInteger:原子更新整型
AtomicLong:原子更新長整形
以AtomicInteger為例:
public final int addAndGet(int delta)
以原子方式將輸入的數(shù)值與實(shí)例的值相加,并返回結(jié)果陡厘。
public final boolean compareAndSet(int expect, int update)
如果輸入的數(shù)值等于預(yù)期值抽米,則以原子的方式設(shè)置輸入的值。
public final int getAndIncrement()
以原子方式將當(dāng)前值加1糙置,注意云茸,這里返回的是自增前的值。
public final int getAndSet(int newValue)
以原子方式設(shè)置為newValue的值谤饭,并返回舊值标捺。
接下來我們看一下getAndIncrement源碼
public final int getAndIncrement()
以原子方式將當(dāng)前值設(shè)置為1懊纳,這里返回的是自增的值。
在JDK1.8中g(shù)etAndIncrement是如何實(shí)現(xiàn)原子操作的呢亡容?我們分析一下源碼嗤疯,
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
getAndIncrement方法調(diào)用了首先Unsafe的getAndAddInt方法,獲取當(dāng)前數(shù)值萍倡,然后循環(huán)compareAndSwapObject驗(yàn)證v和delta是否相等如果不相等返回v身弊,這里意味著A tmoicInteger值是否修改過辟汰。
接下來我們看Unsafe類
//更新變量值為x列敲,如果當(dāng)前值為expected
//o:對象 offset:偏移量 expected:期望值 x:新值
public final native boolean compareAndSwapObject(Object o,long offset,Object expected,Object x);
public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);
public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x);
通過代碼,我們發(fā)現(xiàn)Unsafe只提供了3種CAS方法:compareAndSwapObject帖汞、compareAndSwapInt和compareAndSwapLong戴而,再看AtomicBoolean源碼,發(fā)現(xiàn)它是先把Boolean轉(zhuǎn)換成整
型翩蘸,再使用compareAndSwapInt進(jìn)行CAS所意,所以原子更新char、float和double變量也可以用類似
的思路來實(shí)現(xiàn)催首。
原子更新數(shù)組
通過原子的方式更新數(shù)組里的某個(gè)元素扶踊,Atomic包提供了以下四個(gè)類。
AtomicIntegerArray:原子更新整型數(shù)組里的元素郎任。
AtomicLongArray:原子更新長征信數(shù)組里的元素秧耗。
AtomicReferenceArray:原子更新引用類型數(shù)組里的元素。
以AtomicIntegerArray為例舶治,AtomicIntegerArray的使用實(shí)例代碼如下
public class AtomicIntegerArrayTest {
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]);
}
}
輸出的結(jié)果是3和1霉猛,這里要注意的是數(shù)組value通過構(gòu)造方法傳遞進(jìn)去尺锚,然后AtomicIntegerArray會將當(dāng)前數(shù)組
復(fù)制一份,所以當(dāng)AtomicIntegerArray對內(nèi)部的數(shù)組元素進(jìn)行修改時(shí)惜浅,不會影響傳入的數(shù)組瘫辩。
原子更新引用類型
原子更新基本類型的AtomicInteger,只能更新一個(gè)變量坛悉,則需要使用這個(gè)原子引用類型提供的類杭朱。
AtomicReference:原子更新引用類型。
AtomicReferenceFieldUpdater:原子更新引用類型里的字段吹散。
AtomicMarkableReference:原子更新帶有標(biāo)記的引用類型弧械,可以以原子更新一個(gè)布爾類型的標(biāo)志位和引用類型。
以AtomicReference為例空民,AtomicReference的使用示例代碼如下
public class AtomicReferenceTest {
public static AtomicReference<user> atomicUserRef = new
AtomicReference<user>();
public static void main(String[] args) {
User user = new User("tim"刃唐, 15);
atomicUserRef.set(user);
User updateUser = new User("jack"羞迷, 17);
atomicUserRef.compareAndSet(user, updateUser);
System.out.println(atomicUserRef.get().getName());
System.out.println(atomicUserRef.get().getOld());
}
}
代碼中首先構(gòu)建一個(gè)user對象画饥,然后把user對象設(shè)置進(jìn)AtomicReferenc中衔瓮,最后調(diào)用
compareAndSet方法進(jìn)行原子更新操作,實(shí)現(xiàn)原理同AtomicInteger里的compareAndSet方法抖甘。
原子更新字段類
如果效原子更新某個(gè)類的字段需要使用更新字段類
AtomicIntegerFieldUpdater:原子更新整數(shù)型字段更新器
AtomicLongFieldUpdater:原子更新長整數(shù)字段更新器
AtomicStampedReference:原子更新帶有版本號的引用類型热鞍。
要想原子地更新字段類需要兩步。第一步衔彻,因?yàn)樵痈伦侄晤惗际浅橄箢愞背瑁看问褂玫?br>
時(shí)候必須使用靜態(tài)方法newUpdater()創(chuàng)建一個(gè)更新器,并且需要設(shè)置想要更新的類和屬性艰额。第
二步澄港,更新類的字段(屬性)必須使用public volatile修飾符。
public class AtomicIntegerFieldUpdaterTest {
// 創(chuàng)建原子更新器柄沮,并設(shè)置需要更新的對象類和對象的屬性
private static AtomicIntegerFieldUpdater<User> a = AtomicIntegerFieldUpdater.newUpdater(User.class回梧, "old");
public static void main(String[] args) {
// 設(shè)置tim的年齡是10歲
User conan = new User("tim", 10);
// tim長了一歲祖搓,但是仍然會輸出舊的年齡
System.out.println(a.getAndIncrement(conan));
// 輸出tim現(xiàn)在的年齡
System.out.println(a.get(conan));
}
public static class User {
private String name;
public volatile int old;
public User(String name狱意, int old) {
this.name = name;
this.old = old;
}
public String getName() {
return name;
}
public int getOld() {
return old;
}
}
}
鎖與原子變量性能比較
圖中給出了工作量較低以及適中的情況下的吞吐量。如果線程計(jì)算量少拯欧,那么在鎖和原子變量上競爭會非常激烈详囤。如果線程本地計(jì)算量多,那么鎖和原子變量競爭就會降低哈扮。從圖中可以看出纬纪,在高度競爭的狀態(tài)下,鎖的性能將超過原子變量的性能滑肉,但在真實(shí)情況下包各,原子變量的性能將超過鎖的性能。比如說在交通擁堵的時(shí)候靶庙,紅路燈交通信號可以能讓更多的車通行问畅,在道路不擁堵時(shí),環(huán)形交通則有更多的車輛六荒。
本文由本人編寫總結(jié)护姆,版權(quán)由享學(xué)課堂所有