Java多線程之原子操作類


在並發編程中很容易出現並發安全問題,最簡單的例子就是多線程更新變量i=1,多個線程執行i++操作,就有可能獲取不到正確的值,而這個問題,最常用的方法是通過Synchronized進行控制來達到線程安全的目的。但是由於synchronized是采用的是悲觀鎖策略,並不是特別高效的一種解決方案。實際上,在J.U.C下的Atomic包提供了一系列的操作簡單,性能高效,並能保證線程安全的類去更新多種類型。Atomic包下的這些類都是采用樂觀鎖策略CAS來更新數據。

CAS原理與問題

CAS操作(又稱為無鎖操作)是一種樂觀鎖策略。它假設所有線程訪問共享資源的時候不會出現沖突,因此不會阻塞其他線程的操作。那么,如果出現沖突了怎么辦?無鎖操作是使用CAS(compare and swap)來鑒別線程是否出現沖突,出現沖突就重試當前操作直到沒有沖突為止。

CAS的操作過程

舉例說明:
Atomic包中的AtomicInteger類,是通過Unsafe類下的native函數compareAndSwapInt自旋來保證原子性,
其中incrementAndGet函數調用的getAndAddInt函數如下所示:

public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

CAS有3個操作數,內存值V,舊的預期值A,要修改的新值B。當且僅當預期值A和內存值V相同時,將內存值V修改為B,否則什么都不做。
可見只有自旋實現更新數據操作之后,while循環才能夠結束。

CAS的問題

  1. 自旋時間過長。由compareAndSwapInt函數可知,自旋時間過長會對性能是很大的消耗。
  2. ABA問題。因為CAS會檢查舊值有沒有變化,這里存在這樣一個有意思的問題。比如一個舊值A變為了成B,然后再變成A,剛好在做CAS時檢查發現舊值並沒有變化依然為A,但是實際上的確發生了變化。解決方案可以添加一個版本號可以解決。原來的變化路徑A->B->A就變成了1A->2B->3C,或使用AtomicStampedReference工具類。

Atomic包的使用

原子更新基本類型

Atomic包中原子更新基本類型的工具類:
AtomicBoolean:以原子更新的方式更新boolean;
AtomicInteger:以原子更新的方式更新Integer;
AtomicLong:以原子更新的方式更新Long;

這幾個類的用法基本一致,這里以AtomicInteger為例總結常用的方法

  1. addAndGet(int delta):以原子方式將輸入的數值與實例中原本的值相加,並返回最后的結果;
  2. incrementAndGet() :以原子的方式將實例中的原值進行加1操作,並返回最終相加后的結果;
  3. getAndSet(int newValue):將實例中的值更新為新值,並返回舊值;
  4. getAndIncrement():以原子的方式將實例中的原值加1,返回的是自增前的舊值;

原理不再贅述,參考上文compareAndSwapInt函數。

AtomicInteger使用示例:

public class AtomicExample {

    private static AtomicInteger atomicInteger = new AtomicInteger(2);

    public static void main(String[] args) {
        System.out.println(atomicInteger.getAndIncrement());
        System.out.println(atomicInteger.incrementAndGet());
        System.out.println(atomicInteger.get());
    }
}
// 2 4 4

LongAdder

為了解決自旋導致的性能問題,JDK8在Atomic包中推出了LongAdder類。LongAdder采用的方法是,共享熱點數據分離的計數:將一個數字的值拆分為一個數組。不同線程會命中到數組的不同槽中,各個線程只對自己槽中的那個值進行CAS操作,這樣熱點就被分散了,沖突的概率就小很多;要得到這個數字的話,就要把這個值加起來。相比AtomicLong,並發量大大提高。

優點:有很高性能的並發寫的能力
缺點:讀取的性能不是很高效,而且如果讀取的時候出現並發寫的話,結果可能不是正確的

原子更新數組類型

Atomic包中提供能原子更新數組中元素的工具類:
AtomicIntegerArray:原子更新整型數組中的元素;
AtomicLongArray:原子更新長整型數組中的元素;
AtomicReferenceArray:原子更新引用類型數組中的元素

這幾個類的用法一致,就以AtomicIntegerArray來總結下常用的方法:

  1. addAndGet(int i, int delta):以原子更新的方式將數組中索引為i的元素與輸入值相加;
  2. getAndIncrement(int i):以原子更新的方式將數組中索引為i的元素自增加1;
  3. compareAndSet(int i, int expect, int update):將數組中索引為i的位置的元素進行更新

AtomicIntegerArray與AtomicInteger的方法基本一致,只不過在前者的方法中會多一個指定數組索引位i。

AtomicIntegerArray使用示例:

public class AtomicExample {

    private static int[] value = new int[]{1, 2, 3};
    private static AtomicIntegerArray integerArray = new AtomicIntegerArray(value);

