Java執行緒的啟動與中止

一、執行緒與進程的關係


關於進程與執行緒,百度百科上是這樣描述的:

進程(Process) 是電腦中的程式關於某數據集合上的一次運行活動,是系統進行資源分配和調度的基本單位,是作業系統結構的基礎。 在當代面向執行緒設計的電腦結構中,進程是執行緒的容器。程式是指令、數據及其組織形式的描述,進程是程式的實體。是電腦中的程式關於某數據集合上的一次運行活動,是系統進行資源分配和調度的基本單位,是作業系統結構的基礎。程式是指令、數據及其組織形式的描述,進程是程式的實體。

執行緒(thread) 是作業系統能夠進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運作單位。一條執行緒指的是進程中一個單一順序的控制流,一個進程中可以並發多個執行緒,每條執行緒並行執行不同的任務。

簡單的總結一下,就變成了下面的結果:

進程(Process): 程式運行資源分配的最小單位,進程內部有多個執行緒,會共享這個進程的資源。

執行緒(thread) : CPU調度的最小單位,必須依賴進程而存在。

也就是說 執行緒進程的關係,就像玄幻小說中蟲族的母體和子體一樣,子體離開了母體便不能存活。執行緒亦是如此,必須依賴於進程而存在。

二、Java執行緒的啟動方式


關於Java的執行緒,我們首當其衝的會想到java.lang.Thread類,這是一種最簡單的創建執行緒的方式,我們只需要通過繼承Thread類就可以實現:

/**
 * @author cai
 * 通過繼承Thread類的方式
 */
private static class UserThread extends Thread{

    /**
     * 重寫  Thread 類的  run() 方法
     */
    @Override
    public void run() {
        System.out.println("this is a thread for extends Thread");
    }
}

這裡,我們重寫了Thread類中的run()方法,並列印一條語句來表示執行緒的啟動方式,現在我們來測試一下:

public static void main(String[] args) {

    // 繼承Thread類的方式
    Thread thread = new UserThread();
    thread.start();
}

控制台的列印結果:

this is a thread for extends Thread

從列印結果可以看出,我們的執行緒正常的啟動,中間沒有出現意外。除了這種方式之外,JDK內部又給我們提供了一個介面類:java.lang.Runnable,同樣,我們也可以通過實現該介面,重寫run()方法來啟動一個新的執行緒:

/**
 * @author cai
 * 通過實現Runnable介面的方式
 */
private static class UserRunnable implements Runnable{

    /**
     * 重寫  Runnable 類的  run() 方法
     */
    @Override
    public void run() {
        System.out.println("this is a thread for implements Runnable");
    }
}

這裡我們同樣列印一句話來表示此執行緒的啟動方式,現在來測試一下:

public static void main(String[] args) throws  {
    // 實現Runnable介面的方式
    // 這裡注意:所有執行緒的啟動,都必須通過調用Thread類中start()方法來實現
    Runnable runnable = new UserRunnable();
    new Thread(runnable).start();
}

相信上面的程式碼,小夥伴們都能看懂(多注意一下第二行的注釋,這是重點),現在來看看控制台的列印結果:

this is a thread for implements Runnable

同理,這裡的執行緒也正常的啟動了。但看到這裡,小夥伴們可能就會產生疑問,為什麼JDK要多此一舉,提供了一種Thread類後,為什麼還要提供Runnable介面類呢?

在這裡給有這個疑惑的小夥伴們答疑下:

因為Java是單繼承,只能繼承一個類,不能繼承多個類。而在我們某些實際的業務中,我們可能需要繼承一個類來處理邏輯業務,那麼就不能再繼承Thread類來處理執行緒相關的操作了。所以JDK又為我們提供了一個實現Runnable介面的方式,而且在Java中,一個類是可以實現多個介面的,這樣我們在使用第二種方式處理執行緒就不會有顧忌了。(這裡比較有意思的是:Thread類實現了Runnable介面,有興趣的小夥伴可以去看一下源碼。)

