走進高併發(二)Java並行程式基礎

lemon__jiang發表於2020-01-14

一、程式和執行緒

在作業系統這門課程中,對程式的定義是這樣的:

程式是計算機中的程式關於某資料集合上的一次執行活動,是系統進行資源分配和排程的基本單位,是作業系統結構的基礎。在早期面向程式設計的計算機結構中,進行是程式的基本執行實體;在當代面向執行緒設計的計算機結構中,程式是執行緒的容器。

上面的定義很完整,對程式進行了全方面的定義,但是貌似程式是看不見摸不著的一個東西,實際上,我們可以通過檢視計算機的程式管理器來檢視應用程式的程式。

image-20191119075437166

上述程式列表中,展示了多個應用程式的程式,通常情況下,一個應用程式佔用一個程式,系統資源的分配與調配也是基於程式的。其實可以理解為,一個程式就是一個應用程式。

那麼執行緒和程式究竟是什麼關係呢?簡單說來,程式就是執行緒的“母親”,是承載執行緒的基本單位,也是承載執行緒的容器。舉個例子,一棟公司大樓裡,許多員工都在各司其職,井然有序地工作著,每個員工就可以理解為一個活動執行緒,多個員工有時候會進行分組,每個組的員工共同協調合作完成一份工作,那麼可以理解為執行緒分組,執行緒組內的執行緒共同合作完成工作,有時候,員工會排隊等待領取下午茶,只有當前員工成功領取了下午茶之後才會走出佇列,那麼可以理解為執行緒訪問臨界區,多個執行緒等待臨界區執行緒完成任務後離開臨界區。那麼程式就可以被理解為這棟公司大樓,它是承載公司正常執行(員工日常工作)的載體。

一個程式是由多個執行緒組合而成,那麼可以這麼說執行緒其實就是輕量級的程式,是程式執行的最小單位。現在的程式設計中,強調使用多執行緒,而不是多程式,那是因為執行緒間的切換與排程所消耗的成本遠遠低於程式所消耗的成本。

二、執行緒的生命週期

在Java的Thread類中有一個列舉型別State,State列舉內列舉了執行緒的生命週期,程式碼如下:

public enum State {
        /**
         * Thread state for a thread which has not yet started.
         */
        NEW,

        /**
         * Thread state for a runnable thread.  A thread in the runnable
         * state is executing in the Java virtual machine but it may
         * be waiting for other resources from the operating system
         * such as processor.
         */
        RUNNABLE,

        /**
         * Thread state for a thread blocked waiting for a monitor lock.
         * A thread in the blocked state is waiting for a monitor lock
         * to enter a synchronized block/method or
         * reenter a synchronized block/method after calling
         * {@link Object#wait() Object.wait}.
         */
        BLOCKED,

        /**
         * Thread state for a waiting thread.
         * A thread is in the waiting state due to calling one of the
         * following methods:
         * <ul>
         *   <li>{@link Object#wait() Object.wait} with no timeout</li>
         *   <li>{@link #join() Thread.join} with no timeout</li>
         *   <li>{@link LockSupport#park() LockSupport.park}</li>
         * </ul>
         *
         * <p>A thread in the waiting state is waiting for another thread to
         * perform a particular action.
         *
         * For example, a thread that has called <tt>Object.wait()</tt>
         * on an object is waiting for another thread to call
         * <tt>Object.notify()</tt> or <tt>Object.notifyAll()</tt> on
         * that object. A thread that has called <tt>Thread.join()</tt>
         * is waiting for a specified thread to terminate.
         */
        WAITING,

        /**
         * Thread state for a waiting thread with a specified waiting time.
         * A thread is in the timed waiting state due to calling one of
         * the following methods with a specified positive waiting time:
         * <ul>
         *   <li>{@link #sleep Thread.sleep}</li>
         *   <li>{@link Object#wait(long) Object.wait} with timeout</li>
         *   <li>{@link #join(long) Thread.join} with timeout</li>
         *   <li>{@link LockSupport#parkNanos LockSupport.parkNanos}</li>
         *   <li>{@link LockSupport#parkUntil LockSupport.parkUntil}</li>
         * </ul>
         */
        TIMED_WAITING,

        /**
         * Thread state for a terminated thread.
         * The thread has completed execution.
         */
        TERMINATED;
}

