中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

淺談Java中的atomic包實現原理及應用

發布時間:2020-10-06 22:23:02 來源:腳本之家 閱讀:184 作者:nullzx 欄目:編程語言

1.同步問題的提出

假設我們使用一個雙核處理器執行A和B兩個線程,核1執行A線程,而核2執行B線程,這兩個線程現在都要對名為obj的對象的成員變量i進行加1操作,假設i的初始值為0,理論上兩個線程運行后i的值應該變成2,但實際上很有可能結果為1。

我們現在來分析原因,這里為了分析的簡單,我們不考慮緩存的情況,實際上有緩存會使結果為1的可能性增大。A線程將內存中的變量i讀取到核1算數運算單元中,然后進行加1操作,再將這個計算結果寫回到內存中,因為上述操作不是原子操作,只要B線程在A線程將i增加1的值寫回到內存之前,讀取了內存中i的值(此時i值為0),那么一定就會出現i的結果為1。因為A和B線程讀取的i的值都為0,兩個線程對它加1后的值都為1,兩個線程先后將1寫入到變量i中,也就是說i被兩次寫入的值都為1。

最通常的解決方法是兩個線程中對i加1的代碼用synchronize關鍵字對obj對象加鎖。今天我們介紹一種新的解決方案,即使用Atomic包中的相關類來解決。

2.Atomic在硬件上的支持

在單處理器系統(UniProcessor)中,能夠在單條指令中完成的操作都可以認為是"原子操作",因為中斷只能發生于指令之間(因為線程的調度需要通過中斷完成)。這也是某些CPU指令系統中引入了test_and_set、test_and_clear等指令用于臨界資源互斥的原因。在對稱多處理器(SymmetricMulti-Processor)結構中就不同了,由于系統中有多個處理器在獨立地運行,即使能在單條指令中完成的操作也有可能受到干擾。

在x86平臺上,CPU提供了在指令執行期間對總線加鎖的手段。CPU芯片上有一條引線#HLOCKpin,如果匯編語言的程序中在一條指令前面加上前綴"LOCK",經過匯編以后的機器代碼就使CPU在執行這條指令的時候把#HLOCKpin的電位拉低,持續到這條指令結束時放開,從而把總線鎖住,這樣同一總線上別的CPU就暫時不能通過總線訪問內存了,保證了這條指令在多處理器環境中的原子性。當然,并不是所有的指令前面都可以加lock前綴的,只有ADD,ADC,AND,BTC,BTR,BTS,CMPXCHG,DEC,INC,NEG,NOT,OR,SBB,SUB,XOR,XADD,和XCHG指令前面可以加"LOCK"指令,實現原子操作。

Atomic的核心操作就是CAS(compareandset,利用CMPXCHG指令實現,它是一個原子指令),該指令有三個操作數,變量的內存值V(value的縮寫),變量的當前預期值E(exception的縮寫),變量想要更新的值U(update的縮寫),當內存值和當前預期值相同時,將變量的更新值覆蓋內存值,執行偽代碼如下。

if(V == E){ 
  V = U 
  return true 
}else{ 
  return false 
}

現在我們就用CAS操作來解決上述問題。B線程將內存中的變量i讀取一個臨時變量中(假設此時讀取的值為0),然后再將i的值讀取到core1的算數運算單元中,接下來進行加1操作,比較臨時變量中的值和i當前的值是否相同,如果相同用運算單元中的結果(即i+1)的值覆蓋內存中i的值(注意這一部分就是CAS操作,它是個原子操作,不能被中斷且其它線程中的CAS操作不能同時執行),否則指令執行失敗。如果指令失敗,說明A線程已經將i的值加1。由此可知如果兩個線程一開始讀取的i的值為都為0,那么必然只有一個線程的CAS操作能夠成功,因為CAS操作不能并發執行。對于CAS操作執行失敗的線程,只要循環執行CAS操作,那么一定能夠成功。可以看到并沒有線程阻塞,這和synchronize的原理有著本質的不同。

3.Atomic包簡介及源碼分析

Atomic包中的類基本的特性就是在多線程環境下,當有多個線程同時對單個(包括基本類型及引用類型)變量進行操作時,具有排他性,即當多個線程同時對該變量的值進行更新時,僅有一個線程能成功,而未成功的線程可以向自旋鎖一樣,繼續嘗試,一直等到執行成功。

Atomic系列的類中的核心方法都會調用unsafe類中的幾個本地方法。我們需要先知道一個東西就是Unsafe類,全名為:sun.misc.Unsafe,這個類包含了大量的對C代碼的操作,包括很多直接內存分配以及原子操作的調用,而它之所以標記為非安全的,是告訴你這個里面大量的方法調用都會存在安全隱患,需要小心使用,否則會導致嚴重的后果,例如在通過unsafe分配內存的時候,如果自己指定某些區域可能會導致一些類似C++一樣的指針越界到其他進程的問題。

Atomic包中的類按照操作的數據類型可以分成4組

AtomicBoolean,AtomicInteger,AtomicLong

線程安全的基本類型的原子性操作

AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray

線程安全的數組類型的原子性操作,它操作的不是整個數組,而是數組中的單個元素

AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater

基于反射原理對象中的基本類型(長整型、整型和引用類型)進行線程安全的操作

AtomicReference,AtomicMarkableReference,AtomicStampedReference

線程安全的引用類型及防止ABA問題的引用類型的原子操作

我們一般常用的AtomicInteger、AtomicReference和AtomicStampedReference。現在我們來分析一下Atomic包中AtomicInteger的源代碼,其它類的源代碼在原理上都比較類似。

