【JAVA併發第二篇】Java執行緒的建立與執行,執行緒狀態與常用方法

就行222發表於2020-12-25

1、執行緒的建立與執行

(1)、繼承或直接使用Thread類

繼承Thread類建立執行緒:

/**
 * 主類
 */
public class ThreadTest {
    public static void main(String[] args) {
        //建立執行緒物件
        My_Thread my_thread = new My_Thread();
        //啟動執行緒
        my_thread.start();
    }
}

/**
 * 繼承Thread
 */
class My_Thread extends Thread{
    @Override
    public void run(){  //執行緒的任務
        System.out.println("My_Thread Running");
    }
}

直接使用Thread類建立執行緒:

class ThreadTest02 {
    public static void main(String[] args) {
        //直接使用Thread建立執行緒,"My_Thread"是取得執行緒名
        Thread my_thread = new Thread("My_Thread"){
            @Override
            public void run() {  //執行緒的任務
                System.out.println("My_Thread Running");
            }
        };
        //啟動執行緒
        my_thread.start();
    }
}

以上的方式都是直接使用Thread類建立執行緒,並通過start方法啟動執行緒,但執行緒並不會立即執行,它還需要等待CPU排程,只有執行緒獲得CPU控制權,才算是真正在執行。

直接使用Thread類的好處是:
方便傳參,可在子類裡新增成員變數,通過set方式設定引數或通過建構函式傳參

直接使用Thread類的缺點處是:
執行緒的建立和任務程式碼冗餘在一起。也可能由於繼承了Thread類,故無法再繼承其他類。任務無返回值。

(2)、使用Runnable介面的run方法

/**
 * 主類
 */
public class ThreadTest03 {
    public static void main(String[] args) {
        RunnableTask task = new RunnableTask();
        //建立執行緒,引數1 是任務物件; 引數2 是執行緒名字,推薦寫上
        Thread my_thread = new Thread(task,"My_Thread");
        //啟動執行緒
        my_thread.start();
    }
}
/**
 * Runable介面實現類
 */
class RunnableTask implements Runnable{
    @Override
    public void run(){  //執行緒的任務
        System.out.println("Thread Running");
    }
}

以上的方式是使用Runnable介面的run方法,該方式將任務程式碼與執行緒的建立分離,這樣在多個執行緒具有相同任務時,就可以使用同一個Runnable介面實現,同時該方式的Runnable的實現類也可以繼承其他的類。該方式更靈活,故推薦使用其來建立執行緒。

但其缺點也是任務無返回值。

(3)、使用FutureTask的方式


//建立任務類,類似於Runnable
public class CallerTask implements Callable<String> {
    @Override
    public String call() throws Exception {
        return "hello thread";
    }

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //建立任務物件
        FutureTask<String> futureTask = new FutureTask<>(new CallerTask());
        //啟動執行緒
        new Thread(futureTask,"My_Thread").start();
        //主執行緒等待"My_Thread"的任務執行完畢,並返回結果
        String res = futureTask.get();
        System.out.println(res);
    }
}

上述程式碼實現了Callable介面的call()方法。在main函式內首先建立FutureTask物件(建構函式為CallerTask的例項)。將建立的FutureTask物件作為任務,並放到新建立的執行緒中啟動。執行完畢後,則可以使用get方法等待執行緒裡的任務執行完畢並返回結果。

2、Java執行緒的狀態

Java執行緒在其生命週期中可能有六種狀態。根據Java.lang.Thread類中的列舉型別State的定義,其狀態有以下六種:

①NEW:初始狀態,執行緒已被建立但還未呼叫start()方法來進行啟動。

②RUNNABLE:執行狀態,呼叫start方法後,執行緒處於該狀態。注意,Java執行緒的執行狀態,實際上包含了作業系統中的就緒狀態(已獲得除CPU外的一切執行資源,正在等待CPU排程,獲得CPU控制權)和執行狀態(獲得CPU控制權,執行緒真正在執行)。因此,即使Java中的執行緒處於RUNNABLE狀態,也並不意味著該執行緒就一定正在執行(獲得CPU的控制權),該執行緒也有可能在等待CPU排程。