從上面的程式碼註釋可以瞭解到,執行緒的生命週期包括NEW、RUNNABLE、BLOCKED、WAITING、TIMED_WAITING、TERMINATED六種狀態。

  • NEW狀態:表示執行緒處於剛剛建立好的狀態,執行緒還未執行,需要等待執行緒呼叫start()方法後才表示執行緒開始執行。一旦執行的執行緒,那麼就不會再回到NEW狀態了。
  • RUNNABLE狀態:表示執行緒執行所需要的資源都準備好了,正處於執行狀態。
  • BLOCKED狀態:表示處於RUNNABLE狀態進入了阻塞狀態,進入阻塞狀態的原因可能是因為當前執行緒優先順序低於其他執行緒,暫時尚未獲取鎖,執行緒暫時執行,直到獲取到請求的鎖。
  • WAITING、TIMED_WAITING狀態:都表示執行緒進入等待狀態,兩者的區別在於前者是一個沒有期限的等待,而後者則是有限時間內的等待。兩種等待狀態的執行緒一旦再次執行,那麼又會進入到RUNNABLE狀態。
  • TERMINATED狀態:表示執行緒執行完畢後的狀態,一旦進入到TERMINATED狀態的執行緒就不會再次回到RUNNABLE狀態了。

三、執行緒的基本操作

3.1 開啟執行緒

開啟一個新的執行緒很簡單,在這裡暫時不討論執行緒池的內容,開啟新執行緒只需要使用new關鍵字建立一個執行緒物件,並將其start()起來即可。

Thread thread = new Thread();
thread.start();

呼叫執行緒物件的start()方法以後,會開啟一個新的執行緒去執行執行緒物件的run()方法,這裡需要注意的是,不能直接呼叫run()方法,否則就是在當前執行緒裡直接執行了run()方法,而不是在新執行緒裡執行,這就是start()和run()方法的區別。通常建立執行緒物件的時候會傳入一個Runnable的實現類物件,Runnable介面只有一個run()方法需要去實現,那麼呼叫執行緒的start()方法就會去開啟新執行緒執行實現類物件的run()方法。

3.2 結束執行緒

通常來說,新建執行緒在在完成執行任務後會自動關閉,無需人工理會,但是在某些情況下,可能為了減少不必要的執行流程,會手動去關閉執行緒。從JDK原始碼來看,執行緒類Thread提供了一個停止執行緒的方法stop(),該方法可以停止執行緒的執行,使用起來十分方便,但是它已經被標註為“廢棄”了,也就是說不推薦使用了。這是為什麼呢?原因是因為stop()方法過於暴力,強行將執行中的執行緒停止,這樣就有可能會造成記憶體中資料不一致的現象。

舉個例子,比如新建執行緒的主要執行流程就是給user物件設定ID和名稱,建立新執行緒後,設定完ID就強行停止了執行緒,那麼記憶體中的user物件的兩個屬性中名稱屬性可能就是預設值,並沒有成功設定,這樣其他執行緒讀取該資料就和預想的不一致了。

Thread類的stop()方法會強行停止執行緒,也就釋放該執行緒持有的鎖,釋放鎖後其他執行緒就有機會獲取該鎖,從而讀取到該物件,那麼讀取到的資料就是不完整的資料。

3.3 中斷執行緒

執行緒中斷的解決方案要優於執行緒終止,執行緒中斷不會像執行緒終止一樣,會立馬結束執行緒的後續執行流程,前者更像是得到了一個通知,通知他可以退出執行了,當執行緒接受這樣的通知以後,會在一個合適的時機退出執行緒,並且不會造成髒資料問題。這個特點將執行緒中斷和執行緒終止區別開來,是一個很重要的特點。

在Thread類中,有三個方法與執行緒中斷息息相關:

public void interrupt()
public boolean isInterrupted()
public static boolean interrupted()
  • 第一個方法的作用是設定執行緒的中斷標誌位,也就是通知執行緒需要中斷了。
  • 第二個方法的作用是通過檢查中斷標誌位,來判斷當前執行緒是否被中斷。
  • 第三個方法是一個靜態方法,也是用來檢查執行緒的是否被中斷,但是和第二個方法的區別就是會清除執行緒的中斷標誌位。

以上三個方法的方法體很簡單,讀者可以自行前往Thread類進行閱讀。

下面的案例用來測試執行緒中斷,程式碼如下:

public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(() -> {
            while (true) {
                System.out.println("test");
                Thread.yield();
            }
        });
        thread.start();
        Thread.sleep(2000);
        thread.interrupt();
}

仔細觀察程式碼,發現最後一行使用了執行緒中斷方法,將thread執行緒進行中斷,將程式碼執行起來之後發現,在控制檯一直列印著“test”,完全沒有停止下來的意思。分析原因得知,呼叫執行緒物件的interrupt()方法僅僅是設定了中斷標誌位,並不會去主動中斷執行緒,那麼上文中所說的通知執行緒中斷後,會在合適的時機退出執行緒,那麼何時是合適的時機呢?這就需要人工介入了。

