[Java高併發編程](二)原子類

本篇博客由本人根據衆多優秀的博客文章和書籍整理而來,參考的博客鏈接請看文章最下方,爲尊重參考博客的原創作者,特標爲轉載。

前言

  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

本系列博客目錄貼:

如有錯誤,歡迎指正,不勝感激!

[Java高併發編程](零)前言

參考鏈接

https://www.cnblogs.com/chengxiao/p/6789109.html
http://ifeve.com/java-atomic/
http://ifeve.com/atomic-operation/

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