那麼除了上面的兩種方式之外,Java是否提供了第三種方式呢?答案是肯定的,從JDK1.5開始,JDK為我們提供了一個新的介面類:java.util.concurrent.Callable,我們可以通過實現這個介面來啟動一個新得執行緒,而這種方式與實現Runnable介面來啟動執行緒所不同的是,它會帶有一個返回值,我們來看一下程式碼:

/**
 * @author cai
 * 通過實現Callable介面的方式
 * 帶返回值
 */
private static class UserCallable implements Callable<String>{

    /**
     * 重寫  Callable 類的  run() 方法
     * @return
     * @throws Exception
     */
    @Override
    public String call() throws Exception {
        return "this is a thread for implements Callable(return String).";
    }
}

測試一下:

public static void main(String[] args) throws ExecutionException, InterruptedException {
    // 實現Callable介面的方式   帶返回值
    UserCallable callable = new UserCallable();
    FutureTask<String> future = new FutureTask<String>(callable);
    new Thread(future).start();
    System.out.println(future.get());
}

我們這裡將返回值列印一下:

this is a thread for implements Callable(return String).

可以看出,我們的執行緒正常的啟動,沒有問題。

那麼看了以上三種Java執行緒的啟動方式,相信肯定有很多小夥伴會好奇,如果我要中止一個執行緒,我需要怎麼做呢?讓我們來一起看看吧。

三、Java執行緒的中止


怎樣讓一個正在運行的執行緒安全的停止工作呢?這裡有兩種方法:

1、執行緒自然的終止:程式正常的執行完或者拋出未處理的異常。

程式正常的執行完就不必再說,以上的程式碼都屬於此類,我們來看一看拋出未處理異常的情況,這裡我們將上述實現Runnable介面來啟動執行緒的程式碼修改一下:

/**
 * @author cai
 * 通過實現Runnable介面的方式
 */
private static class UserRunnable implements Runnable{

    /**
     * 重寫  Runnable 類的  run() 方法
     */
    @Override
    public void run() {
        // 重點加了這樣的一行程式碼
        int i = 10 / 0;
        System.out.println("this is a thread for implements Runnable");
    }
}

這裡我們加了一行程式碼,小夥伴們應該都可以看懂,這行程式碼是必定會拋出異常的,但我們又沒有對異常進行處理,現在來看一下控制台的列印結果:

Exception in thread "Thread-0" java.lang.ArithmeticException: / by zero
	at com.cai.thread.create.NewThread$UserRunnable.run(NewThread.java:39)
	at java.lang.Thread.run(Thread.java:748)

從結果可以看出,程式運行到了int i = 10 / 0這裡就停止了,並沒有列印出下一行的結果,由此可見,執行緒到了這裡便停止了,沒有再走下去。

2、調用JDK為我們提供的一系列方法

