java架構之路(多線程)原子操作,Atomic與Unsafe魔術類

  這次不講原理了,主要是一些應用方面的知識,和上幾次的JUC併發編程的知識點更容易理解.

知識回顧:

  上次主要說了Semaphore信號量的使用,就是一個票據的使用,我們舉例了看3D電影拿3D眼鏡的例子,還說了內部的搶3D眼鏡,和後續排隊的源碼解析,還有CountDownLatch的使用,我們是用王者農藥來舉例的,CyclicBarrier柵欄的使用和CountDownLatch幾乎是一致的,Executors用的很少我只是簡單的寫了一個小示例。上次遺漏了一個CountDownLatch和CyclicBarrier的區別。

CountDownLatch和CyclicBarrier的區別:

  區別的根本在於有無主線程參與,這樣就很容易區別了,CountDownLatch有主線程,CyclicBarrier沒有主線程,我們來舉兩個例子,CountDownLatch主線程是遊戲程序,而我們開啓的10個線程是玩家加載程序,我們的遊戲主程序會等待10個玩家加載完成,線程可能結束,然後主程序遊戲程序繼續運行。CyclicBarrier沒有主線程,但是具有重複性,再舉一個例子,年會了,公司團建活動,三人跨柵欄,要求是必須三人全部跨過柵欄以後纔可以繼續跨下一個柵欄。

  CountDownLatch和CyclicBarrier都有讓多個線程等待同步然後再開始下一步動作的意思,但是CountDownLatch的下一步的動作實施者是主線程,具有不可重複性;而CyclicBarrier的下一步動作實施者還是“其他線程”本身,具有往復多次實施動作的特點。

本次新知識

  什麼是原子操作?

  原子(atom)本意是“不能被進一步分割的小粒子”,而原子操作(atomic  operation)意爲”不可被中斷的一個或一系列操作” 。就像是我們的mysql裏面的提到的ACID,原子性,也是不可分割的操作,最小的單位。

  我們以前說的MESI,說到了緩存行,也是上鎖的最小單位,原子變更就不做過多解釋了,就是把一個變量的值改爲另外一個值。比較與交換我們在Semaphore源碼裏也接觸過了,也就是CAS操作需要輸入兩個數值,一箇舊值,一個新值,在將要變更爲新值之前,會比較舊值是否已經改變,如果改變了修改失敗,如果沒有改變,修改成功。

  Atomic的使用

  在Atomic包內一共有12個類,四種原子更新方式,原子更新基本類型,原子更新數組,原子更新字段,Atomic包裏的類基本都是基於Unsafe實現的包裝類。

  

   基本類型:AtomicInteger,AtomicBoolean,AtomicLong。

   引用類型:AtomicReference、AtomicReference的ABA實例、AtomicStampedReference、AtomicMarkableReference。

  數組類型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray。

  屬性原子修改器:AtomicLongFieldUpdater、AtomicReferenceFieldUpdater、AtomicIntegerFieldUpdater。

  來一個簡單的實例,就是開啓10個線程然後做一個自加的操作,還是很好理解的。

public class AtomicIntegerTest {
    static AtomicInteger atomicInteger = new AtomicInteger();

    public static void main(String[] args) {
        for (int i = 0; i<10; i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    atomicInteger.incrementAndGet();
                }
            }).start();
        }

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("自加10次數值:--->"+atomicInteger.get());
    }
}

   ABA問題,ABA這樣更能好理解一些,一眼就可以看出來A已經不是原來的A了,雖然值一樣,但是裏面的屬性變成了紅色的,先來看一段代碼。

package com.xiaocai.main;

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerTest {
    static AtomicInteger atomicInteger = new AtomicInteger(1);
    public static void main(String[] args) {
        Thread main = new Thread(new Runnable() {
            @Override
            public void run() {
                int a = atomicInteger.get();
                System.out.println("操作線程"+Thread.currentThread().getName()+",修改前操作數值:"+a);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                boolean isCasSuccess = atomicInteger.compareAndSet(a,2);
                if(isCasSuccess){
                    System.out.println("操作線程"+Thread.currentThread().getName()+",Cas修改後操作數值:"+atomicInteger.get());
                }else{
                    System.out.println("CAS修改失敗");
                }

            }
        },"主線程");

        Thread other = new Thread(new Runnable() {
            @Override
            public void run() {
                atomicInteger.incrementAndGet();// 1+1 = 2;
                System.out.println("操作線程"+Thread.currentThread().getName()+",自加後值:"+atomicInteger.get());
                atomicInteger.decrementAndGet();// atomic-1 = 2-1;
                System.out.println("操作線程"+Thread.currentThread().getName()+",自減後值:"+atomicInteger.get());
            }
        },"干擾線程");
        main.start();
        other.start();
    }
}

  我們可以看到主線程設置一個初始值爲1,然後進行等待,干擾線程將1修改爲2,又將2修改回1,然後主線程繼續操作1修改爲2,這一系列的動作,主線程並沒有感知到1已經不是原來的1了。

   這樣的操作其實是很危險的,我們假象,小王是銀行的職員,他可以操作每個賬戶的金額(假設啊,具體能不能我也不知道),他將撕蔥的賬戶轉走了1000萬用於炒股,股市大漲,小王賺了2000萬,還了1千萬,自己還剩下2千萬,過幾天撕蔥來查看自己賬戶錢並沒有少,但是錢已經不是那個錢了,有人動過的。所以ABA問題我們還是要想辦法來處理的。我們每次轉賬匯款的操作都是有一個流水號(回執單)的,也就是每次我們加一個版本號碼就可以了,我們來改一下代碼。

