本篇博客由本人根據衆多優秀的博客文章和書籍整理而來,參考的博客鏈接請看文章最下方,爲尊重參考博客的原創作者,特標爲轉載。
前言
Java從JDK1.5開始提供了java.util.concurrent.atomic包,方便程序員在多線程環境下,無鎖的進行原子操作。原子變量的底層使用了處理器提供的原子指令,但是不同的CPU架構可能提供的原子指令不一樣,也有可能需要某種形式的內部鎖,所以該方法不能絕對保證線程不被阻塞。
Atomic包
概述
在JDK1.8中,Atomic包裏一共有17個類,四種原子更新方式,分別是原子更新基本類型,原子更新數組,原子更新引用和原子更新字段。Atomic包裏的類基本都是使用Unsafe實現的包裝類。
除了上圖中的12個類外,Java8在atomic包新增了5個類,分別是Striped64,LongAdder,LongAccumulator,DoubleAdder,DoubleAccumulator。其中,Sriped64作爲父類,其他分別是long和double的具體實現。
原子更新基本類型類
用於通過原子的方式更新基本類型,Atomic包提供了以下三個類:
AtomicBoolean:原子更新布爾類型。
AtomicInteger:原子更新整型。
AtomicLong:原子更新長整型。
AtomicInteger的常用方法如下:
☛ int addAndGet(int delta) :以原子方式將輸入的數值與實例中的值(AtomicInteger裏的value)相加,並返回結果
☛ boolean compareAndSet(int expect, int update) :如果輸入的數值等於預期值,則以原子方式將該值設置爲輸入的值。
☛ int getAndIncrement():以原子方式將當前值加1,注意:這裏返回的是自增前的值。
☛ void lazySet(int newValue):最終會設置成newValue,使用lazySet設置值後,可能導致其他線程在之後的一小段時間內還是可以讀到舊的值。關於該方法的更多信息可以參考併發網翻譯的一篇文章《AtomicLong.lazySet是如何工作的?》
☛ int getAndSet(int newValue):以原子方式設置爲newValue的值,並返回舊值。
AtomicInteger例子代碼如下:
static AtomicInteger ai = new AtomicInteger(1);
public static void main(String[] args) {
System.out.println(ai.getAndIncrement());//返回的是自增前的值
System.out.println(ai.get());
}
運行結果:
1
2
Atomic包提供了三種基本類型的原子更新,但是Java的基本類型裏還有char,float和double等。那麼問題來了,如何原子的更新其他的基本類型呢?Atomic包裏的類基本都是使用Unsafe實現的,讓我們一起看下Unsafe的源碼,發現Unsafe只提供了三種CAS方法,compareAndSwapObject,compareAndSwapInt和compareAndSwapLong,再看AtomicBoolean源碼,發現其是先把Boolean轉換成整型,再使用compareAndSwapInt進行CAS,所以原子更新double也可以用類似的思路來實現。
原子更新數組類
通過原子的方式更新數組裏的某個元素,Atomic包提供了以下三個類:
AtomicIntegerArray:原子更新整型數組裏的元素。
AtomicLongArray:原子更新長整型數組裏的元素。
AtomicReferenceArray:原子更新引用類型數組裏的元素。
AtomicIntegerArray類主要是提供原子的方式更新數組裏的整型,其常用方法如下
☛ int addAndGet(int i, int delta):以原子方式將輸入值與數組中索引i的元素相加。
☛ boolean compareAndSet(int i, int expect, int update):如果當前值等於預期值,則以原子方式將數組位置i的元素設置成update值。
實例代碼如下:
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 //ai數組中索引爲0的值爲3
1 //value數組中索引爲0的值仍爲1
AtomicIntegerArray類需要注意的是,數組value通過構造方法傳遞進去,然後AtomicIntegerArray會將當前數組複製一份,所以當AtomicIntegerArray對內部的數組元素進行修改時,不會影響到傳入的數組。
原子更新引用類型
原子更新基本類型的AtomicInteger,只能更新一個變量,如果要原子的更新多個變量,就需要使用這個原子更新引用類型提供的類。Atomic包提供了以下三個類:
AtomicReference:原子更新引用類型。
AtomicReferenceFieldUpdater:原子更新引用類型裏的字段。
AtomicMarkableReference:原子更新帶有標記位的引用類型。可以原子的更新一個布爾類型的標記位和引用類型。構造方法是AtomicMarkableReference(V initialRef, boolean initialMark)
AtomicReference的使用例子代碼如下:
public static AtomicReference<User> atomicUserRef = new AtomicReference<>();
public static void main(String[] args) {
User user = new User("why", 15);
atomicUserRef.set(user);
User updateUser = new User("Tony", 17);
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;
}
}
運行結果:
Tony
17
原子更新字段類
如果我們只需要某個類裏的某個字段,那麼就需要使用原子更新字段類,Atomic包提供了以下三個類:
AtomicIntegerFieldUpdater:原子更新整型的字段的更新器。
AtomicLongFieldUpdater:原子更新長整型字段的更新器。
AtomicStampedReference:原子更新帶有版本號的引用類型。該類將整數值與引用關聯起來,可用於原子的更數據和數據的版本號,可以解決使用CAS進行原子更新時,可能出現的ABA問題。
原子更新字段類都是抽象類,每次使用都時候必須使用靜態方法newUpdater創建一個更新器。原子更新類的字段的必須使用public volatile修飾符。
AtomicIntegerFieldUpdater的例子代碼如下:
private static AtomicIntegerFieldUpdater<User> a = AtomicIntegerFieldUpdater.newUpdater(User.class, "old");
public static void main(String[] args) {
User conan = new User("conan", 10);
System.out.println(a.getAndIncrement(conan));
System.out.println(a.get(conan));
}
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;
}
}
運行結果如下:
10
11
JDK1.8新增LongAdder、DoubleAdder、LongAccumulator、DoubleAccumulator
詳細介紹請參考如下連接:
Java併發學習(十一)-LongAdder和LongAccumulator探究
Java併發編程札記-(三)JUC原子類-06JDK1.8新增:LongAdder、DoubleAdder、LongAccumulator、DoubleAccumulator
原子類實現分析
我們知道在多線程環境下,類似於num++這樣的複合操作的問題存在線程安全問題,因爲,num++看似簡單的一個操作,實際上是由1.讀取 2.加一 3.寫入三步組成的,這是個複合類的操作(所以在volatile中所說它是無法解決num++的原子性問題的),在併發環境下,如果不做任何同步處理,就會有線程安全問題,讓我們通過下面的例子來看一下:
public class MyThreadD {
public static int count = 0;
public static void main(String[] args) {
//開啓5個線程
for (int i = 0; i < 5; i++) {
new Thread(() -> {
//每個線程當中讓count值自增1000次
for (int j = 0; j < 1000; j++) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
System.out.println("當前線程爲:"+Thread.currentThread().getName() + " count=" + count);
}
}).start();
}
}
}
運行結果爲:
運行多次,只有很小几率能夠運行爲正確結果:5000
悲觀的解決方案(阻塞同步)
這種線程安全問題,最簡單的我們首先想到用加鎖的方式解決,例如使用synchronized同步關鍵字
synchronized (MyThreadD.class) {
count++;
System.out.println("當前線程爲:" + Thread.currentThread().getName() + " count=" + count);
}
運行結果爲:
使用獨佔鎖機制來解決,是一種悲觀的併發策略,抱着一副“總有刁民想害朕”的想法,每次操作數據的時候都認爲別的線程會參與競爭修改。同一時刻只能有一個線程持有鎖,那其他線程就會阻塞。線程的掛起恢復會帶來很大的性能開銷,儘管JVM對於非競爭鎖的獲取和釋放做了很多優化,但是一旦多個線程競爭鎖,頻繁的阻塞喚醒,還是會有很大的性能開銷的。所以,使用synchronized或其他重量級鎖來處理顯然不夠合理。
樂觀的解決方案(非阻塞同步)
樂觀的解決方案,顧名思義,就是很大度樂觀,每次操作數據的時候,都認爲別的線程不會參與競爭修改,也不加鎖。如果操作成功了那最好;如果失敗了,比如中途確定有別的線程進入並修改了數據(依賴於衝突檢測),也不會阻塞,可以採取一些補償機制,一般的策略就是反覆重試。很顯然,這種思想比簡單粗暴的利用鎖來同步要合理的多。
使用原子類來解決上面的線程安全問題:
修改代碼如下
public static AtomicInteger count = new AtomicInteger(0); //定義原子類
count.incrementAndGet(); //count++
運行結果如下:
原子類的實現依賴於CAS算法,這個我們已經在上篇博客中以AtomicInteger爲例進行介紹過了。
[Java高併發編程](一)理解CAS
https://blog.csdn.net/why15732625998/article/details/80092206
本系列博客目錄貼:
如有錯誤,歡迎指正,不勝感激!
參考鏈接
https://www.cnblogs.com/chengxiao/p/6789109.html
http://ifeve.com/java-atomic/
http://ifeve.com/atomic-operation/