③BLOCKED:阻塞狀態,執行緒阻塞於鎖,即執行緒在鎖的競爭中失敗,則處於阻塞狀態。

④WAITING:等待狀態,該狀態的執行緒需要等待其他執行緒的中斷或通知。

⑤TIME-WAITING:超時等待狀態,該狀態下的執行緒也在等待通知,但若在限定時間內沒有,其他執行緒進行通知,那麼超過規定時間的執行緒就會自動“醒來”,繼續執行run方法內的程式碼。

⑥TERMINATED:終止狀態,執行緒執行完畢或者執行緒在執行過程中丟擲異常,則執行緒結束,執行緒處於終止狀態。

在這裡插入圖片描述

阻塞狀態(BLOCKED),是因為其在鎖競爭中失敗而在等待獲得鎖,而等待狀態(WAITING)則是在等待某一事件的發生,常見的如等待其他執行緒的通知或者中斷。

3、Java執行緒Thread類常用方法

(1)、start方法

是否為static方法:否。
作用:啟動一個新執行緒,在新執行緒呼叫run方法。

說明:執行緒呼叫start方法,進入執行狀態(RUNNABLE),但並不意味著執行緒中的程式碼會立即執行,因為Java執行緒中的執行狀態包含了作業系統層面的【就緒狀態】和【執行狀態】,所以只有Java執行緒真正獲得了CPU的控制權,執行緒才能真正地在執行。每個執行緒只能呼叫一次start方法來啟動執行緒,如果多次呼叫則會出現IllegalThreadStateException。

(2)、run方法

是否為static方法:否。
作用:執行緒啟動後會呼叫的方法。

說明:
①若使用繼承Thread類的方式建立執行緒,並重寫了run方法,則執行緒會在啟動後呼叫run方法,執行其中的程式碼。如果繼承時沒有重寫run方法或者run方法中沒有任何程式碼,則該執行緒不會進行任何操作。
②若使用實現Runnable介面的方法建立執行緒,則在呼叫start啟動執行緒後,也會呼叫Runnable實現類中的run方法,如果沒有重寫,則預設不會進行任何操作。

那些run方法和start方法又有什麼區別呢?

③start方法是真正能啟動一個新執行緒的方法,而run方法則是執行緒物件中的普通方法,即使執行緒沒有啟動,也可以通過執行緒物件來呼叫run方法,run方法並不會啟動一個新執行緒。

程式碼如下:


public class StartAndRun{
    public static void main(String[] args) {
        //使用Thread建立執行緒
        Thread t = new Thread("my_thread"){ //為執行緒命名為"my_thread"
            @Override
            public void run() {
                //Thread.currentThread().getName():獲取當前執行緒的名字
                System.out.println("【"+Thread.currentThread().getName()+"】"+"執行緒中的run方法被呼叫");
                for (int i = 0; i < 3; i++) {
                    System.out.println(i);
                }
            }
        };
        //呼叫run方法
        t.run();
        //呼叫start方法
        t.start();
    }
}

其結果如下:

【main】執行緒中的run方法被呼叫
0
1
2
【my_thread】執行緒中的run方法被呼叫
0
1
2

可以看出在my_thread執行緒啟動前(呼叫start方法前),也可以呼叫執行緒物件t中的run方法,呼叫這個run方法的執行緒並不會是my_thread執行緒(因為還沒啟動呢),而是main方法所在的主執行緒main。這是因為run方法是作為執行緒物件的普通方法存在的,可以認為run方法中的程式碼就是新執行緒啟動後所需要執行的任務。如果通過執行緒物件呼叫run方法,那麼在哪個執行緒呼叫的run方法,就由哪個執行緒負責執行。

