1. 程式人生 > 其它 >7.java中的13個原子操作類

7.java中的13個原子操作類

當程式更新一個變數時,如果多執行緒同時更新這個變數,可能得到期望之外的值,比如變數i=1,A執行緒更新i+1,B執行緒也更新i+1,經過兩個執行緒操作之後可能i不等於3,而是等於2。因為A和B執行緒在更新變數i的時候拿到的i都是1,這就是執行緒不安全的更新操作,通常我們會使用synchronized來解決這個問題,synchronized會保證多執行緒不會同時更新變數i。

而Java從JDK 1.5開始提供了java.util.concurrent.atomic包(以下簡稱Atomic包),這個包中的原子操作類提供了一種用法簡單、效能高效、執行緒安全地更新一個變數的方式

因為變數的型別有很多種,所以在Atomic包裡一共提供了13個類,屬於4種類型的原子更新方式,分別是原子更新基本型別、原子更新陣列、原子更新引用、原子更新屬性(欄位)

Atomic包裡的類基本都是使用Unsafe實現的包裝類。

一、原子更新基本型別類

使用原子的方式更新基本型別,Atomic包提供了以下3個類。

  • AtomicBoolean:原子更新布林型別。

  • AtomicInteger:原子更新整型

  • AtomicLong:原子更新長整型。

以上3個類提供的方法幾乎一模一樣,所以本節僅以AtomicInteger為例進行講解,AtomicInteger的常用方法如下。

  • int addAndGet(int delta):以原子方式將輸入的數值與例項中的值(AtomicInteger裡的value)相加,並返回結果

  • boolean compareAndSet(int expect,int update):如果輸入的數值等於預期值,則以原子方式將該值設定為輸入的值

  • int getAndIncrement():以原子方式將當前值加1,注意,這裡返回的是自增前的值

  • void lazySet(int newValue):最終會設定成newValue,使用lazySet設定值後,可能導致其他執行緒在之後的一小段時間內還是可以讀到舊的值。關於該方法的更多資訊可以參考併發程式設計網翻譯的一篇文章《AtomicLong.lazySet是如何工作的?》,文章地址是“http://ifeve.com/how-does-atomiclong-lazyset-work/”。

  • int getAndSet(int newValue):以原子方式設定為newValue的值,並返回舊值

程式碼清單7-1 AtomicIntegerTest.java

import java.util.concurrent.atomic.AtomicInteger;

	public class AtomicIntegerTest {

		static AtomicInteger ai = new AtomicInteger(1);

		public static void main(String[] args) {

		System.out.println(ai.getAndIncrement());

		System.out.println(ai.get());

	}

}

輸出結果如下。

1
2

那麼getAndIncrement是如何實現原子操作的呢?讓我們一起分析其實現原理,getAndIncrement的原始碼如程式碼清單7-2所示。

程式碼清單7-2 AtomicInteger.java

public final int getAndIncrement () {  
    for (; ; ) {  
        int current = get();  
        int next = current + 1;  
        if (compareAndSet(current, next)) return current;  
    }  
}  
public final boolean compareAndSet ( int expect, int update){  
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);  
}

原始碼中for迴圈體的第一步先取得AtomicInteger裡儲存的數值,第二步對AtomicInteger的當前數值進行加1操作,關鍵的第三步呼叫compareAndSet方法來進行原子更新操作,該方法先檢查當前數值是否等於current,等於意味著AtomicInteger的值沒有被其他執行緒修改過,則將AtomicInteger的當前數值更新成next的值如果不等compareAndSet方法會返回false,程式會進入for迴圈重新進行compareAndSet操作

Atomic包提供了3種基本型別的原子更新,但是Java的基本型別裡還有char、float和double等。那麼問題來了,如何原子的更新其他的基本型別呢?Atomic包裡的類基本都是使用Unsafe實現的,讓我們一起看一下Unsafe的原始碼,如程式碼清單7-3所示。

程式碼清單7-3 Unsafe.java

/**  
 * 如果當前數值是expected,
 * 則原子的將Java變數更新成x * 
 * @return 如果更新成功則返回true   
 */  
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);

通過程式碼,我們發現Unsafe只提供了3種CAS方法:compareAndSwapObject、compareAndSwapInt和compareAndSwapLong,再看AtomicBoolean原始碼,發現它是先把Boolean轉換成整型,再使用compareAndSwapInt進行CAS,所以原子更新char、float和double變數也可以用類似的思路來實現。

二、原子更新陣列

通過原子的方式更新數組裡的某個元素,Atomic包提供了以下4個類。

  • AtomicIntegerArray:原子更新整型數組裡的元素

  • AtomicLongArray:原子更新長整型數組裡的元素。

  • AtomicReferenceArray:原子更新引用型別數組裡的元素。

