Java并发编程—Atomic原子类
目錄
Atomic
1. AtomicInteger
a. 多線程并發訪問問題
b. 用 AtomicInteger 類解決
2. AtomicIntegerArray
a. 多線程并發訪問問題
b. 用 AtomicIntegerArray 類解決
相關問題
Atomic
在 java.util.concurrent.atomic 包下定義了一些對“變量”操作的“原子類”,它們可以保證對“變量”操作的:原子性、有序性、可見性:
- java.util.concurrent.atomic.AtomicInteger:對 int 變量操作的“原子類”;
- java.util.concurrent.atomic.AtomicLong:對 long 變量操作的“原子類”;
- java.util.concurrent.atomic.AtomicBoolean:對 boolean 變量操作的“原子類”;
1. AtomicInteger
a. 多線程并發訪問問題
class MyThread extends Thread {public static volatile int a = 0;@Overridepublic void run() {for (int i = 0; i < 10000; i++) {//線程1:取出a的值a=0(被暫停)a++;//寫回}System.out.println("修改完畢!");} }public class Test {public static void main(String[] args) throws InterruptedException {//1.啟動兩個線程MyThread t1 = new MyThread();MyThread t2 = new MyThread();t1.start();t2.start();Thread.sleep(1000);System.out.println("獲取a最終值:" + MyThread.a);//最終結果仍然不正確。} } /* 輸出 修改完畢! 修改完畢! 獲取a最終值:14791*/- 加入 volatile 關鍵字并不能解決變量自增操作的原子性問題;
b. 用 AtomicInteger 類解決
- 用 AtomicInteger 類解決了多線程的原子性問題,無論程序運行多少次,其結果總是正確的;
c. 工作原理:CAS 機制
在 Unsafe 類中,調用了一個:compareAndSwapInt() 方法,此方法的幾個參數:
- var1:傳入的 AtomicInteger 對象
- var2:AtommicInteger 內部變量的偏移地址
- var5:之前取出的 AtomicInteger 中的值
- var5 + var4:預期結果
此方法使用了一種"比較并交換(Compare And Swap)"的機制,它會用 var1 和 var2 先獲取內存中 AtomicInteger 中的值,然后和傳入的,之前獲取的值 var5 做一下比較,也就是比較當前內存的值和預期的值是否一致,如果一致就修改為 var5 + var4,否則就繼續循環,再次獲取 AtomicInteger 中的值,再進行比較并交換,直至成功交換為止;compareAndSwapInt() 方法是"線程安全"的;我們假設兩個線程交替運行的情況,看看它是怎樣工作的:
- 初始 AtomicInteger 的值為0
- 線程A執行:var5 = this.getIntVolatile(var1,var2);獲取的結果為:0
- 線程A被暫停
- 線程B執行:var5 = this.getIntVolatile(var1,var2);獲取的結果為:0
- 線程B執行:this.compareAndSwapInt(var1,var2,var5,var5 + var4)
- 線程B成功將 AtomicInteger 中的值改為1
- 線程A恢復運行,執行:this.compareAndSwapInt(var1,var2,var5,var5 + var4)
- 此時線程A使用 var1 和 var2 從 AtomicInteger 中獲取的值為:1,而傳入的 var5 為0,比較失敗,返回 false,繼續循環
- 線程A執行:var5 = this.getIntVolatile(var1,var2);獲取的結果為:1
- 線程A執行:this.compareAndSwapInt(var1,var2,var5,var5 + var4)
- 此時線程A使用 var1 和 var2 從 AtomicInteger 中獲取的值為:1,而傳入的var5為1,比較成功,將其修改為 var5 + var4,也就是2,將 AtomicInteger 中的值改為2,結束;
CAS 機制也被稱為樂觀鎖機制、自旋鎖機制,因為大部分比較的結果為 true,就直接修改了。只有少部分多線程并發的情況會導致 CAS 失敗,而再次循環;
- 關于樂觀鎖與悲觀鎖的實際應用;
- 關于CAS理論
2. AtomicIntegerArray
常用的數組操作的原子類,它們可以解決數組的多線程并發訪問的安全性問題:
- java.util.concurrent.atomic.AtomicIntegetArray:對 int 數組操作的原子類;
- java.util.concurrent.atomic.AtomicLongArray:對 long 數組操作的原子類;
- java.utio.concurrent.atomic.AtomicReferenceArray:對引用類型數組操作的原子類;
a. 多線程并發訪問問題
class MyThread extends Thread {private static int[] intArray = new int[1000];//不直接使用數組@Overridepublic void run() {for (int i = 0; i < getIntArrayLength(); i++) {intArray[i]++;}}public static int getIntArray(int i) {return intArray[i];}public static int getIntArrayLength() {return intArray.length;} }public class Test {public static void main(String[] args) throws InterruptedException {for (int i = 0; i < 1000; i++) {new MyThread().start();//創建1000個線程,每個線程為數組的每個元素+1}Thread.sleep(1000 * 5);//讓所有線程執行完畢System.out.println("主線程休息5秒醒來");for (int i = 0; i < MyThread.getIntArrayLength(); i++) {System.out.println(MyThread.getIntArray(i));}} } /* 部分輸出 ... 999 999 1000 1000 ...*/- 可以發現,有些元素并不是1000;
b. 用 AtomicIntegerArray 類解決
- AtomicIntegerArray 類可以保證數組的多線程安全;
- 可見,每次運行的結果都是正確的;
相關問題
1:為什么會出現Atomic類
在多線程或者并發環境中,我們常常會遇到這種情況 int i=0; i++ 稍有經驗的同學都知道這種寫法是線程不安全的。為了達到線程安全的目的,我們通常會用synchronized來修飾對應的代碼塊。現在我們有了新的方法,就是使用J.U.C包下的atomic類。
2:Atomic類的原理是什么呢
一句話來說,atomic類是通過自旋CAS操作volatile變量實現的。CAS是compare and swap的縮寫,即比較后(比較內存中的舊值與預期值)交換(將舊值替換成預期值)。它是sun.misc包下Unsafe類提供的功能,需要底層硬件指令集的支撐。使用volatile變量是為了多個線程間變量的值能及時同步。
3:為什么使用Atomic類
按理來說,使用synchroized已經能滿足功能需求了。為什么還會有這個類呢?那肯定是性能的問題了。在JDK1.6之前,synchroized是重量級鎖,即操作被鎖的變量前就對對象加鎖,不管此對象會不會產生資源競爭。這屬于悲觀鎖的一種實現方式。而CAS會比較內存中對象和當前對象的值是否相同,相同的話才會更新內存中的值,不同的話便會返回失敗。這是樂觀鎖的一中實現方式。這種方式就避免了直接使用內核狀態的重量級鎖。但是在JDK1.6以后,synchronized進行了優化,引入了偏向鎖,輕量級鎖,其中也采用了CAS這種思想,效率有了很大的提升。
4:Atomic類的缺點
1)ABA問題:對于一個舊的變量值A,線程2將A的值改成B又改成可A,此時線程1通過CAS看到A并沒有變化,但實際A已經發生了變化,這就是ABA問題。解決這個問題的方法很簡單,記錄一下變量的版本就可以了,在變量的值發生變化時對應的版本也做出相應的變化,然后CAS操作時比較一下版本就知道變量有沒有發生變化。atomic包下AtomicStampedReference類實現了這種思路。Mysql中Innodb的多版本并發鎖也是這個原理。
2)自旋問題:atomic類會多次嘗試CAS操作直至成功或失敗,這個過程叫做自旋。通過自旋的過程我們可以看出自旋操作不會將線程掛起,從而避免了內核線程切換,但是自旋的過程也可以看做CPU死循環,會一直占用CPU資源。這種情形在單CPU的機器上是不能容忍的,因此自旋一般都會有個次數限制,即超過這個次數后線程就會放棄時間片,等待下次機會。因此自旋操作在資源競爭不激烈的情況下確實能提高效率,但是在資源競爭特別激烈的場景中,CAS操作會的失敗率就會大大提高,這時使用中重量級鎖的效率可能會更高。當前,也可以使用LongAdder類來替換,它則采用了分段鎖的思想來解決并發競爭的問題。
總結
以上是生活随笔為你收集整理的Java并发编程—Atomic原子类的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java并发编程—常见面试题
- 下一篇: Java并发编程—为什么wait/not