1. 程式人生 > >一文看盡Java-多執行緒概念

一文看盡Java-多執行緒概念

一、前言

    主要講解一下多執行緒中的一些概念,本文之後就開始針對JUC包的設計開始解讀;

二、概念

    執行緒安全

    1.存在共享資料(臨界資源);2.多個執行緒同時操作共享資料;只有同時出現這兩種情況的時候才會造成執行緒安全問題;

    解決執行緒安全

    同一時刻有且只有一個執行緒在操作共享資料,其他執行緒必須等到該執行緒處理完資料以後在對共享資料進行操作;

    多執行緒特性

    原子性

    現在的作業系統主要是通過時間分片的形式來管理執行緒或者程序,Java程式語言一句語言需要多條CPU指令來完成,Java在多執行緒切換的時候由於不滿足原子性的特徵,導致共享變數產生意料之外的結果;典型的count+=1,如下圖,共享變數的count的指最終結果是1而不是2;

   

    可見性

    在多處理器(CPU)系統中,每個處理器都有自己的快取記憶體,而它們又共享同一主記憶體,整體結構如下圖:

    

    在單核CPU的情況下,CPU快取與記憶體資料一致性問題容易處理,因為所有執行緒的操作都是針對同一個CPU的快取,一個執行緒對快取的寫對於另外的執行緒一定是可見的,整體的執行情況如下圖:

    

    在多CPU的情況下,每個CPU都有自己的快取,這個時候每個共享變數在CPU中的快取都是不可見的,這個時候就產生了CPU快取與記憶體資料一致性的問題,整體執行的情況如下圖,由於count變數分別在不同的CPU上執行,相互看不到對方的操作,這個時候變數count就會不一致,產生意料之外的結果,針對這種我也寫了一個demo;

   

/**
 * @author wtz
 *
 * 執行緒可見性demo
 */
public class ThreadVisiable {

    private int count = 0;

    private void add() {
        int retry = 0;
        while (retry < 10000) {
            count += 1;
            retry++;
        }
    }

    public int sumCount() throws InterruptedException {

        Thread thread1 = new Thread(() -> {
            add();
        });

        Thread thread2 = new Thread(() -> {
            add();
        });

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();

        return count;
    }

    public static void main(String[] args) throws InterruptedException {
        ThreadVisiable threadVisiable = new ThreadVisiable();
        int count = threadVisiable.sumCount();
        System.out.println(count);
    }

}
View Code

    有序性

    編譯器為了優化效能,有的時候會改變程式中語句的執行順序,在Java經典的雙重檢查建立單例模式,就是其中的一個體現,程式碼如下圖:

/**
 * @author wtz
 * <p>
 * 雙重鎖定單例模式 指令重排
 */
public class Singleton {

    private static Singleton singleton;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }

}
View Code

    程式碼整體上看起來無任何瑕疵,但是實際這個方法並不完美,問題出在new的操作上,正常情況下new Singleton()執行的操作如下步驟:

    1.在記憶體中分配一塊空間;

    2.在記憶體上初始化Singleton物件;

    3.將記憶體地址賦值給singleton變數;

    經過編譯器優化以後可能是這個樣子:  

    1.在記憶體中分配一塊空間;

    2.將記憶體地址賦值給singleton變數;

    3.在記憶體上初始化Singleton物件; 

    優化以後當CPU時間片切換時間剛好是執行緒B判斷為空的時候,這個時候singleton此時不為空,不需要進入鎖中,這個時候就返回為初始化的singleton,整體性執行過程如下圖:

   

     

    競態條件&臨界區

    當兩個執行緒競爭同一資源時,如果對資源的訪問順序敏感,就稱存在競態條件。導致競態條件發生的程式碼區稱作臨界區;

    互斥鎖

    互斥鎖解決了併發程式中的原子性問題,保證同一時刻只有一個執行緒執行,保證了一個或多個操作在CPU執行的過程中不被中斷,Java原生語言主要是通過synchronized實現互斥鎖;

    Java記憶體模型 

    Java記憶體模型主要是為了解決記憶體可見性和指令重排(編譯優化)的問題,使用記憶體模型約束了CPU快取和編譯優化;Java記憶體模型(JMM)從不同的角度來說都可以說很多的東西,比如從執行緒角度來說,JMM規範不同執行緒之間執行緒通訊的問題,從作業系統的角度來說,JMM規範了工作執行緒與記憶體之間訪問的問題;我們主要從程式設計師角度來看這個問題的話我認為可以從三方面說起:

    1.volatile、synchronized和final語義;

    2.JUC併發包;

    3.happens-before;

    我們主要說第3點,第1,2點以後在補充,我們先要明白一些概念,才能更好的理解後面的一些內容;happens-before主要有8個原則,我們通俗的話來講講:

    1.程式的順序性,單執行緒的每個前面的操作優先於後面的操作;

    2.volatile,對於volatile修飾的變數,寫的操作一定優先於讀的操作,也就是說對變數寫操作對於後續的讀操作都是可見的;

    3.鎖,解鎖的操作優先於加鎖的操作,在Java鎖指的就是synchronized,變數在解鎖之前的操作,在重新加鎖之後一定可以看到;

    4.傳遞性,A優先於B,B優先於C,則A優先於C;

    5.執行緒開始原則,主執行緒A啟動子執行緒B,則子執行緒B能夠看到主執行緒A在啟動B子執行緒之前的操作;

    6.執行緒終止原則,主執行緒A等待子執行緒B完成,當子執行緒B完成以後,主執行緒A能夠看到子執行緒的B的操作;

    7.執行緒中斷原則,對執行緒interrupt()方法優先於發生被中斷執行緒檢測到中斷事件的發生;

    8.物件終結規則,建構函式的執行一定優先於它的finalize方法;

    等待-通知機制

    等待-通知機制主要是為了處理迴圈等待造成的CPU消耗問題,主要有以下兩個步驟:

    1.執行緒首先獲取互斥鎖,當執行緒要求的條件不滿足時,釋放互斥鎖,進入等待狀態;

    2.當要求的條件滿足時,通知等待的執行緒,重新獲取互斥鎖;

    Java原生語言主要是通過synchronized + wait + notify/notifyAll實現;

    活躍性

    死鎖

    死鎖的定義一組相互競爭資源的執行緒因相互等待,導致永久阻塞的現象,發生死鎖必備的四個條件:

    1.互斥,共享資源同時只有佔用一個執行緒;

    2.佔有且等待,執行緒A獲取共享資源X,在等待共享資源Y的時候,不是釋放共享資源Y;

    3.不可搶佔,其他執行緒不能搶佔執行緒A獲取的共享資源;

    4.迴圈等待,執行緒A等待執行緒B獲取的共享資源,執行緒B等待執行緒A獲取的共享資源;

    只要破壞其中任意一個條件就可以跑壞死鎖;

    活鎖

    執行緒之間互相謙讓,導致執行緒無法執行下去,解決方案通過給執行緒隨機等待一個時間;

    飢餓

    執行緒不能正常的訪問共享資源,並且無法執行下去,解決執行緒飢餓的辦法:

    1.保證資源的公平性,也就執行緒的優先順序一樣;

    2.保證資源充足;

    3.避免執行緒長時間佔用鎖執行;

 三、結束

  歡迎大家加群438836709!歡迎大家關注我!