【JAVA併發第二篇】Java線程的建立與運行,線程狀態與經常使用方法

一、線程的建立與運行

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

繼承Thread類建立線程:java

/**
 * 主類
 */
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類建立線程:windows

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控制權,纔算是真正在執行。ide

直接使用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的實現類也能夠繼承其餘的類。該方式更靈活,故推薦使用其來建立線程。線程

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

(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方法等待線程裏的任務執行完畢並返回結果。對象

二、Java線程的狀態

Java線程在其生命週期中可能有六種狀態。根據Java.lang.Thread類中的枚舉類型State的定義,其狀態有如下六種:blog

①NEW:初始狀態,線程已被建立但還未調用start()方法來進行啓動。繼承

②RUNNABLE:運行狀態,調用start方法後,線程處於該狀態。注意,Java線程的運行狀態,實際上包含了操做系統中的就緒狀態(已得到除CPU外的一切運行資源,正在等待CPU調度,得到CPU控制權)和運行狀態(得到CPU控制權,線程真正在執行)。所以,即便Java中的線程處於RUNNABLE狀態,也並不意味着該線程就必定正在執行(得到CPU的控制權),該線程也有可能在等待CPU調度。

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

④WAITING:等待狀態,該狀態的線程須要等待其餘線程的中斷或通知。

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

⑥TERMINATED:終止狀態,線程執行完畢或者線程在執行過程當中拋出異常,則線程結束,線程處於終止狀態。

在這裏插入圖片描述

阻塞狀態(BLOCKED),是由於其在鎖競爭中失敗而在等待得到鎖,而等待狀態(WAITING)則是在等待某一事件的發生,常見的如等待其餘線程的通知或者中斷。

三、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調度執行的狀況。

相關文章
相關標籤/搜索