在並發編程中很容易出現並發安全問題,最簡單的例子就是多線程更新變量i=1,多個線程執行i++操作,就有可能獲取不到正確的值,而這個問題,最常用的方法是通過Synchronized進行控制來達到線程安全的目的。但是由於synchronized是采用的是悲觀鎖策略,並不是特別高效的一種解決方案。實際上,在J.U.C下的Atomic包提供了一系列的操作簡單,性能高效,並能保證線程安全的類去更新多種類型。Atomic包下的這些類都是采用樂觀鎖策略CAS來更新數據。
CAS原理與問題
CAS操作(又稱為無鎖操作)是一種樂觀鎖策略。它假設所有線程訪問共享資源的時候不會出現沖突,因此不會阻塞其他線程的操作。那么,如果出現沖突了怎么辦?無鎖操作是使用CAS(compare and swap)來鑒別線程是否出現沖突,出現沖突就重試當前操作直到沒有沖突為止。
CAS的操作過程
舉例說明:
Atomic包中的AtomicInteger類,是通過Unsafe類下的native函數compareAndSwapInt自旋來保證原子性,
其中incrementAndGet函數調用的getAndAddInt函數如下所示:
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
CAS有3個操作數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改為B,否則什么都不做。
可見只有自旋實現更新數據操作之后,while循環才能夠結束。
CAS的問題
- 自旋時間過長。由
compareAndSwapInt
函數可知,自旋時間過長會對性能是很大的消耗。 - ABA問題。因為CAS會檢查舊值有沒有變化,這里存在這樣一個有意思的問題。比如一個舊值A變為了成B,然后再變成A,剛好在做CAS時檢查發現舊值並沒有變化依然為A,但是實際上的確發生了變化。解決方案可以添加一個版本號可以解決。原來的變化路徑A->B->A就變成了1A->2B->3C,或使用AtomicStampedReference工具類。
Atomic包的使用
原子更新基本類型
Atomic包中原子更新基本類型的工具類:
AtomicBoolean:以原子更新的方式更新boolean;
AtomicInteger:以原子更新的方式更新Integer;
AtomicLong:以原子更新的方式更新Long;
這幾個類的用法基本一致,這里以AtomicInteger為例總結常用的方法
- addAndGet(int delta):以原子方式將輸入的數值與實例中原本的值相加,並返回最后的結果;
- incrementAndGet() :以原子的方式將實例中的原值進行加1操作,並返回最終相加后的結果;
- getAndSet(int newValue):將實例中的值更新為新值,並返回舊值;
- getAndIncrement():以原子的方式將實例中的原值加1,返回的是自增前的舊值;
原理不再贅述,參考上文compareAndSwapInt
函數。
AtomicInteger使用示例:
public class AtomicExample {
private static AtomicInteger atomicInteger = new AtomicInteger(2);
public static void main(String[] args) {
System.out.println(atomicInteger.getAndIncrement());
System.out.println(atomicInteger.incrementAndGet());
System.out.println(atomicInteger.get());
}
}
// 2 4 4
LongAdder
為了解決自旋導致的性能問題,JDK8在Atomic包中推出了LongAdder類。LongAdder采用的方法是,共享熱點數據分離的計數:將一個數字的值拆分為一個數組。不同線程會命中到數組的不同槽中,各個線程只對自己槽中的那個值進行CAS操作,這樣熱點就被分散了,沖突的概率就小很多;要得到這個數字的話,就要把這個值加起來。相比AtomicLong,並發量大大提高。
優點:有很高性能的並發寫的能力
缺點:讀取的性能不是很高效,而且如果讀取的時候出現並發寫的話,結果可能不是正確的
原子更新數組類型
Atomic包中提供能原子更新數組中元素的工具類:
AtomicIntegerArray:原子更新整型數組中的元素;
AtomicLongArray:原子更新長整型數組中的元素;
AtomicReferenceArray:原子更新引用類型數組中的元素
這幾個類的用法一致,就以AtomicIntegerArray來總結下常用的方法:
- addAndGet(int i, int delta):以原子更新的方式將數組中索引為i的元素與輸入值相加;
- getAndIncrement(int i):以原子更新的方式將數組中索引為i的元素自增加1;
- compareAndSet(int i, int expect, int update):將數組中索引為i的位置的元素進行更新
AtomicIntegerArray與AtomicInteger的方法基本一致,只不過在前者的方法中會多一個指定數組索引位i。
AtomicIntegerArray使用示例:
public class AtomicExample {
private static int[] value = new int[]{1, 2, 3};
private static AtomicIntegerArray integerArray = new AtomicIntegerArray(value);
public static void main(String[] args) {
//對數組中索引為2的位置的元素加3
int result = integerArray.getAndAdd(2, 3);
System.out.println(integerArray.get(2));
System.out.println(result);
}
}
// 6 3
原子更新引用類型
如果需要原子更新引用類型變量的話,為了保證線程安全,Atomic也提供了相關的類:
- AtomicReference
:原子更新引用類型; - AtomicReferenceFieldUpdater:原子更新引用類型里的字段;
- AtomicMarkableReference:原子更新帶有標記位的引用類型;
AtomicReference使用示例:
public class AtomicExample {
private static AtomicReference<User> reference = new AtomicReference<>();
public static void main(String[] args) {
User user1 = new User("a", 1);
reference.set(user1);
User user2 = new User("b",2);
User user = reference.getAndSet(user2);
System.out.println(user);
System.out.println(reference.get());
}
static class User {
private String userName;
private int age;
public User(String userName, int age) {
this.userName = userName;
this.age = age;
}
@Override
public String toString() {
return "User{" +
"userName='" + userName + '\'' +
", age=" + age +
'}';
}
}
}
// User{userName='a', age=1}
// User{userName='b', age=2}
AtomicReferenceFieldUpdater使用示例:
public class AtomicExample {
public static void main(String[] args) {
AtomicReferenceFieldUpdater updater = AtomicReferenceFieldUpdater.newUpdater(Dog.class, String.class, "name");
Dog dog1 = new Dog();
updater.compareAndSet(dog1, dog1.name, "cat");
System.out.println(dog1.name);
}
}
class Dog {
volatile String name = "dog1";
}
原子更新字段類型
如果需要更新對象的某個字段,Atomic同樣也提供了相應的原子操作類:
- AtomicIntegeFieldUpdater:原子更新整型字段類;
- AtomicLongFieldUpdater:原子更新長整型字段類;
要想使用原子更新字段需要兩步操作:
原子更新字段類型類都是抽象類,只能通過靜態方法newUpdater來創建一個更新器,並且需要設置想要更新的類和屬性;
更新類的屬性必須使用public volatile進行修飾;
AtomicIntegerFieldUpdater使用示例:
public class AtomicExample {
private static AtomicIntegerFieldUpdater updater = AtomicIntegerFieldUpdater.newUpdater(User.class, "age");
public static void main(String[] args) {
User user = new User("a", 1);
System.out.println(updater.getAndAdd(user, 5));
System.out.println(updater.addAndGet(user, 1));
System.out.println(updater.get(user));
}
static class User {
private String userName;
public volatile int age;
public User(String userName, int age) {
this.userName = userName;
this.age = age;
}
@Override
public String toString() {
return "User{" +
"userName='" + userName + '\'' +
", age=" + age +
'}';
}
}
}
解決CAS的ABA問題
AtomicStampedReference:原子更新引用類型,這種更新方式會帶有版本號,從而解決CAS的ABA問題
AtomicStampedReference使用示例:
public class AtomicExample {
public static void main(String[] args) {
Integer init1 = 1110;
// Integer init2 = 126;
AtomicStampedReference<Integer> reference = new AtomicStampedReference<>(init1, 1);
int curent1 = reference.getReference();
// Integer current2 = reference.getReference();
reference.compareAndSet(reference.getReference(), reference.getReference() + 1, reference.getStamp(), reference.getStamp() + 1);//正確寫法
// reference.compareAndSet(current2, current2+1, reference.getStamp(), reference.getStamp() + 1);//正確寫法
// reference.compareAndSet(1110, 1111, reference.getStamp(), reference.getStamp() + 1);//錯誤寫法
// reference.compareAndSet(curent1, curent1+1, reference.getStamp(), reference.getStamp() + 1);//錯誤寫法
// reference.compareAndSet(current2, current2 + 1, reference.getStamp(), reference.getStamp() + 1);
System.out.println("reference.getReference() = " + reference.getReference());
}
}
AtomicStampedReference踩過的坑
參考上面的代碼,分享一個筆者遇到的一次坑。AtomicStampedReference的compareAndSet
函數中,前兩個參數是使用包裝類的。所以當參數超過128時,而且傳入參數並不是reference.getReference()獲取的話,會導致expectedReference == current.reference為false,則無法進行更新。
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)));
}
最后,限於筆者經驗水平有限,歡迎讀者就文中的觀點提出寶貴的建議和意見。如果想獲得更多的學習資源或者想和更多的是技術愛好者一起交流,可以關注我的公眾號『全菜工程師小輝』后台回復關鍵詞領取學習資料、進入前后端技術交流群和程序員副業群。同時也可以加入程序員副業群Q群:735764906 一起交流。