【Java線程】線程安全三元素:原子性、可見性、有序性

目錄

定義 首先你們須要思考一下何爲線程安全性呢??java

《Java併發編程實戰》書中給出定義:當多個線程訪問某個類時,無論運行時環境採用何種調度方式或者這些線程將如何交替執行,而且在調用代碼中不須要任何額外的同步,這個類都能表現出正確的行爲,那麼這個類就是線程安全的。程序員

 對於線程安全性主要從如下幾個方面出發:原子性、有序性、可見性。數據庫

 原子性:提供互斥訪問,同一時刻只能有一個線程對數據進行操做; 例如:atomicXXX類,synchronized關鍵字的應用。  編程

有序性:一個線程觀察其餘線程中的指令執行順序,因爲指令重排序,該觀察結果通常雜亂無序;例如,happens-before原則。 安全

可見性:一個線程對主內存的修改能夠及時地被其餘線程看到;例如:synchronized,volatile。markdown

 原子性

 AtomicXxx 多線程

談起原子性確定離不開衆所周知的Atomic包,JDK裏面提供了不少atomic類,AtomicInteger,AtomicLong,AtomicBoolean等等。 併發

 以AtomicInteger爲例: app

class AtomicIntegerExample {
    private static final Logger log = LoggerFactory.getLogger(AtomicIntegerExample.class);
    // 請求總數
    public static int requestTotal = 500;
    // 併發執行的線程數
    public static int threadTotal = 20;

    public static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) throws Exception {
        ExecutorService executorService = Executors.newCachedThreadPool();//獲取線程池
        final Semaphore semaphore = new Semaphore(threadTotal);//定義信號量
        final CountDownLatch countDownLatch = new CountDownLatch(requestTotal);
        for (int i = 0; i < requestTotal ; i++) {
            executorService.execute(() -> {
                try {
                    semaphore.acquire();
                    add();
                    semaphore.release();
                } catch (Exception e) {
                    log.error("exception", e);
                }
                countDownLatch.countDown();
            });
        }
        countDownLatch.await();
        executorService.shutdown();
        log.info("count:{}", count.get());
    }

    private static void add() {
        count.incrementAndGet();
    }
}
複製代碼

跟着這個Demo,試着debuge一下,看下底層如何實現的??? 關鍵方法:incrementAndGet()oop

/**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
複製代碼

 AtomicInteger中的incrementAndGet方法就是樂觀鎖的一個實現,使用自旋(循環檢測更新)的方式來更新內存中的值並經過底層CPU執行來保證是更新操做是原子操做。

 使用自旋鎖機制便會形成何種問題呢???

 若是長時間自旋不成功,則會給CPU帶來很是大的執行開銷。  

隨之咱們跟進getAndAddInt方法,即魔法類UnSafe,關於此類,後期小編抽時間也會整理出一篇文章出來,敬請期待吧,,,哈哈~ 

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;
   }
複製代碼

 你們先分析一下這個方法的代碼結構:do-while(),而後再理解執行邏輯。 

首先經過調用getIntVolatile()方法,使用對象的引用與值的偏移量獲得當前值,而後調用compareAndSwapInt檢測若是obj內的value和expect相等,就證實沒有其餘線程改變過這個變量,那麼就更新它爲update,若是這一步的CAS沒有成功,那就採用自旋的方式繼續進行CAS操做。 

 對於上面的方法參數須要特殊解釋一下,要否則真的會很懵逼:

 compareAndSwapInt()但願達到的目標是對於var1對象,若是當前的值var2和底層的值var5相等,那麼把它更新成後面的值(var5+var4).  

但願你們可以理解清楚,更重要的是小編不要理解錯誤了,若是存在問題,但願大佬私信不當之處,及時改正。  

原子性底層實現核心思想是:CAS,可是CAS中存在ABA問題。  

compareAndSet是首先檢查當前引用是否等於預期引用,而且當前標誌是否等於預期標誌,若是所有相等,則以原子方式將該引用和該標誌的值設置爲給定的更新值。  

何爲ABA呢??? 

若是一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,可是實際上卻變化了。這就是CAS的ABA問題。 

那面對ABA問題,你們是想着如何解決呢???能夠思考一下數據庫中樂觀鎖機制,版本號。 故JDK引出AtomicStampedReference…  

AtomicStampedReference 

先看下這個類的方法,你們要注意翻譯註釋,理解各個參數的含義

/**
     * Atomically sets the value of both the reference and stamp
     * to the given update values if the
     * current reference is {@code ==} to the expected reference
     * and the current stamp is equal to the expected stamp.
     *
     * @param expectedReference the expected value of the reference
     * @param newReference the new value for the reference
     * @param expectedStamp the expected value of the stamp
     * @param newStamp the new value for the stamp
     * @return {@code true} if successful
     */
    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)));
    }
