1. 程式人生 > 其它 >Java原子類

Java原子類

技術標籤:Java多執行緒

1.為什麼需要原子類

當程式更新一個變數時,如果是多執行緒同時更新這個變數,可能得到的結果與期望值不同。比如:有一個變數i,A執行緒執行i+1,B執行緒也執行i+1,經過兩個執行緒的操作後,變數i的值可能不是期望的3,而是2。這是因為,可能在A執行緒和B執行緒執行的時候拿到的i的值都是1,這就是執行緒不安全的更新操作,通常我們會使用synchronized來解決這個問題,synchronized能保證多執行緒不會同時更新變數i.

從java1.5開始,jdk提供了java.util.concurrent.atomic包,這個包中的原子操作類,提供了一種用法簡單,效能高效,執行緒安全的更新一個變數的方式。

atomic包裡面一共提供了13個類,分為4種類型,分別是:原子更新基本型別,原子更新陣列,原子更新引用,原子更新屬性,這13個類都是使用Unsafe實現的包裝類。

2.原子更新基本型別類

atomic提供了3個類用於原子更新基本型別:分別是AtomicInteger原子更新整形,AtomicLong原子更新長整形,AtomicBoolean原子更新bool值。由於這三個類提供的方法幾乎是一樣的,我以AtomicInteger為例進行說明。

  AtomicInteger的常用方法有:

  • int addAndGet(int delta):以原子的方式將輸入的值與例項中的值相加,並把結果返回
  • boolean compareAndSet(int expect,int update):如果輸入值等於預期值,以原子的方式將該值設定為輸入的值
  • final int getAndIncrement():以原子的方式將當前值加1,並返回加1之前的值
  • void lazySet(int newValue):最終會設定成newValue,使用lazySet設定值後,可能導致其他執行緒在之後的一小段時間內還是可以讀到舊的值。
  • int getAndSet(int newValue):以原子的方式將當前值設定為newValue,並返回設定之前的舊值
public class AtomicIntegerTest {
    static AtomicInteger atomicInteger = new AtomicInteger(1);
    static int a = 1;

    public static void main(String[] args) {
        for (int i = 0; i < 10000; i++) {
            new Thread(() -> {
                atomicInteger.getAndIncrement();
            }).start();
        }
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("使用原子類的值:" + atomicInteger.get());

        for (int i = 0; i < 10000; i++) {
            new Thread(() -> {
                a++;
            }).start();
        }
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("不使用原子類的值:" + a);
    }
}

不使用原子類計算的值會有一些出入

AtomicInteger 類主要利用 CAS (compare and swap) + volatile 和 native 方法來保證原子操作,從而避免 synchronized 的高開銷,執行效率大為提升。

CAS的原理是拿期望的值和原本的一個值作比較,如果相同則更新成新的值。UnSafe 類的 objectFieldOffset() 方法是一個本地方法,這個方法是用來拿到“原來的值”的記憶體地址。另外 value 是一個volatile變數,在記憶體中可見,因此 JVM 可以保證任何時刻任何執行緒總能拿到該變數的最新值。

3.原子更新陣列

atomic裡提供了三個類用於原子更新數組裡面的元素,分別是:

  • AtomicIntegerArray:原子更新整形數組裡的元素
  • AtomicLongArray:原子更新長整形數組裡的元素
  • AtomicReferenceArray:原子更新引用數組裡的元素

因為每個類裡面提供的方法都一致,因此以AtomicIntegerArray為例來說明。AtomicIntegerArray主要提供了以原子方式更新數組裡的整數,常見方法如下:

  • int addAndGet(int i,int delta):以原子的方式將輸入值與陣列中索引為i的元素相加
  • boolean compareAndSet(int i,int expect,int update):如果當前值等於預期值,則以原子方式將陣列位置i的元素設定成update值。
public class AtomicIntegerArrayTest {
    static int[] value = new int[]{1, 2};
    static AtomicIntegerArray ai = new AtomicIntegerArray(value);

    public static void main(String[] args) {
        new Thread(() -> {
            ai.getAndSet(0, 3);
            //輸出3
            System.out.println(ai.get(0));
            //輸出1
            System.out.println(value[0]);
        }).start();

    }
}

需要注意的是,陣列value通過構造方法傳遞進去,然後AtomicIntegerArray會將當前陣列複製一份,所以當AtomicIntegerArray對內部的陣列元素進行修改時,不會影響傳入的陣列。

4.引用型別原子類

原子更新基本型別的AtomicInteger只能更新一個變數,如果要原子更新多個變數,就需要使用原子更新引用型別提供的類了。原子引用型別atomic包主要提供了以下幾個類:

  • AtomicReference:原子更新引用型別
  • AtomicReferenceFieldUpdater:原子更新引用型別裡的欄位
  • AtomicMarkableReference:原子更新帶有標記位的引用型別。可以原子更新一個布林型別的標記位和引用型別。構造方法是AtomicMarkableReference(V initialRef,booleaninitialMark)注意:AtomicMarkableReference是將一個boolean值作是否有更改的標記,本質就是它的版本號只有兩個,true和false, 修改的時候在這兩個版本號之間來回切換,這樣做並不能解決ABA的問題,只是會降低ABA問題發生的機率而已
public class AtomicReferenceArrayTest {
    private static AtomicReference<User> reference = new AtomicReference<>();

    public static void main(String[] args) {
        User user = new User("tom", 23);
        reference.set(user);
        User updateUser = new User("ketty", 34);
        reference.compareAndSet(user, updateUser);
        System.out.println(reference.get().getName());
        System.out.println(reference.get().getAge());

    }

    static class User {

        private String name;
        private int age;

        public User(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public String getName() {
            return name;
        }

        public int getAge() {
            return age;
        }
    }
}

5.原子更新欄位類

如果需要原子更新某個物件的某個欄位,就需要使用原子更新屬性的相關類,atomic中提供了一下幾個類用於原子更新屬性:

  • AtomicIntegerFieldUpdater:原子更新整形屬性的更新器
  • AtomicLongFieldUpdater:原子更新長整形的更新器
  • AtomicStampedReference:原子更新帶有版本號的引用型別。該類將整數值與引用關聯起來,可用於原子的更新資料和資料的版本號,可以解決使用CAS進行原子更新時可能出現的ABA問題。

  想要原子的更新欄位,需要兩個步驟:

  1.因為原子更新欄位類都是抽象類,每次使用的時候必須使用靜態方法newUpdater()建立一個更新器,並且需要設定想要更新的類和屬性

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

public class AtomicIntegerFieldUpdaterTest {

    //建立原子更新器
    private static AtomicIntegerFieldUpdater<User> updater = AtomicIntegerFieldUpdater.newUpdater(User.class,"age");

    public static void main(String[] args){
        User user = new User("ketty",21);
        //ketty長了一歲
        updater.getAndIncrement(user);
        //輸出22
        System.out.println(updater.get(user));
    }

    static class User{

        private String name;
        public volatile int age;

        public User(String name, int age) {
            this.name = name;
            this.age = age;
        }

        public String getName() {
            return name;
        }

        public int getAge() {
            return age;
        }
    }
}