高級Java開發工程師帶你走進原子操作,一篇文章搞清楚原子操作

原子特性: 原子是最小的粒子,不可再分

這並不是一個化學課,而是巧妙的借用了化學上的一個概念,即原子是最小的粒子,不可再分;原子操作也是不能再分的操作;
爲了能把這個講明白,下文基本都是大白話,其實Java本來並不是很難,而是總有一些人喜歡把簡單的概念給複雜化。小編不喜歡
那種說辭,所以儘量簡單易懂。如有問題,歡迎提出問題。共同交流進步,最後謝謝你的閱讀。


舉例說明原子操作重要性

在很多場景中我們需要我們的操作是原子特性的,如果我們寫的程序都是單線程的,其實我們沒必要考慮原子操作。但是假如
我們寫多線程操作,或者是在Web服務中來更新對象屬性,那麼就必須要來考慮原子操作問題了。

舉一個🌰例子A:

int a = 1;

可以看到程序對變量 a 操作,其實是有多個步驟進行的。在單線程環境下基本不會發生任何問題

舉一個🌰例子B(單線程操作):

public class Tester {

    private static Integer a = 1;

    private static AtomicInteger aa = new AtomicInteger(1);

    private static void restore() {
        a = 1;
        aa = new AtomicInteger(1);
    }

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            test("第" + i + "次");
            restore();
        }
    }

    private static void test(String str) {
        for (int i = 1; i <= 1000; i++) {
            new Thread(() -> a = a + 1).start();
            new Thread(() -> aa.addAndGet(1)).start();
        }
        System.out.print(str + "常規操作a=" + a);
        System.out.println(" <===> "+str+"原子操作操作aa=" + aa);
    }
}

規律:

        /**
         * i              i+1
         * 1: a = 1 + 1 = 2
         * 2: a = 2 + 1 = 3
         * 3: a = 3 + 1 = 4
         * 4: a = 4 + 1 = 5
         * 5: a = 5 + 1 = 6
         * 6: a = 6 + 1 = 7
         * 7: a = 7 + 1 = 8
         * 8: a = 8 + 1 = 9
         * 9: a = 9 + 1 = 10
         * 10:a = 10 + 1 = 11
         */

如上面代碼變量a是基本類型,變量aa是原子類型,正常情況對a或者aa進行1000次操作結果都應該是
1001。正常情況我們可以理解是單線程操作。結果也是沒有問題的。

舉一個🌰例子C(多線程操作):

public class Tester {

    private static Integer a = 1;

    private static AtomicInteger aa = new AtomicInteger(1);

    private static void restore() {
        a = 1;
        aa = new AtomicInteger(1);
    }

    public static void main(String[] args) throws Exception {
        for (int i = 0; i < 10; i++) {
            test("第" + i + "次");
            restore();
        }
    }

    private static void test(String str) throws Exception {
        for (int i = 1; i <= 100; i++) {
            new Thread(() -> a = a + 1).start();
            new Thread(() -> a = a + 1).start();

            new Thread(() -> aa.addAndGet(1)).start();
            new Thread(() -> aa.addAndGet(1)).start();
            Thread.sleep(1);
        }
        System.out.print(str + "常規操作a=" + a);
        System.out.println(" <===> " + str + "原子操作操作aa=" + aa);
    }
    
}

規律:

    /**
     * i          2 * i + 1
     * 1: a = 1 + 1 + 1 = 3
     * 2: a = 3 + 1 + 1 = 5
     * 3: a = 5 + 1 + 1 = 7
     * 4: a = 7 + 1 + 1 = 9
     * 5:                 11
     * 6:                 13
     * 7:                 15
     * 8:                 17
     * 9:                 19
     * 10:                21
     */

多線程環境下操作會不會有問題呢? 出現了問題。我們看到使用常規操作的a變量出現了數據不一致情況。

實際上當循環的次數越多,出現錯誤的機率就越大,如下我們循環了1000次。

問題分析

我們思考爲什麼基本類型進行多線程操作時候回出現這種情況呢? 其實問題答案最開始已經說了。 我們通過這張圖
就可以找到原因。

對變量的每次操作其實都有3個步驟

  1. 讀取變量值
  2. 變量值操作
  3. 變量重新賦值。

我們模擬一下錯誤的原因。

當A線程讀取a=1,並對1+1。但是還未對變量重新賦值a=2的時候,
B線程也讀取了A還未賦值的變量,此時變量還是1,那麼B線程因爲讀取了還未更新的數據,所以也做1+1的操作。然後B對a
重新賦值了此時a=2,是B賦值的。這個時候A因爲已經執行完了前兩個步驟,最後也重新賦值了a=2。

這樣數據就更新丟了。這就是因爲數據更新不是原子性從而導致的問題。

因爲數據更新丟了,所以出現了。

如何解決這種問題

如何解決這種問題,其實很簡單隻要我們保證我們的操作是原子操作即可,簡單來說就是講更新的三個步驟合併成一個步驟即可,在Java中JDK已經爲我們提供了很多的
原子操作每一個基本類型都對應一個原子操作。

原子基礎類

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-hrW44BIR-1589381172662)(https://img.springlearn.cn/blog/learn_1589378016000.png)]

原子基礎類API

原子數組類

原子更新數組API

原子引用類

注意:

想要原子的更新字段,需要兩個步驟:

1.每次使用的時候必須使用靜態方法newUpdater()創建一個更新器,並且需要設置想要更新的類和屬性

2.更新類的字段(屬性)必須使用public volatile修飾符

最後我們看一下原子操作的原理

最後求關注,求訂閱,感謝您的閱讀,本文由 程序猿升級課 版權所有。如若轉載,請註明出處:程序猿升級課(https://blog.springlearn.cn/)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章