    public static void main(String[] args) {
        //對數組中索引為2的位置的元素加3
        int result = integerArray.getAndAdd(2, 3);
        System.out.println(integerArray.get(2));
        System.out.println(result);
    }
}
// 6 3

原子更新引用類型

如果需要原子更新引用類型變量的話,為了保證線程安全,Atomic也提供了相關的類:

  1. AtomicReference :原子更新引用類型;
  2. AtomicReferenceFieldUpdater:原子更新引用類型里的字段;
  3. AtomicMarkableReference:原子更新帶有標記位的引用類型;

AtomicReference使用示例:

public class AtomicExample {

    private static AtomicReference<User> reference = new AtomicReference<>();

    public static void main(String[] args) {
        User user1 = new User("a", 1);
        reference.set(user1);
        User user2 = new User("b",2);
        User user = reference.getAndSet(user2);
        System.out.println(user);
        System.out.println(reference.get());
    }

    static class User {
        private String userName;
        private int age;

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

        @Override
        public String toString() {
            return "User{" +
                    "userName='" + userName + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
}
// User{userName='a', age=1}
// User{userName='b', age=2}

AtomicReferenceFieldUpdater使用示例:

public class AtomicExample {

    public static void main(String[] args) {
        AtomicReferenceFieldUpdater updater = AtomicReferenceFieldUpdater.newUpdater(Dog.class, String.class, "name");
        Dog dog1 = new Dog();
        updater.compareAndSet(dog1, dog1.name, "cat");
        System.out.println(dog1.name);
    }
}

class Dog {
    volatile String name = "dog1";
}

原子更新字段類型

如果需要更新對象的某個字段,Atomic同樣也提供了相應的原子操作類:

  1. AtomicIntegeFieldUpdater:原子更新整型字段類;
  2. AtomicLongFieldUpdater:原子更新長整型字段類;

要想使用原子更新字段需要兩步操作:
原子更新字段類型類都是抽象類,只能通過靜態方法newUpdater來創建一個更新器,並且需要設置想要更新的類和屬性;
更新類的屬性必須使用public volatile進行修飾;

AtomicIntegerFieldUpdater使用示例:

public class AtomicExample {

    private static AtomicIntegerFieldUpdater updater = AtomicIntegerFieldUpdater.newUpdater(User.class, "age");

    public static void main(String[] args) {
        User user = new User("a", 1);
        System.out.println(updater.getAndAdd(user, 5));
        System.out.println(updater.addAndGet(user, 1));
        System.out.println(updater.get(user));
    }

    static class User {
        private String userName;
        public volatile int age;

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

        @Override
        public String toString() {
            return "User{" +
                    "userName='" + userName + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
}

解決CAS的ABA問題

AtomicStampedReference:原子更新引用類型,這種更新方式會帶有版本號,從而解決CAS的ABA問題

AtomicStampedReference使用示例:

public class AtomicExample {

    public static void main(String[] args) {
        Integer init1 = 1110;
//        Integer init2 = 126;
        AtomicStampedReference<Integer> reference = new AtomicStampedReference<>(init1, 1);
        int curent1 = reference.getReference();
//        Integer current2 = reference.getReference();
        reference.compareAndSet(reference.getReference(), reference.getReference() + 1, reference.getStamp(), reference.getStamp() + 1);//正確寫法
//        reference.compareAndSet(current2, current2+1, reference.getStamp(), reference.getStamp() + 1);//正確寫法
//        reference.compareAndSet(1110, 1111, reference.getStamp(), reference.getStamp() + 1);//錯誤寫法
//        reference.compareAndSet(curent1, curent1+1, reference.getStamp(), reference.getStamp() + 1);//錯誤寫法
//        reference.compareAndSet(current2, current2 + 1, reference.getStamp(), reference.getStamp() + 1);
        System.out.println("reference.getReference() = " + reference.getReference());
    }
}

AtomicStampedReference踩過的坑

參考上面的代碼,分享一個筆者遇到的一次坑。AtomicStampedReference的compareAndSet函數中,前兩個參數是使用包裝類的。所以當參數超過128時,而且傳入參數並不是reference.getReference()獲取的話,會導致expectedReference == current.reference為false,則無法進行更新。

public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&
            expectedStamp == current.stamp &&
            ((newReference == current.reference &&
              newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }

最后,限於筆者經驗水平有限,歡迎讀者就文中的觀點提出寶貴的建議和意見。如果想獲得更多的學習資源或者想和更多的是技術愛好者一起交流,可以關注我的公眾號『全菜工程師小輝』后台回復關鍵詞領取學習資料、進入前后端技術交流群和程序員副業群。同時也可以加入程序員副業群Q群:735764906 一起交流。

哎呀,如果我的名片丟了。微信搜索“全菜工程師小輝”,依然可以找到我


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM