JDK源碼閱讀,手寫AtomicInteger

關於java併發的三大特性,原子性、可見性、有序性關乎線程安全問題的基本原理,JDK提供java.util.concurrent.atomic包來對數據類型進行包裝,以實現各數據類型的原子性操作。
關於三大特性與volatile關鍵字可參考文章深入理解volatile關鍵字
接下來就通過一道簡單的題目層層解讀AtomicInteger的底層原理。

demo1:i++的原子性問題

public class AtomicIntegerDemo1 {
    private int value = 0;

    public void add() {
        value++;
    }

    public static void main(String[] args) throws InterruptedException {
        AtomicIntegerDemo1 atomicIntegerDemo1 = new AtomicIntegerDemo1();
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    atomicIntegerDemo1.add();
                }
            }).start();
        }
        Thread.sleep(1000);//等待所有線程執行完畢再輸出
        System.out.println(atomicIntegerDemo1.value);
    }
}

這是一道簡單的題目,創建20個線程,每個線程對共享變量value進行1000次自加操作。不論運行多少次,結果都不是我們期望的20000:
在這裏插入圖片描述
究其原因,假設現在value值爲1,線程1將變量value的值從堆空間(線程共享)讀取到虛擬機棧(線程私有)中進行自加操作value值變爲2,再將新值2刷新到堆內存。這個過程中可能線程1還沒來得及刷新到堆內存,線程2也讀取value值到自己的棧內存,然後將新值2刷新到堆內存。這樣兩個線程對變量value一共自加兩次,value值應當是3,而實際value的值仍然是2。這就是線程安全問題。本質原因還是因爲value++操作是非原子操作。
要實現線程安全方法有兩種:

  • 同步阻塞:通過sychronized關鍵字或者Lock對象加鎖,public void sychronized add() { value++; },保證add()方法的原子性。同步阻塞方式優點是安全,實現簡單,缺點是併發效率低。
  • 非阻塞併發:通過CAS(比較並交換)機制實現樂觀鎖,只有噹噹前值與期望值一樣時,纔將舊值替換爲當前值,否則重新讀取當前值,這種循環重試機制也叫自旋鎖。異步併發的優點是性能高,CPU利用率高,線程持續運行,省去了CPU線程調度的性能消耗,缺點是如果當前值一直不與期望值一樣,將會一直循環重試。樂觀鎖還可能出現ABA問題。

以上代碼的運行結果並不會是我們期望的20000。將add()方法設置爲同步方法可以解決問題,那再來看看AtomicInteger如何解決原子性問題的。
注意:以上代碼中給value屬性添加volatile關鍵字並不能解決問題,因爲volatile關鍵字只有保證可見性和有序性的語義,而不能保證原子性。

demo2:使用AtomicInteger類

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerDemo2 {
    private AtomicInteger value = new AtomicInteger();

    public void add() {
        value.getAndAdd(1);
    }

    public static void main(String[] args) throws InterruptedException {
        AtomicIntegerDemo2 atomicIntegerDemo2 = new AtomicIntegerDemo2();
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    atomicIntegerDemo2.add();
                }
            }).start();
        }
        Thread.sleep(1000);//等待所有線程執行完畢再輸出
        System.out.println(atomicIntegerDemo2.value);
    }
}

用AtomicInteger類對象的getAndAdd()方法代替int類型的i++操作,最後輸出結果是我們期望的20000.
在這裏插入圖片描述
那麼,AtomicInteger是如何實現原子性運算的呢。點開jdk源碼
查看AtomicInteger的getAndAdd()方法:

public final int getAndAdd(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }

有一個unsafe對象,

private static final Unsafe unsafe = Unsafe.getUnsafe();

這個unsafe對象所屬類Unsafe提供了許多native方法,比如

public native int getIntVolatile(Object var1, long var2);

用getIntVolatile()在底層通過對象(var1)和偏移量(var2)獲取變量內存地址,直接通過內存地址而不是符號引用得到變量的當前值。有了對象(var1),屬性地址偏移量(var2),舊值(var5 ),加上期望值(var5 + var4),就可以調用CAS操作(compareAndSwapInt方法)。unsafe對象的getAndAddInt方法如下:

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

噹噹前線程執行第4行後,變量值被其他線程刷新,則當前線程執行第5行方法返回false,重新執行第4行獲取值,直到當前值與期望值一樣,才修改變量的值並返回新值。
看完了源碼想仿造源碼自己動手寫一下AtomicInteger以加深印象。

demo3:仿造源碼實現的AtomicInteger類

import sun.misc.Unsafe;

