1. 程式人生 > >Java多執行緒之原子操作atomic的使用CAS(七)

Java多執行緒之原子操作atomic的使用CAS(七)

3-5、java.util.concurrent.atomic:執行緒安全的原子操作包

在JDK1.5+的版本中,Doug Lea和他的團隊還為我們提供了一套用於保證執行緒安全的原子操作。我們都知道在多執行緒環境下,對於更新物件中的某個屬性、更新基本型別資料、更新陣列(集合)都可能產生髒資料問題(如果您不清楚這個問題,請Google或者Baidu。這邊文章本身不討論髒資料產生的具體原因)。

為了避免多執行緒環境下的髒資料問題,JDK1.5的版本中為我們提供了java.util.concurrent.atomic原子操作包。所謂“原子”操作,是指一組不可分割的操作:操作者對目標物件進行操作時,要麼完成所有操作後其他操作者才能操作;要麼這個操作者不能進行任何操作;

java.util.concurrent.atomic原子操作包為我們提供了四類原子操作:原子更新基本型別,原子更新陣列,原子更新引用和原子更新欄位。靈活使用它們完全可以我們在日常工作中遇到的多執行緒資料髒讀問題。

這裡寫圖片描述

3-5-1、原子操作基本型別

AtomicBoolean:布林資料的原子操作 
AtomicInteger:整型數字的原子操作 
AtomicLong:長整型數字的原子操作

這裡我們首先使用AtomicInteger給出一段使用程式碼,讓各位讀者對基本型別的原子操作有一個感性的認識,然後再給出常用的API方法。基本的使用過程如下:

package test.thread.atomic;