public class AtomicIntegerTest {
    static AtomicStampedReference atomicInteger = new AtomicStampedReference<>(1,0);
    public static void main(String[] args) {
        Thread main = new Thread(new Runnable() {
            @Override
            public void run() {
                int stamp = atomicInteger.getStamp(); //獲取當前標識別
                System.out.println("操作線程"+Thread.currentThread().getName()+"修改前的版本號爲:"+stamp+",修改前操作數值:"+atomicInteger.getReference());
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                boolean isCasSuccess = atomicInteger.compareAndSet(1,2,stamp,stamp +1);  //此時expectedReference未發生改變,但是stamp已經被修改了,所以CAS失敗
                if(isCasSuccess){
                    System.out.println("操作線程"+Thread.currentThread().getName()+",Cas修改後操作數值:"+atomicInteger.getReference());
                }else{
                    System.out.println("CAS修改失敗,當前版本爲:"+atomicInteger.getStamp());
                }

            }
        },"主線程");

        Thread other = new Thread(new Runnable() {
            @Override
            public void run() {
                int stamp = atomicInteger.getStamp();
                atomicInteger.compareAndSet(1,2,atomicInteger.getStamp(),atomicInteger.getStamp()+1);
                System.out.println("操作線程"+Thread.currentThread().getName()+",版本號爲:"+stamp+",修改後的版本號爲:"+atomicInteger.getStamp()+",自加後值:"+atomicInteger.getReference());

                int newStamp = atomicInteger.getStamp();
                atomicInteger.compareAndSet(2,1,atomicInteger.getStamp(),atomicInteger.getStamp()+1);
                System.out.println("操作線程"+Thread.currentThread().getName()+",版本號爲:"+newStamp+",修改後的版本號爲:"+atomicInteger.getStamp()+",自減後值:"+atomicInteger.getReference());

            }
        },"干擾線程");
        main.start();
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        other.start();
    }
}

  我們先初始一個主線程,並且設置版本號爲0。然後干擾線程進行修改,每次修改時版本號加一,干擾線程結束,而主線程想繼續修改時,發現版本不匹配,修改失敗。

  其餘Atomic的類使用都是大同小異的,可以自行嘗試一遍。

  Unsafe魔術類的使用

   Unsafe是位於sun.misc包下的一個類,主要提供一些用於執行低級別、不安全操作的 方法,如直接訪問系統內存資源、自主管理內存資源等,這些方法在提升Java運行效率、增 強Java語言底層資源操作能力方面起到了很大的作用。但由於Unsafe類使Java語言擁有了 類似C語言指針一樣操作內存空間的能力,這無疑也增加了程序發生相關指針問題的風險。 在程序中過度、不正確使用Unsafe類會使得程序出錯的概率變大,使得Java這種安全的語 言變得不再“安全”,因此對Unsafe的使用一定要慎重。

  在過去的幾篇博客裏也說到了Unsafe這個類,我們需要通過反射來使用它,比如讀寫屏障、加鎖解鎖,線程的掛起操作等等。

   如何獲取Unsafe實例?

  1、從getUnsafe方法的使用限制條件出發,通過Java命令行命令-Xbootclasspath/a把 調用Unsafe相關方法的類A所在jar包路徑追加到默認的bootstrap路徑中,使得A被 引導類加載器加載,從而通過Unsafe.getUnsafe方法安全的獲取Unsafe實例。 java ­Xbootclasspath/a:${path}   // 其中path爲調用Unsafe相關方法的類所在jar包路徑。 

  2、通過反射獲取單例對象theUnsafe。 

public static Unsafe reflectGetUnsafe() {
    try {
        Field field = Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        return (Unsafe) field.get(null);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return null;
}

總結:

  這次博客完全沒有代碼的解析閱讀,都是一些簡單的使用,我們開始時候說到了什麼是原子操作,接下來我們說了Atomic類的基本使用,再就是什麼是ABA問題,如何用Atomic來解決ABA問題,再就是我們的魔術類Unsafe類,越過虛擬機直接來操作我們的系統的一些操作(不是超級熟練別玩這個,玩壞了不好修復)。希望對大家在工作面試中能有一些幫助。

 

 

最進弄了一個公衆號,小菜技術,歡迎大家的加入

 

 

 

 

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