複製代碼

此方法會檢查當前引用是否等於預期引用,而且當前標誌是否等於預期標誌; 

若是所有相等,則以原子方式將該引用和該標誌的值設置爲給定的更新值。

 可見性 

簡單劃下重點: 什麼是線程間的可見性? 

一個線程對共享變量值的修改,可以及時的被其餘線程看到。

 什麼是共享變量?

 若是一個變量在多個線程的工做內存中都存在副本,那麼這個變量就是這幾個線程的共享變量。 

 什麼是java內存模型?

(Java Memory Model,簡稱JMM) JMM描述了java程序中各類變量(線程共享變量)的訪問規則,以及在JVM中將變量存儲到內存和從內存中讀取出變量這樣的底層細節。

 規則1: 

1>全部的變量都存儲在主內存中

 2>每一個線程都有本身獨立的工做內存,裏面保存該線程使用到的變量的副本(主內存中該變量的一份拷貝)

 規則2: 

1>線程對共享變量的全部操做都必須在本身的工做內存中進行,不能直接從主內存中讀寫 

2>不一樣線程之間沒法直接訪問其餘線程工做內存中的變量,線程間變量的傳遞須要經過主內存來完成。 

有序性

 有序性是指程序在執行的時候,程序的代碼執行順序和語句的順序是一致的。 

爲何會出現不一致的狀況呢?—重排序 

在Java內存模型中,容許編譯器和處理器對指令進行重排序,可是重排序過程不會影響到單線程程序的執行,卻會影響到多線程併發執行的正確性。

 對於有序性,小編以前讀過周志明的《深刻理解Java虛擬機》書中是這樣介紹有序性的: Happends-Before原則  

1. 程序次序規則:一個線程內,按照代碼順序,書寫在前面的操做先行發生於書寫在後面的操做;
2.鎖定規則:一個unLock操做先行發生於後面對同一個鎖lock操做;
3.volatile變量規則:對一個變量的寫操做先行發生於後面對這個變量的讀操做;
4.傳遞規則:若是操做A先行發生於操做B,而操做B又先行發生於操做C,則能夠得出操做A先行發生於操做C;
5.線程啓動規則:Thread對象的start()方法先行發生於此線程的每一個一個動做;
6.線程中斷規則:對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生;
7.線程終結規則:線程中全部的操做都先行發生於線程的終止檢測,咱們能夠經過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經終止執行;
8.對象終結規則:一個對象的初始化完成先行發生於他的finalize()方法的開始;
複製代碼

對於線程的可見性和有序性的理解,須要創建Java內存模型在基礎上理解和思考,雖然理解起來有點抽象,每次讀到系列文章,都是能收穫不一樣的知識點,書讀百遍其義自見,哈哈,,,繼續加油吧!!!向每一位正在努力的程序員致敬!!! 

出處:blog.csdn.net/xuan\_lu/ar…?

看完四件事❤️

若是你以爲這篇內容對你還蠻有幫助,我想邀請你幫我三個小忙:

  1. 點贊,轉發,有大家的 『點贊和評論』,纔是我創造的動力。

  2. 關注公衆號 『 JavaAC 』,不按期分享原創知識。

  3. 同時能夠期待後續文章ing🚀

  4. .關注後掃碼便可獲取學習資料包

相關文章
相關標籤/搜索