1. 程式人生 > >併發:Java中的13個原子操作類。

併發: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設定值後,可能會導致其他執行緒在之後的一段時間內還是可以讀到舊的值。
  • int getAndSet(int newValue):以原子方式設定為newValue的值,並返回舊值。

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

那麼getAndIncrement是如何實現原子操作的呢?讓我們一起分析其實現原理,getAndIncrement的原始碼如下所示。

    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的原始碼,如下所示。

/**
 *    如果當前數值是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的使用示例程式碼如下所示。

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

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

原子更新引用型別

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

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

以上幾個類提供的方法幾乎一樣,所以這裡以AtomicReference為例進行講解,AtomicReference的使用示例程式碼如下所示。
 

public class AtomicReferenceTest {
	public static AtomicReference<User> atomicUserRef = new AtomicReference<User>();
	
	public static void main(String[] args) {
		User user = new User("hello", 18);
		atomicUserRef.set(user);
		User updateUser = new User("world", 20);
		atomicUserRef.compareAndSet(user, updateUser);
		System.out.println(atomicUserRef.get().getName());
		System.out.println(atomicUserRef.get().getOld());
	}

	static class User {
		private String name;
		private int old;
		public User(String name, int old) {
			this.name = name;
			this.old = old;
		}
		public String getName() {
			return name;
		}
		public int getOld() {
			return old;
		}
	}
}

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

原子更新欄位類

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

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

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

以上3個類提供的方法幾乎一樣,所以這裡僅以AstomicIntegerFieldUpdater為例進行講解,AstomicIntegerFieldUpdater的示例程式碼如下所示。

public class AstomicIntegerFieldUpdaterTest {
	// 建立原子更新器,並設定需要更新的物件類和物件的屬性
	private static AtomicIntegerFieldUpdater<User> a = AtomicIntegerFieldUpdater.newUpdater(User.class, "old");
	public static void main(String[] args) {
		// 設定年齡是10歲
		User user = new User("test", 10);
		// 長了一歲,但是仍然會輸出舊的年齡
		System.out.println(a.getAndIncrement(user));
		// 輸出現在的年齡
		System.out.println(a.get(user));
	}
	
	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;
			}
	}
}