Java併發--原子變數類的使用
注:本篇部落格主要內容來源於網路,侵刪~
引言
我們假設你已經熟練掌握了CAS,原子變數類等的相關概念。這篇部落格中,我們主要討論原子變數類的使用。
原子變數類
原子變數類共12個,分4組:
- 計數器:
AtomicInteger
,AtomicLong
,AtomicBoolean
,AtomicReference
。 - 域更新器:
AtomicIntegerFieldUpdater
,AtomicLongFieldUpdater
,AtomicReferenceFieldUpdater
。 - 陣列:
AtomicIntegerArray
,AtomicLongArray
,AtomicReferenceArray
。 - 複合變數:
AtomicMarkableReference
,AtomicStampedReference
。
在每組中我會選擇其中一個較有代表性的進行分析與舉例。
AtomicReference
使用說明
AtomicReference的作用是對"物件"進行原子操作。
原始碼分析
public class AtomicReference<V> implements java.io.Serializable {
private static final long serialVersionUID = -1848883965231344442L;
// 獲取Unsafe物件,Unsafe的作用是提供CAS操作
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
// 獲取相應欄位相對Java物件的“起始地址”的偏移量
valueOffset = unsafe.objectFieldOffset
(AtomicReference.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
// volatile型別
private volatile V value;
public AtomicReference(V initialValue) {
value = initialValue;
}
public AtomicReference() {
}
public final V get() {
return value;
}
public final void set(V newValue) {
value = newValue;
}
public final void lazySet(V newValue) {
unsafe.putOrderedObject(this, valueOffset, newValue);
}
public final boolean compareAndSet(V expect, V update) {
return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}
public final boolean weakCompareAndSet(V expect, V update) {
return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}
public final V getAndSet(V newValue) {
while (true) {
V x = get();
if (compareAndSet(x, newValue))
return x;
}
}
public String toString() {
return String.valueOf(get());
}
}
關於上述程式碼只有兩點需要強調:
valueOffset = unsafe.objectFieldOffset(AtomicReference.class.getDeclaredField("value"))
通過相關欄位的偏移量獲取值比直接使用反射獲取相應欄位的值效能要好許多;
使用舉例
class Person {
volatile long id;
public Person(long id) {
this.id = id;
}
public String toString() {
return "id:" + id;
}
}
public class AtomicReferenceTest {
public static void main(String[] args) {
// 建立兩個Person物件,它們的id分別是101和102。
Person p1 = new Person(101);
Person p2 = new Person(102);
// 新建AtomicReference物件,初始化它的值為p1物件
AtomicReference ar = new AtomicReference(p1);
// 通過CAS設定ar。如果ar的值為p1的話,則將其設定為p2。
ar.compareAndSet(p1, p2);
Person p3 = (Person)ar.get();
System.out.println("p3 is "+p3);
System.out.println("p3.equals(p1)="+p3.equals(p1));
}
}
AtomicReferenceFieldUpdater
接下來所有的原子變數類不再進行原始碼分析。事實上所有原子變數類的實現都大同小異,感興趣的同學可以閱讀原始碼。
使用說明
一個基於反射的工具類,它能對指定類的指定的volatile引用欄位進行原子更新。(注意這個欄位不能是private的)
通過呼叫AtomicReferenceFieldUpdater的靜態方法newUpdater就能建立它的例項,該方法要接收三個引數:
- 包含該欄位的物件的類;
- 將被更新的物件的類;
- 將被更新的欄位的名稱。
使用舉例
class Dog {
volatile String name = "dog1";
}
public class App {
public static void main(String[] args) throws Exception {
AtomicReferenceFieldUpdater updater = AtomicReferenceFieldUpdater.newUpdater(Dog.class, String.class, "name");
Dog dog1 = new Dog();
updater.compareAndSet(dog1, dog1.name, "test");
System.out.println(dog1.name);
}
}
AtomicReferenceArray
使用說明
可以用原子方式更新其元素的物件引用陣列。
以下是AtomicReferenceArray類中可用的重要方法的列表:
序列 | 方法 | 描述 |
---|---|---|
1 | public AtomicReferenceArray(int length) | 建構函式,建立給定長度的新 AtomicReferenceArray。 |
2 | public AtomicReferenceArray(E[] array) | 建構函式,建立與給定陣列具有相同長度的新 AtomicReferenceArray,並從給定陣列複製其所有元素。 |
3 | public boolean compareAndSet(int i, E expect, E update) | 如果當前值==期望值,則將位置i處的元素原子設定為給定的更新值。 |
4 | public E get(int i) | 獲取位置i的當前值。 |
5 | public E getAndSet(int i, E newValue) | 將位置i處的元素原子設定為給定值,並返回舊值。 |
6 | public void set(int i, E newValue) | 將位置i處的元素設定為給定值。 |
使用舉例
public class TestThread {
// 建立原子引用陣列
private static String[] source = new String[10];
private static AtomicReferenceArray<String> atomicReferenceArray = new AtomicReferenceArray<String>(source);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < atomicReferenceArray.length(); i++) {
atomicReferenceArray.set(i, "item-2");
}
Thread t1 = new Thread(new Increment());
Thread t2 = new Thread(new Compare());
t1.start();
t2.start();
t1.join();
t2.join();
}
static class Increment implements Runnable {
public void run() {
for(int i = 0; i < atomicReferenceArray.length(); i++) {
String add = atomicReferenceArray.getAndSet(i, "item-" + (i+1));
System.out.println("Thread " + Thread.currentThread().getId()
+ ", index " + i + ", value: " + add);
}
}
}
static class Compare implements Runnable {
public void run() {
for(int i = 0; i< atomicReferenceArray.length(); i++) {
System.out.println("Thread " + Thread.currentThread().getId()
+ ", index " + i + ", value: " + atomicReferenceArray.get(i));
boolean swapped = atomicReferenceArray.compareAndSet(i, "item-2", "updated-item-2");
System.out.println("Item swapped: " + swapped);
if(swapped) {
System.out.println("Thread " + Thread.currentThread().getId()
+ ", index " + i + ", updated-item-2");
}
}
}
}
}
AtomicStampedReference
使用說明
AtomicStampedReference主要用來解決在使用CAS演算法的過程中,可能會產生的ABA問題。一般我們會使用帶有版本戳version的記錄或物件標記來解決ABA問題,AtomicStampedReference實現了這個作用,它通過包裝[E, Integer]的元組來對物件標記版本戳stamp。
以下是AtomicStampedReference類中可用的重要方法的列表:
序列 | 方法 | 描述 |
---|---|---|
1 | public AtomicStampedReference(V initialRef, int initialStamp) | 構造方法,傳入引用和戳。 |
2 | public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp) | 如果當前引用 == 預期值並且當前版本戳 == 預期版本戳,將更新新的引用和新的版本戳到記憶體。 |
3 | public void set(V newReference, int newStamp) | 設定當前引用的新引用和版本戳。 |
4 | public boolean attemptStamp(V expectedReference, int newStamp) | 如果當前引用 == 預期引用,將更新新的版本戳到記憶體。 |
使用舉例
下面的程式碼分別用AtomicInteger和AtomicStampedReference來對初始值為100的原子整型變數進行更新,AtomicInteger會成功執行CAS操作,而加上版本戳的AtomicStampedReference對於ABA問題會執行CAS失敗:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;
public class ABA {
private static AtomicInteger atomicInt = new AtomicInteger(100);
private static AtomicStampedReference atomicStampedRef = new AtomicStampedReference(100, 0);
public static void main(String[] args) throws InterruptedException {
Thread intT1 = new Thread(new Runnable() {
@Override
public void run() {
atomicInt.compareAndSet(100, 101);
atomicInt.compareAndSet(101, 100);
}
});
Thread intT2 = new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
}
boolean c3 = atomicInt.compareAndSet(100, 101);
System.out.println(c3); // true
}
});
intT1.start();
intT2.start();
intT1.join();
intT2.join();
Thread refT1 = new Thread(new Runnable() {
@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
}
atomicStampedRef.compareAndSet(100, 101, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
atomicStampedRef.compareAndSet(101, 100, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
}
});
Thread refT2 = new Thread(new Runnable() {
@Override
public void run() {
int stamp = atomicStampedRef.getStamp();
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
}
boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp + 1);
System.out.println(c3); // false
}
});
refT1.start();
refT2.start();
}
}
效能比較:鎖與原子變數
事實上,CAS的效能總是優於鎖。我們分兩種情況進行討論。
1. 執行緒間競爭程度較高
對於鎖來說,激烈的競爭意味著執行緒頻繁的掛起與恢復,頻繁的上下文切換,這些操作都是非常耗費系統資源的;對於CAS演算法來說,激烈的競爭意味著執行緒將對競爭進行頻繁的處理(重試,回退,放棄等策略)。
即使如此,一般來說,CAS演算法的效能依舊優於鎖。
2. 執行緒間競爭程度較低
較低的競爭程度意味著CAS操作總是能夠成功;對於鎖來說,雖然鎖之間的競爭度也隨之下降,但由於獲取鎖與釋放鎖的操作不但耗費系統資源,並且其中本身就包含著CAS操作,因此在這種情況下,CAS操作的效能依舊優於鎖。
總結
- 這篇部落格並沒有講述CAS操作以及可能產生的ABA問題,但是我們必須熟悉這兩個知識點;
- 這篇部落格的主要目的是構建起大家對原子變數類的一個認識,以至於在以後的專案開發中,可以去思考如何使用這些原子變數類;
- 對於原子變數與鎖之間的優勢與劣勢,效能間的比較,有一個較為清晰的認識。
參考閱讀
Java併發程式設計實戰