import
java.util.concurrent.atomic.AtomicInteger; public class TestAtomic { public static void main(String[] args) throws Exception { // 例項化了一個AtomicInteger類的物件atomic並定義初始值為1 AtomicInteger atomic = new AtomicInteger(1); // 進行atomic的原子化操作:增加1並且獲取這個增加後的新值 atomic.incrementAndGet(); } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

在以上兩句程式碼中,我們看到了原子操作的基本使用。但是有的讀者要問了,這和index++有什麼不同嗎?最大的不同是:index++不是執行緒安全的。本文由於篇幅限制不過多介紹為什麼它不是執行緒安全的,這裡有一篇文章,詳細記錄了index++或者++index的處理過程,感興趣的讀者可以參考:http://www.importnew.com/17056.html

那麼我們重點分析一下AtomicInteger的原始碼,來看一下為什麼incrementAndGet()方法是怎麼做到原子性的(只列出相關部分的程式碼):

public class AtomicInteger extends Number implements java.io.Serializable {
    ......
    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();

    ......
    private volatile int value;
    ......

    /**
     * Gets the current value.
     *
     * @return the current value
     */
    public final int get() {
        return value;
    }   

    ......

    /**
     * Atomically increments by one the current value.
     * @return the updated value
     */
    public final int incrementAndGet() {
        for (;;) {
            int current = get();
            int next = current + 1;
            if (compareAndSet(current, next))
                return next;
        }
    }

    ......

    /**
     * Atomically sets the value to the given updated value
     * if the current value {@code ==} the expected value.
     * @param expect the expected value
     * @param update the new value
     * @return true if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

3-5-2、悲觀鎖和樂觀鎖

上一小節中給出的AtomicInteger原始碼,有部分讀者是不是感覺有點看不懂?是不是有幾個疑問在您心中:為什麼AtomicInteger不使用synchronized關鍵字就可以實現執行緒安全的原子性操作?為什麼incrementAndGet方法中居然還有一個死迴圈?

要解決這些疑問,我們首先就要介紹樂觀鎖和悲觀鎖以及JAVA對它的支援。悲觀鎖是一種獨佔鎖,它假設的前提是“衝突一定會發生”,所以處理某段可能出現數據衝突的程式碼時,這個程式碼段就要被某個執行緒獨佔。而獨佔意味著“其它即將執行這段程式碼的其他執行緒”都將進入“阻塞”/“掛起”狀態。是的,synchronized關鍵字就是java對於悲觀鎖的實現。

由於悲觀鎖的影響下,其他執行緒都將進入 阻塞/掛起 狀態。而我們在之前的文章中都已經講過,CPU執行執行緒狀態切換是要耗費相當資源的,這主要涉及到CPU暫存器的操作。所以悲觀鎖在效能上不會有太多驚豔的表現(但是也不至於成為效能瓶頸)

有悲觀鎖的存在當然就有樂觀鎖的存在。樂觀鎖假定“衝突不一定會出現”,如果出現衝突則進行重試,直到衝突消失。 由於樂觀鎖的假定條件,所以樂觀鎖不會獨佔資源,效能自然在多數情況下就會好於悲觀鎖。AtomicInteger是一個標準的樂觀鎖實現,sun.misc.Unsafe是JDK提供的樂觀鎖的支援。為什麼是多數情況呢?因為一旦多執行緒對某個資源的搶佔頻度達到了某種規模,就會導致樂觀鎖內部出現多次更新失敗的情況,最終造成樂觀鎖內部進入一種“活鎖”狀態。這時樂觀鎖的效能反而沒有悲觀鎖好。

您在incrementAndGet中,會看到有一個“死迴圈”,這是incrementAndGet方法中有“比較—重試”的需求。現在您明白了悲觀鎖和樂觀鎖的不同,那我們再次審視incrementAndGet方法中的程式碼(JDK1.7):

public final int incrementAndGet() {
    // 一直迴圈的目的是為了“預期值”與“真實值”不一致的情況下,
    // 能夠重新進行+1計算
    for (;;) {
        // 取得/重新取得 當前的value值
        int current = get();
        // 將當前值+1
        int next = current + 1;
        // 這是最關鍵的,使用JDK中實現的CAS機制
        // 對當前值和預期值進行比較
        // 如果當前值和預期的不一樣,說明有某一個其他執行緒完成了值的更改
        // 那麼進行下一次迴圈,進行重新操作(因為之前的操作結果就不對了)
        if (compareAndSet(current, next))
            return next;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

這就是整個利用樂觀鎖進行原子操作的過程。當然您在理解了這個過程後,就可以將樂觀鎖的支援直接運用到您的業務程式碼中,幫助改善效能了。祝賀您!

在程式碼中還有一個volatile關鍵字,volatile關鍵字用於修飾變數,執行緒在每次使用該變數時,都會讀取變數修改後的最的值。注意,如果只是使用volatile,也不足以保證資料操作的原子性

3-5-3、原子運算元組

AtomicIntegerArray:原子操作整型陣列 
AtomicLongArray:原子操作長整型陣列 
AtomicReferenceArray:原子操作物件引用陣列(後文會介紹物件引用的原子操作)

我們首先來看一看AtomicIntegerArray的基本使用。程式碼如下所示:

package test.thread.atomic;

import java.util.concurrent.atomic.AtomicIntegerArray;

public class TestAtomicArray {
    public static void main(String[] args) throws Exception {
        AtomicIntegerArray atomicArray = new AtomicIntegerArray(5);
        // 設定指定索引位的數值
        atomicArray.set(0, 5);

        // 您也可以通過以下方法設定
        //(實際上預設值為0,這裡加了5)
        // atomicArray.addAndGet(0, 5);

        // --
        int current = atomicArray.decrementAndGet(0);
        System.out.println("current = " + current);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

在程式碼中,我們使用addAndGet方法設定數字指定索引位的值;使用decrementAndGet方法將指定索引位的值減少1,並且取得最新值。

public class AtomicIntegerArray implements java.io.Serializable {
    ......
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final int base = unsafe.arrayBaseOffset(int[].class);
    ......

    private final int[] array;

    ......

    /**
     * Creates a new AtomicIntegerArray of the given length, with all
     * elements initially zero.
     *
     * @param length the length of the array
     */
    public AtomicIntegerArray(int length) {
        array = new int[length];
    }

    ......

    /**
     * Atomically decrements by one the element at index {@code i}.
     *
     * @param i the index
     * @return the updated value
     */
    public final int decrementAndGet(int i) {
        return addAndGet(i, -1);
    }

    ......

    public final int addAndGet(int i, int delta) {
        long offset = checkedByteOffset(i);
        while (true) {
            int current = getRaw(offset);
            int next = current + delta;
            if (compareAndSetRaw(offset, current, next))
                return next;
        }
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

如果您想檢視AtomicIntegerArray中的完整操作方式,可以檢視JDK的API文件,這裡的文章只給出一些主要的操作方式,以便您進行檢視:

  • get(int i):獲取陣列指定位置的值,並不會改變原來的值。
  • set(int i, int newValue):為陣列指定索引位設定一個新值。陣列的索引位都是從0開始計數。
  • getAndSet(int i, int newValue):獲取陣列指定位置的原始值後,用newValue這個新值進行覆蓋。
  • getAndAdd(int i, int delta):獲取陣列指定索引位的原始值後,為陣列指定索引位的值增加delta。那麼還有個類似的操作為:addAndGet。
  • incrementAndGet(int i):為陣列指定索引位的值+1後,然後獲取這個位置上的新值。當然,還有個類似的操作:getAndIncrement。
  • decrementAndGet(int i):為陣列指定索引位的值-1後,然後獲取這個位置上的新值。當然,類似的操作為:getAndDecrement。

和上文中我們介紹的AtomicInteger類相似,AtomicIntegerArray中的decrementAndGet方法(還有其他操作方法)也是樂觀鎖的一個應用

實際上不僅如此,在JDK1.5+中,Doug Lea和他的團隊為我們提供的執行緒安全的資料操作,基本上都是基於樂觀鎖的實現。包括(但不限於):java.util.concurrent.atomic包中的原子資料操作、java.util.concurrent包中的執行緒安全的資料結構等等。

3-5-4、原子操作物件欄位

  • AtomicIntegerFieldUpdater:整型資料欄位更新器
  • AtomicLongFieldUpdater:長型資料欄位更新器
  • AtomicReferenceFieldUpdater:物件資料欄位更新器
  • AtomicReference:物件原子操作

java.util.concurrent.atomic還為我們提供了進行物件(和物件中依賴)原子操作的方式。當然,同樣也似基於樂觀鎖。為了演示這樣的操作,我們首先要定義一個被操作的類,以便稍後對它進行例項化。

在這個示例程式中,我們定義了一個“學生”類:Student,並且為這個Student引入了一個“成績”類:Performance。我們先來看看這兩個類的定義:

/**
 * 代表學生的Student類
 * @author yinwenjie
 */
class Student {
    /**
     * 學生成績
     */
    private Performance performance;
    /**
     * 學生姓名
     */
    private String name;

    public Student(String name , Integer performance) {
        this.name = name;
        this.performance = new Performance();
        this.performance.setPerformance(performance);
    }

    /**
     * @return the performance
     */
    public Performance getPerformance() {
        return performance;
    }
    /**
     * @param performance the performance to set
     */
    public void setPerformance(Performance performance) {
        this.performance = performance;
    }
    /**
     * @return the name
     */
    public String getName() {
        return name;
    }
    /**
     * @param name the name to set
     */
    public void setName(String name) {
        this.name = name;
    }
}

/**
 * 代表著學生成績
 * @author yinwenjie
 */
class Performance {
    /**
     * 成績屬性是一個整數
     */
    private Integer performance;

    /**
     * @return the performance
     */
    public Integer getPerformance() {
        return performance;
    }

    /**
     * @param performance the performance to set
     */
    public void setPerformance(Integer performance) {
        this.performance = performance;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70

好了,我們來看看原子操作包是如何幫助我們進行多執行緒安全的物件(和物件引用)操作的。

首先我們例項化這個Student物件,然後使用AtomicReference對這個物件進行操作:

public static void main(String[] args) throws RuntimeException {
    Student student = new Student("yinwenjie" , 80);
    AtomicReference<Student> ref = new AtomicReference<Student>(student);

    student = new Student("yinwenjie" , 70);
    Student oldStudent = ref.getAndSet(student);

    System.out.println(student + "和" + oldStudent + "是兩個物件");
    System.out.println("AtomicReference保證了賦值時的原子操作性");
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

可以看出,我們使用AtomicReference對某一個物件的賦值過程進行了操作。但是很明顯,這絕對不是我們的目的。我們的目的是,保證student物件不變,只是改變student的成績屬性。所以,我們應當使用AtomicReferenceFieldUpdater

public class Student {
    ......
    /**
     * 學生成績
     */
    private volatile Performance performance;

    /**
     * 學生成績“更改者”
     */
    // 會重點講解關於“更改器”的引數問題
    private AtomicReferenceFieldUpdater<Student, Performance> performance_updater = 
    AtomicReferenceFieldUpdater.newUpdater(Student.class, Performance.class, "performance");
    ......

    /**
     * @return the performance
     */
    public Performance getPerformance() {
        return performance;
    }

    /**
     * @param performance the performance to set
     */
    public void setPerformance(Performance performance) {
        // 注意,這裡設定的是updater,而不是直接設定performance屬性
        performance_updater.set(this, performance);
    }

    ......
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • AtomicReferenceFieldUpdater.newUpdater這個靜態方法是為了建立一個新的“更新器”。其中的三個引數分別表示:持有要進行原子操作屬性的類、要進行原子操作的類和要進行原子操作的屬性的名稱。

  • 對於Student類來說,持有要進行原子操作屬性的類當然就是Student類本身;要進行原子操作的類當然就是Performance類;屬性名稱我們設定的名稱是“performance”。

  • 另外需要注意的是setPerformance方法。在這個方法中,我們不再直接設定performance引數,而是使用updater的set方法間接設定performance引數

下面,我們就來測試一下更改後的Student類的使用效果。首先看一下測試程式碼:

public static void main(String[] args) throws RuntimeException {

    Student student = new Student();
    Performance newPerformance = new Performance();
    newPerformance.setPerformance(80);
    // 注意,這樣student中的performance屬性
    // 就是用了樂觀機制,保證了操作的執行緒安全性
    student.setPerformance(newPerformance);

    // 再設定一次
    Performance otherPerformance = new Performance();
    otherPerformance.setPerformance(100);
    student.setPerformance(otherPerformance);
    System.out.println("student還是一個");
    System.out.println(newPerformance + "和" + otherPerformance + "不一樣了");
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16

以下是執行效果: