淺談Java中的atomic包實(shí)現(xiàn)原理及應(yīng)用
1.同步問題的提出
假設(shè)我們使用一個(gè)雙核處理器執(zhí)行A和B兩個(gè)線程,核1執(zhí)行A線程,而核2執(zhí)行B線程,這兩個(gè)線程現(xiàn)在都要對(duì)名為obj的對(duì)象的成員變量i進(jìn)行加1操作,假設(shè)i的初始值為0,理論上兩個(gè)線程運(yùn)行后i的值應(yīng)該變成2,但實(shí)際上很有可能結(jié)果為1。
我們現(xiàn)在來分析原因,這里為了分析的簡(jiǎn)單,我們不考慮緩存的情況,實(shí)際上有緩存會(huì)使結(jié)果為1的可能性增大。A線程將內(nèi)存中的變量i讀取到核1算數(shù)運(yùn)算單元中,然后進(jìn)行加1操作,再將這個(gè)計(jì)算結(jié)果寫回到內(nèi)存中,因?yàn)樯鲜霾僮鞑皇窃硬僮?,只要B線程在A線程將i增加1的值寫回到內(nèi)存之前,讀取了內(nèi)存中i的值(此時(shí)i值為0),那么一定就會(huì)出現(xiàn)i的結(jié)果為1。因?yàn)锳和B線程讀取的i的值都為0,兩個(gè)線程對(duì)它加1后的值都為1,兩個(gè)線程先后將1寫入到變量i中,也就是說i被兩次寫入的值都為1。
最通常的解決方法是兩個(gè)線程中對(duì)i加1的代碼用synchronize關(guān)鍵字對(duì)obj對(duì)象加鎖。今天我們介紹一種新的解決方案,即使用Atomic包中的相關(guān)類來解決。
2.Atomic在硬件上的支持
在單處理器系統(tǒng)(UniProcessor)中,能夠在單條指令中完成的操作都可以認(rèn)為是"原子操作",因?yàn)橹袛嘀荒馨l(fā)生于指令之間(因?yàn)榫€程的調(diào)度需要通過中斷完成)。這也是某些CPU指令系統(tǒng)中引入了test_and_set、test_and_clear等指令用于臨界資源互斥的原因。在對(duì)稱多處理器(SymmetricMulti-Processor)結(jié)構(gòu)中就不同了,由于系統(tǒng)中有多個(gè)處理器在獨(dú)立地運(yùn)行,即使能在單條指令中完成的操作也有可能受到干擾。
在x86平臺(tái)上,CPU提供了在指令執(zhí)行期間對(duì)總線加鎖的手段。CPU芯片上有一條引線#HLOCKpin,如果匯編語言的程序中在一條指令前面加上前綴"LOCK",經(jīng)過匯編以后的機(jī)器代碼就使CPU在執(zhí)行這條指令的時(shí)候把#HLOCKpin的電位拉低,持續(xù)到這條指令結(jié)束時(shí)放開,從而把總線鎖住,這樣同一總線上別的CPU就暫時(shí)不能通過總線訪問內(nèi)存了,保證了這條指令在多處理器環(huán)境中的原子性。當(dāng)然,并不是所有的指令前面都可以加lock前綴的,只有ADD,ADC,AND,BTC,BTR,BTS,CMPXCHG,DEC,INC,NEG,NOT,OR,SBB,SUB,XOR,XADD,和XCHG指令前面可以加"LOCK"指令,實(shí)現(xiàn)原子操作。
Atomic的核心操作就是CAS(compareandset,利用CMPXCHG指令實(shí)現(xiàn),它是一個(gè)原子指令),該指令有三個(gè)操作數(shù),變量的內(nèi)存值V(value的縮寫),變量的當(dāng)前預(yù)期值E(exception的縮寫),變量想要更新的值U(update的縮寫),當(dāng)內(nèi)存值和當(dāng)前預(yù)期值相同時(shí),將變量的更新值覆蓋內(nèi)存值,執(zhí)行偽代碼如下。
if(V == E){ V = U return true }else{ return false }
現(xiàn)在我們就用CAS操作來解決上述問題。B線程將內(nèi)存中的變量i讀取一個(gè)臨時(shí)變量中(假設(shè)此時(shí)讀取的值為0),然后再將i的值讀取到core1的算數(shù)運(yùn)算單元中,接下來進(jìn)行加1操作,比較臨時(shí)變量中的值和i當(dāng)前的值是否相同,如果相同用運(yùn)算單元中的結(jié)果(即i+1)的值覆蓋內(nèi)存中i的值(注意這一部分就是CAS操作,它是個(gè)原子操作,不能被中斷且其它線程中的CAS操作不能同時(shí)執(zhí)行),否則指令執(zhí)行失敗。如果指令失敗,說明A線程已經(jīng)將i的值加1。由此可知如果兩個(gè)線程一開始讀取的i的值為都為0,那么必然只有一個(gè)線程的CAS操作能夠成功,因?yàn)镃AS操作不能并發(fā)執(zhí)行。對(duì)于CAS操作執(zhí)行失敗的線程,只要循環(huán)執(zhí)行CAS操作,那么一定能夠成功??梢钥吹讲]有線程阻塞,這和synchronize的原理有著本質(zhì)的不同。
3.Atomic包簡(jiǎn)介及源碼分析
Atomic包中的類基本的特性就是在多線程環(huán)境下,當(dāng)有多個(gè)線程同時(shí)對(duì)單個(gè)(包括基本類型及引用類型)變量進(jìn)行操作時(shí),具有排他性,即當(dāng)多個(gè)線程同時(shí)對(duì)該變量的值進(jìn)行更新時(shí),僅有一個(gè)線程能成功,而未成功的線程可以向自旋鎖一樣,繼續(xù)嘗試,一直等到執(zhí)行成功。
Atomic系列的類中的核心方法都會(huì)調(diào)用unsafe類中的幾個(gè)本地方法。我們需要先知道一個(gè)東西就是Unsafe類,全名為:sun.misc.Unsafe,這個(gè)類包含了大量的對(duì)C代碼的操作,包括很多直接內(nèi)存分配以及原子操作的調(diào)用,而它之所以標(biāo)記為非安全的,是告訴你這個(gè)里面大量的方法調(diào)用都會(huì)存在安全隱患,需要小心使用,否則會(huì)導(dǎo)致嚴(yán)重的后果,例如在通過unsafe分配內(nèi)存的時(shí)候,如果自己指定某些區(qū)域可能會(huì)導(dǎo)致一些類似C++一樣的指針越界到其他進(jìn)程的問題。
Atomic包中的類按照操作的數(shù)據(jù)類型可以分成4組
AtomicBoolean,AtomicInteger,AtomicLong
線程安全的基本類型的原子性操作
AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
線程安全的數(shù)組類型的原子性操作,它操作的不是整個(gè)數(shù)組,而是數(shù)組中的單個(gè)元素
AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
基于反射原理對(duì)象中的基本類型(長(zhǎng)整型、整型和引用類型)進(jìn)行線程安全的操作
AtomicReference,AtomicMarkableReference,AtomicStampedReference
線程安全的引用類型及防止ABA問題的引用類型的原子操作
我們一般常用的AtomicInteger、AtomicReference和AtomicStampedReference?,F(xiàn)在我們來分析一下Atomic包中AtomicInteger的源代碼,其它類的源代碼在原理上都比較類似。
1.有參構(gòu)造函數(shù)
public AtomicInteger(int initialValue) { value = initialValue; }
從構(gòu)造函數(shù)函數(shù)可以看出,數(shù)值存放在成員變量value中
private volatile int value;
成員變量value聲明為volatile類型,說明了多線程下的可見性,即任何一個(gè)線程的修改,在其它線程中都會(huì)被立刻看到
2.compareAndSet方法(value的值通過內(nèi)部this和valueOffset傳遞)
public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }
這個(gè)方法就是最核心的CAS操作
3.getAndSet方法,在該方法中調(diào)用了compareAndSet方法
public final int getAndSet(int newValue) { for (;;) { int current = get(); if (compareAndSet(current, newValue)) return current; } }
如果在執(zhí)行if(compareAndSet(current,newValue)之前其它線程更改了value的值,那么導(dǎo)致value的值必定和current的值不同,compareAndSet執(zhí)行失敗,只能重新獲取value的值,然后繼續(xù)比較,直到成功。
4.i++的實(shí)現(xiàn)
public final int getAndIncrement() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return current; } }
5. ++i的實(shí)現(xiàn)
public final int incrementAndGet() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return next; } }
4.使用AtomicInteger例子
下面的程序,利用AtomicInteger模擬賣票程序,運(yùn)行結(jié)果中不會(huì)出現(xiàn)兩個(gè)程序賣了同一張票,也不會(huì)賣到票為負(fù)數(shù)
package javaleanning; import java.util.concurrent.atomic.AtomicInteger; public class SellTickets { AtomicInteger tickets = new AtomicInteger(100); class Seller implements Runnable{ @Override public void run() { while(tickets.get() > 0){ int tmp = tickets.get(); if(tickets.compareAndSet(tmp, tmp-1)){ System.out.println(Thread.currentThread().getName()+" "+tmp); } } } } public static void main(String[] args) { SellTickets st = new SellTickets(); new Thread(st.new Seller(), "SellerA").start(); new Thread(st.new Seller(), "SellerB").start(); } }
5.ABA問題
上述的例子運(yùn)行結(jié)果完全正確,這是基于兩個(gè)(或多個(gè))線程都是向同一個(gè)方向?qū)?shù)據(jù)進(jìn)行操作,上面的例子中兩個(gè)線程都是是對(duì)tickets進(jìn)行遞減操作。再比如,多個(gè)線程對(duì)一個(gè)共享隊(duì)列都進(jìn)行對(duì)象的入列操作,那么通過AtomicReference類也可以得到正確的結(jié)果(AQS中維護(hù)的隊(duì)列其實(shí)就是這個(gè)情況),但是多個(gè)線程即可以入列也可以出列,也就是數(shù)據(jù)的操作方向不一致,那么可能出現(xiàn)ABA的情況。
我們現(xiàn)在拿一個(gè)比較好理解的例子來解釋ABA問題,假設(shè)有兩個(gè)線程T1和T2,這兩個(gè)線程對(duì)同一個(gè)棧進(jìn)行出棧和入棧的操作。
我們使用AtomicReference定義的tail來保存棧頂位置
AtomicReference<T> tail;
假設(shè)T1線程準(zhǔn)備出棧,對(duì)于出棧操作我們只需要將棧頂位置由sp通過CAS操作更新為newSP即可,如圖1所示。但是在T1線程執(zhí)行tail.compareAndSet(sp,newSP)之前系統(tǒng)進(jìn)行了線程調(diào)度,T2線程開始執(zhí)行。T2執(zhí)行了三個(gè)操作,A出棧,B出棧,然后又將A入棧。此時(shí)系統(tǒng)又開始調(diào)度,T1線程繼續(xù)執(zhí)行出棧操作,但是在T1線程看來,棧頂元素仍然為A,(即T1仍然認(rèn)為B還是棧頂A的下一個(gè)元素),而實(shí)際上的情況如圖2所示。T1會(huì)認(rèn)為棧沒有發(fā)生變化,所以tail.compareAndSet(sp,newSP)執(zhí)行成功,棧頂指針被指向了B節(jié)點(diǎn)。而實(shí)際上B已經(jīng)不存在于堆棧中,T1將A出棧后的結(jié)果如圖3所示,這顯然不是正確的結(jié)果。
6.ABA問題的解決方法
使用AtomicMarkableReference,AtomicStampedReference。使用上述兩個(gè)Atomic類進(jìn)行操作。他們?cè)趯?shí)現(xiàn)compareAndSet指令的時(shí)候除了要比較當(dāng)對(duì)象的前值和預(yù)期值以外,還要比較當(dāng)前(操作的)戳值和預(yù)期(操作的)戳值,當(dāng)全部相同時(shí),compareAndSet方法才能成功。每次更新成功,戳值都會(huì)發(fā)生變化,戳值的設(shè)置是由編程人員自己控制的。
public Boolean compareAndSet(V expectedReference, V newReference, int expectedStamp,int newStamp) { Pair<V> current = pair; return expectedReference == current.reference && expectedStamp == current.stamp && ((newReference == current.reference && newStamp == current.stamp) || casPair(current, Pair.of(newReference, newStamp))); }
這時(shí)的compareAndSet方法需要四個(gè)參數(shù)expectedReference,newReference,expectedStamp,newStamp,我們?cè)谑褂眠@個(gè)方法時(shí)要保證期望的戳值和要更新戳值不能一樣,通常newStamp=expectedStamp+1
還拿上述的例子
假設(shè)線程T1在彈棧之前:sp指向A,戳值為100。
線程T2執(zhí)行:將A出棧后,sp指向B,戳值變?yōu)?01,
B出棧后,sp指向C,戳值變?yōu)?02,
A入棧后,sp指向A,戳值變?yōu)?03,
線程T1繼續(xù)執(zhí)行compareAndSet語句,發(fā)現(xiàn)sp雖然還是指向A,但是戳值的預(yù)期值100和當(dāng)前值103不同,所以compareAndSet失敗,需要從新獲取newSP的值(此時(shí)newSP就會(huì)指向C),以及戳的預(yù)期值103,然后再次進(jìn)行compareAndSet操作,這樣A成功出棧,sp會(huì)指向C。
注意,由于compareAndSet只能一次改變一個(gè)值,無法同時(shí)改變newReference和newStamp,所以在實(shí)現(xiàn)的時(shí)候,在內(nèi)部定義了一個(gè)類Pair類將newReference和newStamp變成一個(gè)對(duì)象,進(jìn)行CAS操作的時(shí)候,實(shí)際上是對(duì)Pair對(duì)象的操作
private static class Pair<T> { final T reference; final int stamp; private Pair(T reference, int stamp) { this.reference = reference; this.stamp = stamp; } static <T> Pair<T> of(T reference, int stamp) { return new Pair<T>(reference, stamp); } }
對(duì)于AtomicMarkableReference而言,戳值是一個(gè)布爾類型的變量,而AtomicStampedReference中戳值是一個(gè)整型變量。
總結(jié)
以上就是本文關(guān)于淺談Java中的atomic包實(shí)現(xiàn)原理及應(yīng)用的全部?jī)?nèi)容,希望對(duì)大家有所幫助。感興趣的朋友可以繼續(xù)參閱本站其他相關(guān)專題,如有不足之處,歡迎留言指出。
- Java concurrency之AtomicReference原子類_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
- 深入了解Java atomic原子類的使用方法和原理
- Java多線程Atomic包操作原子變量與原子類詳解
- Java多線程atomic包介紹及使用方法
- Java AtomicInteger類的使用方法詳解
- Java concurrency之AtomicLongArray原子類_動(dòng)力節(jié)點(diǎn)Java學(xué)院整理
- Java中對(duì)AtomicInteger和int值在多線程下遞增操作的測(cè)試
- Java AtomicInteger類使用方法實(shí)例講解
- 詳解Java高并發(fā)編程之AtomicReference
相關(guān)文章
Java編寫計(jì)算器的常見方法實(shí)例總結(jié)
這篇文章主要介紹了Java編寫計(jì)算器的常見方法,結(jié)合實(shí)例形式總結(jié)分析了Java實(shí)現(xiàn)計(jì)算器功能的常用方法,需要的朋友可以參考下2016-04-04SpringBoot2.3.0配置JPA的實(shí)現(xiàn)示例
這篇文章主要介紹了SpringBoot2.3.0配置JPA的實(shí)現(xiàn)示例,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-08-08使用java寫的矩陣乘法實(shí)例(Strassen算法)
這篇文章主要給大家介紹了關(guān)于如何使用java寫的矩陣乘法(Strassen算法)的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-02-02使用Jackson實(shí)現(xiàn)Map與Bean互轉(zhuǎn)方式
這篇文章主要介紹了使用Jackson實(shí)現(xiàn)Map與Bean互轉(zhuǎn)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-08-08mybatisplus實(shí)現(xiàn)自動(dòng)創(chuàng)建/更新時(shí)間的項(xiàng)目實(shí)踐
Mybatis-Plus提供了自動(dòng)填充功能,可以通過實(shí)現(xiàn)MetaObjectHandler接口來實(shí)現(xiàn)自動(dòng)更新時(shí)間的功能,本文就來介紹一下mybatisplus實(shí)現(xiàn)自動(dòng)創(chuàng)建/更新時(shí)間的項(xiàng)目實(shí)踐,感興趣的可以了解下2024-01-01Java中List與數(shù)組之間的相互轉(zhuǎn)換
在日常Java學(xué)習(xí)或項(xiàng)目開發(fā)中,經(jīng)常會(huì)遇到需要int[]數(shù)組和List列表相互轉(zhuǎn)換的場(chǎng)景,然而往往一時(shí)難以想到有哪些方法,最后可能會(huì)使用暴力逐個(gè)轉(zhuǎn)換法,往往不是我們所滿意的,下面這篇文章主要給大家介紹了關(guān)于Java中List與數(shù)組之間的相互轉(zhuǎn)換,需要的朋友可以參考下2023-05-05java Apache poi 對(duì)word doc文件進(jìn)行讀寫操作
這篇文章主要介紹了Apache poi 對(duì)word doc文件進(jìn)行讀寫操作的相關(guān)資料,需要的朋友可以參考下2017-01-01