public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(() -> {
            while (true) {
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println("Interrupted");
                    break;
                }
                System.out.println("test");
                Thread.yield();
            }
        });
        thread.start();
        Thread.sleep(2000);
        thread.interrupt();
}

上述程式碼在合適的位置加入了一個判斷,判斷當前執行緒的中斷標誌位是否被設定為中斷,從而決定是否要中斷執行緒,這就是所說的何時的時機。

這裡需要注意一點的是,Thread.sleep()方法如果發生異常,那麼當前執行緒設定的中斷標誌位會被清除掉,如果要捕獲此類異常,那麼需要重新設定中斷標誌位,也就是通知執行緒需要中斷了。

3.4 等待和通知

為了適應執行緒間的協作能力,JDK的Object類提供了wait()和notify()方法,即等待方法和通知方法,等待方法指的是呼叫某個類物件的wait()方法後,當前執行緒進入到等待狀態,等待直到其他執行緒內的同一物件呼叫了notify()方法為止,其他執行緒將通知當前執行緒繼續執行後續流程。由於這兩個方法位於Object類中,那麼就代表任何物件都是可以呼叫這兩個方法。

當某個執行緒呼叫了object物件的wait()方法,那麼該執行緒就進入了等待object物件的佇列中,由於多個執行緒執行到同一位置,都會進入到等待object物件的佇列中,都在等待其他執行緒呼叫object物件的notify()方法,當呼叫了notify()方法後,並不是所有的等待執行緒都會繼續執行後續流程,而是其中的某個執行緒收到通知後繼續執行後續流程。notify()方法是從等待佇列中隨機選取一個執行緒去啟用,並不是所有的執行緒都能收到通知。當然,Object類也提供了notifyAll()方法,那麼它的作用就是通知所有的等待執行緒繼續後續流程。

這裡有個細節需要注意,那就是呼叫wait()和notify()首先都必須包含在synchronized語句中,因為它們的呼叫必須獲取到目標物件的鎖。下圖展示了wait()方法和notify()方法的工作流程細節。

image-20191122073456465

wait()方法和sleep()方法都可以讓執行緒等待,但是二者還是有卻別的:

  • wait()方法使執行緒進入等待,但是可以重新被喚醒
  • wait()方法會釋放目標物件的鎖,而sleep()方法不會釋放任何資源
3.5 掛起和繼續

掛起(suspend)和繼續(resume)是一對JDK提供的執行緒介面,掛起可以將當前執行緒暫停,直到對應執行緒執行了繼續介面,否則將不會釋放目標物件的資源,這一對方法已經被JDK標註為“廢棄”,不再被推薦使用。

3.6 等待執行緒結束(join)和謙讓(yeild)

當一個執行緒的輸入可能非常依賴另外一個或者多個執行緒的輸出,此時,這個執行緒就必須等待被依賴的執行緒執行完畢,才能繼續執行。對於這種需求,JDK提供了join()方法來實現這個功能。JDK提供了兩個join()方法,方法簽名如下所示:

public final void join() throws InterruptedException
public final synchronized void join(long millis) throws InterruptedException

方法一表示目標執行緒呼叫後,那麼當前執行緒就會一直等待,直到目標執行緒執行完畢。方法二設定了一個最大等待時間,如果超過這個最大等待時間,那麼當前執行緒就不會等待目標執行緒執行完畢,就會繼續執行後續的流程。

public class JoinThread extends Thread {

    private volatile static int i = 0;

    @Override
    public void run() {
        for (i = 0; i < 100000; i++) ;
    }

    public static void main(String[] args) throws InterruptedException {
        JoinThread joinThread = new JoinThread();
        joinThread.start();
        joinThread.join();
        System.out.println(i);
    }
}

上述程式碼中,如果不使用join()方法,那麼列印出來的i值為0或者很小很小的值,使用了join()方法後,那麼始終會等待新執行緒的執行完畢後繼續執行,此時列印出來的i值始終是100000。

執行緒謙讓是指執行緒主動讓出CPU,讓出CPU後還會進入到資源爭奪中,至於還有沒有機會再爭奪到資源,那就不一定了。JDK提供了yeild()方法來實現此功能,目的是為了讓低優先順序的執行緒儘量少佔用過多資源,儘量讓出資源給高優先順序的執行緒。

瞭解更多幹貨,歡迎關注我的微信公眾號:爪哇論劍(微訊號:itlemon)
微信公眾號-爪哇論劍-itlemon

相關文章