總的來說,Thread類的物件例項對應著作業系統實際存在的一個執行緒,該物件例項負責提供給使用者去操作執行緒、獲取執行緒資訊。start方法會呼叫native修飾的本地方法start0,最終在作業系統中啟動一個執行緒,並會在本地方法中呼叫執行緒物件例項的run方法。所以,呼叫run方法並不會啟動一個執行緒,它只是作為執行緒物件等著被呼叫。

(3)、join方法

是否為static方法:否。
作用:用於同步,可以使用該方法讓執行緒之間的並行執行變為序列執行。

有程式碼如下:

/**
 * 主類
 */
public class Join {
    public static void main(String[] args) throws InterruptedException {
        Task task = new Task();
        Thread t1 = new Thread(task,"耗子尾汁");

        //啟動執行緒
        t1.start();

        //主執行緒列印
        for(int i = 0; i < 4; i++){
            if (i == 2) {
                //join方法:使main執行緒與t1執行緒同步執行,即t1執行緒執行完,main執行緒才會繼續
                t1.join();  
            }
            //Thread.currentThread().getName():獲取當前執行緒的名稱
            System.out.println("【"+Thread.currentThread().getName()+"】" + i); 
        }
    }
}

/**
 * Runnable介面實現類
 */
class Task implements Runnable{
    @Override
    public void run() {
        for(int i = 0; i < 3; i++){
            System.out.println("【"+Thread.currentThread().getName()+"】"+i);
        }
    }
}

其輸出如下:

【main】0
【main】1
【耗子尾汁】0
【耗子尾汁】1
【耗子尾汁】2
【耗子尾汁】3
【main】2
【main】3

在上面的程式碼中,建立了一個命名為“耗子尾汁”的執行緒,並通過start方法啟動。主執行緒和“耗子尾汁”執行緒都有迴圈列印i的任務。在“耗子尾汁”執行緒啟動後,就會進入執行狀態(Runnable),等待CPU排程,以獲得CPU使用權來列印i。而主執行緒在執行“耗子尾汁”執行緒的start方法後,就會繼續往下執行,迴圈列印i。正常來講,主執行緒和“耗子尾汁”執行緒應該處於並行執行的狀態,即二者會各自執行自己的for迴圈。但由於在主執行緒的for迴圈中呼叫了join方法,使得主執行緒交出了CPU的控制權,並返回到“耗子尾汁”執行緒,等待該執行緒執行完畢,主執行緒才繼續執行。所以join方法就相當於在主執行緒中同步“耗子尾汁”執行緒,使“耗子尾汁”執行緒執行完,才會繼續執行主執行緒。其最終效果就是可以使用該方法讓執行緒之間的並行執行變為序列執行。

join方法是可以傳參的。join(10)的意思就是,如果在A執行緒中呼叫了B執行緒.join(10),那麼A執行緒就會同步等待B執行緒10毫秒,10毫秒後,A、B執行緒就會並行執行。

同時也要注意,只有執行緒啟動了,呼叫join方法才有意義。在上述程式碼中,如果“耗子尾汁”執行緒沒有呼叫start方法來啟動,那麼join並不會起作用。

(4)、getId方法、getName方法、setName方法
是否為static方法:均為否。
作用:
①getId方法:獲取執行緒長整型的id、這個執行緒id是唯一的。
②getName方法:獲取執行緒名
③setName(String):設定執行緒名

(5)、getPriority方法、setPriority(int)方法
是否為static方法:均為否。
作用:
①setPriority(int)方法:設定執行緒的優先順序,優先順序的範圍為1-10。
②getPriority方法:獲取執行緒的優先順序。

現在的主流作業系統(windows、Linux等)基本都採用了時分的形式來排程執行執行緒,即將CPU的時間分為一個個時間片(這些時間片相等的),執行緒會得到若干時間片,時間片用完就會發生執行緒排程,並等待下一次的分配。執行緒優先順序就是決定執行緒需要多或者少分配一些時間片。