stop(),resume(),suspend(),interrupt(),isInterrupted(),interrupted()這些方法都是JDK為我們提供的,但是“stop(),resume(),suspend()`已經不建議使用了,原因如下:

stop() : 會導致執行緒不會正常的釋放資源,「惡意」的中斷當前正在運行的執行緒,不管執行緒的邏輯是否完整。

suspend()resume() : 極易造成公共的同步對象的獨佔,使得其他執行緒無法訪問公共同步對象,產生死鎖。(這個例子以後在講執行緒鎖的時候會解釋。)

我們先來看一下調用stop()的例子:

/**
 * @author cai
 *  調用 stop() 方法的例子
 */
private static class UserRunnable implements Runnable{

    @Override
    public void run() {
        try {
            // 讓此執行緒休眠1秒鐘
            Thread.sleep(1000);
        }catch (Exception e){
            // 異常 捕獲處理
        }
        // 此處不會運行,控制台不會列印
        // 若實際開發中,這裡要處理一個很重要的業務邏輯,那麼這裡就會有很大的問題。
        // 所以不建議使用
        System.out.println("程式碼到此處不會運行");
    }
}

public static void main(String[] args) throws InterruptedException {
    Runnable runnable = new UserRunnable();
    Thread thread = new Thread(runnable);
    thread.start();
    // 強行中止執行緒
    // 從這裡可以看出,JDK已經不建議使用stop()方法了,添加了@Deprecated註解
    thread.stop();
}

我這裡將測試程式碼也一併貼了上去,可以在程式碼的注釋中得到相關的結果。講完這些,我們來看看剩下的三個方法:interrupt(),isInterrupted(),interrupted()

interrupt():調用此方法會中斷一個執行緒,但並不是強行關閉這個執行緒,只是先給這個執行緒打個招呼,同事將執行緒的中斷標誌位設置為true,執行緒是否中斷,由執行緒本身決定。

isInterrupted():判斷當前的執行緒是否處於中斷狀態(返回true或者false)。

interrupted():靜態方法,判斷當前的執行緒是否處於中斷狀態,同時將中斷的標誌位設置為false

注意:如果方法中拋出了InterruptedException異常,那麼執行緒的中斷標誌位會被複位成false,如果確實需要中斷執行緒的操作,我們需要在catch語句中再次調用interrupt()方法。

解釋完了,直接上程式碼吧:

/**
 * @author cai
 * 調用 interrupt(),isInterrupted(),interrupted() 方法的例子
 */
private static class UserThread extends Thread{

    // 給執行緒一個名字,創建對象時賦值
    public UserThread(String threadName) {
        super(threadName);
    }

    @Override
    public void run() {
        // 獲得執行緒的名字
        String threadName = Thread.currentThread().getName();
       try {
           // @2
           System.out.println(threadName+" flag is " + isInterrupted());
           // 休眠一秒鐘   需要捕獲異常 InterruptedException  @3
           Thread.sleep(1000);
       }catch (InterruptedException e){
           // 列印一下  isInterrupted() 的狀態  @4
           System.out.println(threadName+" catch interrput flag is " + isInterrupted());
           // 調用 interrupt() 方法  中斷執行緒操作  @5
           interrupt();
       }
        // 列印執行緒的名字  @6
        System.out.println(threadName+" interrput flag is " + interrupted());
        // @7
        System.out.println(threadName+" interrput flag is " + isInterrupted());
    }
}

public static void main(String[] args) throws InterruptedException {
    // interrupt(),isInterrupted(),interrupted()  演示
    Thread thread = new UserThread("caiThread");
    thread.start();
    // @1
    thread.interrupt();
}

這裡為了方便解釋,我分了步驟:

1、@1 的位置調用的interrupt()方法,所以這裡的標誌位是true,所以 @2 的位置列印結果為true

2、@3 位置的sleep方法會拋出InterruptedException異常,我這裡捕獲了,在看之前的理論,拋出此異常,標誌位會重置為false,所以@4 這裡的列印結果為false

3、@5 位置再次調用了interrupt(),又把標誌位改為了false,所以 @6 這裡列印的結果為true,但是這裡注意的是,@6 調用了interrupted()這個靜態方法,所以標誌位又變為了false,所以@7 列印的結果為false

控制台列印結果:

caiThread catch flag is true
caiThread catch interrput flag is false
caiThread interrput flag is true
caiThread interrput flag is false

小夥伴們可以通過對比結果、程式碼和解釋一起看,相信還是很容易明白的。

對執行緒的了解再多一點點


Java執行緒總歸下來有五種狀態:

新建、就緒、阻塞、運行、死亡

而這裡對應的方法卻有很多種,具體的關係,我這裡準備了一張圖:

image-20200526162456870-Java執行緒狀態圖

這張圖上面的各種方法我都會在下次的文章中分享,這次的分享就到這裡,希望大家能夠喜歡。

最後


程式碼地址//github.com/caimm123/javaThread (先閱讀README.md 文件)

:若有轉載,請標明原處。如若有錯,也歡迎小夥伴前來指正。