public class AtomicIntegerDemo3 {
    private volatile int value;
    //提供底層CAS操作的對象
    private static final Unsafe unsafe=Unsafe.getUnsafe();
    //屬性在內存中相對於對象的地址偏移量
    private static final long valueOffset;
    static{
        try {
            //通過unsafe對象提供的方法獲取value屬性偏移量
            valueOffset = unsafe.objectFieldOffset(AtomicIntegerDemo3.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    /**
     * CAS操作方法
     * @param current 舊值
     * @param update 期望值
     * @return 修改是否成功
     */
    public boolean compareAndSet(int current,int update){
        return unsafe.compareAndSwapInt(this,valueOffset,current,update);
    }

    //相當於AtomicInteger中getAndAdd()方法:將給定的值原子地添加到當前值
    public void add() {
        int current;
        do {
            //線程獲取當前值
            current = unsafe.getIntVolatile(this,valueOffset);
        }while (!compareAndSet(current,current+1));//修改失敗則重試
    }

    public static void main(String[] args) throws InterruptedException {
        AtomicIntegerDemo3 atomicIntegerDemo3 = new AtomicIntegerDemo3();
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    atomicIntegerDemo3.add();
                }
            }).start();
        }
        Thread.sleep(1000);//等待所有線程執行完畢再輸出
        System.out.println(atomicIntegerDemo3.value);
    }
}

想法很簡單,拿到底層內存地址操作相關的底層Unsafe類的實例,根據本類的value屬性用unsafe對象的objectFieldOffset()方法獲取屬性相對本類實例的地址偏移量,然後通過循環CAS操作改變屬性值。
但是運行時報了異常:
在這裏插入圖片描述
第8行private static final Unsafe unsafe=Unsafe.getUnsafe();報錯,點開getUnsafe()方法,

	@CallerSensitive
    public static Unsafe getUnsafe() {
        Class var0 = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
            throw new SecurityException("Unsafe");
        } else {
            return theUnsafe;
        }
    }

再點開VM.isSystemDomainLoader()方法:

public static boolean isSystemDomainLoader(ClassLoader var0) {
        return var0 == null;
    }

可以看出爲直接調用getUnsafe()方法是不安全的,sunjdk設定只要調用類類加載器不爲空,則拋出異常並提示Unsafe,也就是該方法只能虛擬機自己調用。
既然我們不能直接調用getUnsafe()方法來獲取Unsafe的實例,那就用反射來獲取吧。

demo4:手寫AtomicInteger

import sun.misc.Unsafe;
import java.lang.reflect.Field;

public class AtomicIntegerDemo4 {
    private volatile int value;
    //提供底層CAS操作的對象
    private static final Unsafe unsafe;
    //屬性在內存中相對於對象的地址偏移量
    private static final long valueOffset;
    static{
        try {
            //getUnsafe()方法不好使,通過反射獲取unsafe對象
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            //Unsafe類中的theUnsafe屬性爲private,設置可訪問權限
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);
            //通過unsafe對象提供的方法獲取value屬性偏移量
            valueOffset = unsafe.objectFieldOffset
                    (AtomicIntegerDemo4.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

    /**
     * CAS操作方法
     * @param current 舊值
     * @param update 期望值
     * @return 修改是否成功
     */
    public boolean compareAndSet(int current,int update){
        return unsafe.compareAndSwapInt(this,valueOffset,current,update);
    }

    //相當於AtomicInteger中getAndAdd()方法
    public void add() {
        int current;
        do {
            //線程獲取當前值
            current = unsafe.getIntVolatile(this,valueOffset);
        }while (!compareAndSet(current,current+1));//修改失敗則重試
    }

    public static void main(String[] args) throws InterruptedException {
        AtomicIntegerDemo4 atomicIntegerDemo4 = new AtomicIntegerDemo4();
        for (int i = 0; i < 20; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    atomicIntegerDemo4.add();
                }
            }).start();
        }
        Thread.sleep(1000);//等待所有線程執行完畢再輸出
        System.out.println(atomicIntegerDemo4.value);
    }
}

修改過後通過反射獲取unsafe對象,運行成功,能夠正確輸出20000:
在這裏插入圖片描述
雖然只是完成了getAndAdd()一個方法,但是通過閱讀源碼和仿造手寫對Atomic原子類的原理有了一定的瞭解。
另外,測試發現,把value屬性的volatile關鍵字去掉也不影響結果,jdk源碼添加的關鍵字應該是不會多餘的,猜測是與unsafe.getIntVolatile()這個方法有關,但是這是個native方法不能查看。
望有大神不吝賜教0.0

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