1.有參構造函數

public AtomicInteger(int initialValue) { 
  value = initialValue;
}

從構造函數函數可以看出,數值存放在成員變量value中

private volatile int value;

成員變量value聲明為volatile類型,說明了多線程下的可見性,即任何一個線程的修改,在其它線程中都會被立刻看到

2.compareAndSet方法(value的值通過內部this和valueOffset傳遞)

public final boolean compareAndSet(int expect, int update) {
 return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

這個方法就是最核心的CAS操作

3.getAndSet方法,在該方法中調用了compareAndSet方法

public final int getAndSet(int newValue) {
    for (;;) {
      int current = get();
      if (compareAndSet(current, newValue))
        return current;
    }
}

如果在執行if(compareAndSet(current,newValue)之前其它線程更改了value的值,那么導致value的值必定和current的值不同,compareAndSet執行失敗,只能重新獲取value的值,然后繼續比較,直到成功。

4.i++的實現

public final int getAndIncrement() {
    for (;;) {
      int current = get();
      int next = current + 1;
      if (compareAndSet(current, next))
        return current;
    }
}

5. ++i的實現

public final int incrementAndGet() {
    for (;;) {
      int current = get();
      int next = current + 1;
      if (compareAndSet(current, next))
        return next;
    }
}

4.使用AtomicInteger例子

下面的程序,利用AtomicInteger模擬賣票程序,運行結果中不會出現兩個程序賣了同一張票,也不會賣到票為負數

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問題

上述的例子運行結果完全正確,這是基于兩個(或多個)線程都是向同一個方向對數據進行操作,上面的例子中兩個線程都是是對tickets進行遞減操作。再比如,多個線程對一個共享隊列都進行對象的入列操作,那么通過AtomicReference類也可以得到正確的結果(AQS中維護的隊列其實就是這個情況),但是多個線程即可以入列也可以出列,也就是數據的操作方向不一致,那么可能出現ABA的情況。

我們現在拿一個比較好理解的例子來解釋ABA問題,假設有兩個線程T1和T2,這兩個線程對同一個棧進行出棧和入棧的操作。

我們使用AtomicReference定義的tail來保存棧頂位置

AtomicReference<T> tail;

淺談Java中的atomic包實現原理及應用

假設T1線程準備出棧,對于出棧操作我們只需要將棧頂位置由sp通過CAS操作更新為newSP即可,如圖1所示。但是在T1線程執行tail.compareAndSet(sp,newSP)之前系統進行了線程調度,T2線程開始執行。T2執行了三個操作,A出棧,B出棧,然后又將A入棧。此時系統又開始調度,T1線程繼續執行出棧操作,但是在T1線程看來,棧頂元素仍然為A,(即T1仍然認為B還是棧頂A的下一個元素),而實際上的情況如圖2所示。T1會認為棧沒有發生變化,所以tail.compareAndSet(sp,newSP)執行成功,棧頂指針被指向了B節點。而實際上B已經不存在于堆棧中,T1將A出棧后的結果如圖3所示,這顯然不是正確的結果。

6.ABA問題的解決方法

使用AtomicMarkableReference,AtomicStampedReference。使用上述兩個Atomic類進行操作。他們在實現compareAndSet指令的時候除了要比較當對象的前值和預期值以外,還要比較當前(操作的)戳值和預期(操作的)戳值,當全部相同時,compareAndSet方法才能成功。每次更新成功,戳值都會發生變化,戳值的設置是由編程人員自己控制的。

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)));
}

這時的compareAndSet方法需要四個參數expectedReference,newReference,expectedStamp,newStamp,我們在使用這個方法時要保證期望的戳值和要更新戳值不能一樣,通常newStamp=expectedStamp+1

還拿上述的例子

假設線程T1在彈棧之前:sp指向A,戳值為100。

線程T2執行:將A出棧后,sp指向B,戳值變為101,

B出棧后,sp指向C,戳值變為102,

A入棧后,sp指向A,戳值變為103,

線程T1繼續執行compareAndSet語句,發現sp雖然還是指向A,但是戳值的預期值100和當前值103不同,所以compareAndSet失敗,需要從新獲取newSP的值(此時newSP就會指向C),以及戳的預期值103,然后再次進行compareAndSet操作,這樣A成功出棧,sp會指向C。

注意,由于compareAndSet只能一次改變一個值,無法同時改變newReference和newStamp,所以在實現的時候,在內部定義了一個類Pair類將newReference和newStamp變成一個對象,進行CAS操作的時候,實際上是對Pair對象的操作

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);
  }
}

對于AtomicMarkableReference而言,戳值是一個布爾類型的變量,而AtomicStampedReference中戳值是一個整型變量。

總結

以上就是本文關于淺談Java中的atomic包實現原理及應用的全部內容,希望對大家有所幫助。感興趣的朋友可以繼續參閱本站其他相關專題,如有不足之處,歡迎留言指出。

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

碌曲县| 香格里拉县| 达州市| 元谋县| 互助| 沁源县| 南皮县| 康马县| 临武县| 汝城县| 岱山县| 三河市| 黄陵县| 缙云县| 台东市| 蕲春县| 尤溪县| 堆龙德庆县| 沂南县| 高碑店市| 桃园市| 获嘉县| 辽宁省| 九江市| 徐水县| 襄垣县| 北辰区| 江西省| 肥城市| 长葛市| 唐河县| 梅河口市| 安陆市| 玉山县| 兰溪市| 石楼县| 兴义市| 开封县| 阜平县| 莱芜市| 呼伦贝尔市|