Java執行緒的優先順序範圍為1-10,預設優先順序為5。優先順序高的執行緒分配的時間片的數量要都多於優先順序低的執行緒。可通過setPriority(int)方法來設定。頻繁阻塞的執行緒(比如I/O操作或休眠)的執行緒需要設定較高優先順序,而計算任務較重(比如偏向運算操作或需要較多CPU時間)的執行緒則設定較低優先順序,以避免CPU會被獨佔。

需要注意的是,Java執行緒的優先順序設定只能給作業系統建議,並不能直接決定執行緒的排程,Java執行緒的排程只能由作業系統決定。作業系統完全可以忽略Java執行緒的優先順序設定。在不同的作業系統上Java執行緒的優先順序會存在差異,一些作業系統會直接無視優先順序的設定。所以一些在邏輯上有先後順序的操作,不能依靠設定Java執行緒的優先順序來完成。

Java子執行緒的預設優先順序與父執行緒的優先順序一致,例如在main方法中建立執行緒,那麼主執行緒(預設為5)就是這個新執行緒的父執行緒,該新執行緒的預設優先順序為父執行緒的優先順序。如果給主執行緒設定優先順序為4,那麼這個新執行緒的預設優先順序就為4。

(6)、getState()方法、isAlive()方法
是否為static方法:均為否。
作用:
①getState()方法:獲取執行緒的狀態(NEW、RUNNABLE、WATING、BLOCKED、TIME_WATING、TERMINATED)
②isAlive()方法:判斷執行緒是否存活,即是否執行緒已啟動但尚未終止((還沒有執行完
畢))。

(7)、interrupt()方法
是否為static方法:否。
作用:中斷執行緒,當A執行緒執行時,B執行緒可以通過A執行緒的物件例項來呼叫A執行緒的interrput()方法設定執行緒A的中斷標誌位true,並立即返回。設定中斷僅僅是設定標誌,通過設定中斷標誌並不能直接終止該執行緒的執行,而是被中斷的執行緒根據中斷狀態自行處理。如果打斷的是正在執行中的執行緒,那麼該執行緒就會被設定中斷標誌。但如果執行緒正在執行sleep方法或者上面所說的join方法時,被呼叫了interrupt方法,那麼這個被打斷的執行緒會丟擲出 InterruptedException異常,並清除打斷標誌。

(8)、interrupted()方法、isInterrupted()方法
是否為static方法:interrupted為非static方法、isInterrupted為static方法
作用:均為判斷執行緒是否被打斷。區別在於interrupted()方法不會清除中斷標記,isInterrupted()方法會清除中斷標誌。

(9)、sleep(long n)方法
是否為static方法:是。
作用:讓執行緒休眠,當一個執行中的執行緒呼叫sleep方法後,該執行緒就會掛起,並把剩下的CPU時間片交給其他執行緒,但並不會直接指定由哪個執行緒佔用,需要作業系統來進行排程。執行緒在休眠期間不參與CPU排程,但也不會把執行緒佔有的其他資源(比如鎖)進行釋放。

需要注意的是,休眠時間到後執行緒也並不會直接繼續執行,而是進入等待CPU排程的狀態。同時由於sleep方法是靜態方法,使用t.sleep()並不會讓t執行緒進入休眠,而是讓當前執行緒進入休眠(比如在main方法中呼叫t.sleep(),實際上是讓主執行緒進入休眠)。

(10)、yield() 方法
是否為static方法:是。
作用:使執行緒讓出CPU控制權。實際上該方法只是向作業系統請求讓出自己的CPU控制權,但作業系統也可以選擇忽略。執行緒呼叫該方法讓出CPU控制權後,會進入就緒狀態,也有可能遇到剛讓出CPU控制權後又被CPU排程執行的情況。

相關文章