AtomicIntegerArray類主要是提供原子的方式更新數組裡的整型,其常用方法如下。

  • int addAndGet(int i,int delta):以原子方式將輸入值與陣列中索引i的元素相加

  • boolean compareAndSet(int i,int expect,int update):如果當前值等於預期值,則以原子方式將陣列位置i的元素設定成update值

以上幾個類提供的方法幾乎一樣,所以本節僅以AtomicIntegerArray為例進行講解,AtomicIntegerArray的使用例項程式碼如程式碼清單7-4所示。

程式碼清單7-4 AtomicIntegerArrayTest.java

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

以下是輸出的結果。

3
1

需要注意的是,陣列value通過構造方法傳遞進去,然後AtomicIntegerArray會將當前陣列複製一份,所以當AtomicIntegerArray對內部的陣列元素進行修改時,不會影響傳入的陣列

三、原子更新引用型別

原子更新基本型別的AtomicInteger,只能更新一個變數,如果要原子更新多個變數,就需要使用這個原子更新引用型別提供的類。Atomic包提供了以下3個類。

  • AtomicReference:原子更新引用型別

  • AtomicReferenceFieldUpdater:原子更新引用型別裡的欄位。

  • AtomicMarkableReference:原子更新帶有標記位的引用型別。可以原子更新一個布林型別的標記位和引用型別。構造方法是AtomicMarkableReference(V initialRef,boolean initialMark)。

以上幾個類提供的方法幾乎一樣,所以本節僅以AtomicReference為例進行講解,AtomicReference的使用示例程式碼如程式碼清單7-5所示。

程式碼清單7-5 AtomicReferenceTest.java

public class AtomicReferenceTest {  
    public static AtomicReference<user> atomicUserRef = new AtomicReference<user>();  
  
    public static void main(String[] args) {  
        User user = new User("conan", 15);
		atomicUserRef.set(user);  
        User updateUser = new User("Shinichi", 17); 
		atomicUserRef.compareAndSet(user, updateUser);  
        System.out.println(atomicUserRef.get().getName());  
        System.out.println(atomicUserRef.get().getOld());  
    }  
  
    @data
	@gouzaoqi
    static class User {  
        private String name;  
        private int old;  
   
    }  
}

程式碼中首先構建一個user物件,然後把user物件設定進AtomicReferenc中,最後呼叫compareAndSet方法進行原子更新操作,實現原理同AtomicInteger裡的compareAndSet方法。程式碼執行後輸出結果如下。

Shinichi
17

四、原子更新欄位類

如果需原子地更新某個類裡的某個欄位時,就需要使用原子更新欄位類,Atomic包提供了以下3個類進行原子欄位更新。

  • AtomicIntegerFieldUpdater:原子更新整型的欄位的更新器

  • AtomicLongFieldUpdater:原子更新長整型欄位的更新器。

  • AtomicStampedReference:原子更新帶有版本號的引用型別。該類將整數值與引用關聯起來,可用於原子的更新資料和資料的版本號,可以解決使用CAS進行原子更新時可能出現的ABA問題。

要想原子地更新欄位類需要兩步。
1、第一步,因為原子更新欄位類都是抽象類,每次使用的時候必須使用靜態方法newUpdater()建立一個更新器,並且需要設定想要更新的類和屬性。
2、第二步,更新類的欄位(屬性)必須使用public volatile修飾符。

以上3個類提供的方法幾乎一樣,所以本節僅以AstomicIntegerFieldUpdater為例進行講解,AstomicIntegerFieldUpdater的示例程式碼如程式碼清單7-6所示。

程式碼清單7-6 AtomicIntegerFieldUpdaterTest.java

public class AtomicIntegerFieldUpdaterTest {

	// 建立原子更新器,並設定需要更新的物件類和物件的屬性

	private static AtomicIntegerFieldUpdater<User> a = 
	AtomicIntegerFieldUpdater.newUpdater(User.class, "old");

	public static void main(String[] args) {

	// 設定柯南的年齡是10歲

	User conan = new User("conan", 10);

	// 柯南長了一歲,但是仍然會輸出舊的年齡

	System.out.println(a.getAndIncrement(conan));

	// 輸出柯南現在的年齡

	System.out.println(a.get(conan));

	}
	
	@data
	@AllArgsConstructor
	public static class User {

	private String name;

	public volatile int old;

	}

}

程式碼執行後輸出如下。

10
11

五、本章小結

本章介紹了JDK中併發包裡的13個原子操作類以及原子操作類的實現原理,讀者需要熟悉這些類和使用場景,在適當的場合下使用它