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 文件)

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