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

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

知識回顧:java

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

CountDownLatch和CyclicBarrier的區別:面試

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

  CountDownLatch和CyclicBarrier都有讓多個線程等待同步而後再開始下一步動做的意思,可是CountDownLatch的下一步的動做實施者是主線程,具備不可重複性;而CyclicBarrier的下一步動做實施者仍是「其餘線程」自己,具備往復屢次實施動做的特色。編程

本次新知識bootstrap

  什麼是原子操做?數組

  原子(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實例?

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

  二、經過反射獲取單例對象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類,越過虛擬機直接來操做咱們的系統的一些操做(不是超級熟練別玩這個,玩壞了很差修復)。但願對你們在工做面試中能有一些幫助。

 

 

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

 

相關文章
相關標籤/搜索