Java多線程:生命周期,實現(xiàn)與調(diào)度

前面幾篇文章為Java多線程做足了鋪墊,這篇終于到了正題,一起學(xué)習(xí)一下Java多線程的基礎(chǔ)知識。

1. Java線程生命周期

下圖表述了Java線程的幾個基本狀態(tài):

  • New新建狀態(tài):線程創(chuàng)建后即進入。
  • Runnable就緒狀態(tài):調(diào)用線程的start方法后,線程不會立刻運行,而是進入就緒狀態(tài),等待CPU調(diào)度。
  • Running運行狀態(tài): CPU開始調(diào)度處于就緒狀態(tài)的線程后,線程進入運行狀態(tài)。<font color="red">換言之,就緒狀態(tài)是運行狀態(tài)的唯一入口。</font>
  • Blocked阻塞狀態(tài):線程由于某種原因放棄CPU的使用權(quán),停止執(zhí)行,此時進入阻塞狀態(tài),一直到進入就緒態(tài)CPU才能對其進行重新調(diào)度。
    產(chǎn)生Blocked狀態(tài)的幾種可能
  1. 等待阻塞:運行線程中的wait()方法,使本地線程進入阻塞狀態(tài)。

  2. 同步阻塞:線程獲取sychronized同步鎖失敗,進入同步阻塞狀態(tài)。

  3. 其他阻塞:調(diào)用線程的sleep()或join()發(fā)出I/O請求,線程進入阻塞狀態(tài)。當(dāng)sleep()超時,join()等待線程終止或超時,處理完I/O時,線程重新進入就緒狀態(tài)。

  • Dead死亡狀態(tài):線程執(zhí)行完畢或異常退出。該線程結(jié)束了生命周期。

2. Java線程實現(xiàn)方法

2.1. 繼承Thread類,重寫run()方法

最基本的線程方式,通過繼承Thread類,重寫run()方法即可運行子線程。缺點是Java類只能有一個父類,如果所要使用的類本身有其他需要繼承的實體,就會比較麻煩。

class MyThread extends Thread {
    private volatile boolean isRunning = true;
    public boolean isRunning() {
        return isRunning;
    }
    public void setRunning(boolean isRunning) {
        this.isRunning = isRunning;
    }
    @Override
    public void run() {
        System.out.println("進入到run方法中了");
        while (isRunning == true) {
        }
        System.out.println("線程執(zhí)行完成了");//結(jié)束線程,進入dead態(tài)
    }
}
public class RunThread{
    public static void main(String[] args) {
        try {
            MyThread thread = new MyThread();//創(chuàng)建線程,進入new態(tài)
            thread.start();//start線程,進入runnable態(tài),CPU開始調(diào)度后進入running態(tài)
            Thread.sleep(1000);//阻塞主線程,進入blocked態(tài)
            thread.setRunning(false);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

2.2. 實現(xiàn)Runnable接口,便于繼承其他類

針對Thread類繼承可能存在困難的問題,利用Java接口無限制的特性,利用接口來進行線程實現(xiàn)。在運行時,需要運行一個thread并把接入了Runnable的類以參數(shù)形式傳給thread。

class MyClass implements Runnable {
    private volatile boolean isRunning = true;
    public boolean isRunning() {
        return isRunning;
    }
    public void setRunning(boolean isRunning) {
        this.isRunning = isRunning;
    }
    @Override
    public void run() {
        System.out.println("進入到run方法中了");
        while (isRunning == true) {
        }
        System.out.println("線程執(zhí)行完成了");
    }
}
public class RunThread{
    public static void main(String[] args) {
        try {
            MyClass myClass=new MyClass();
            Thread thread=new Thread(myClass);
            thread.start();
            Thread.sleep(1000);
            myClass.setRunning(false);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

我們需要知道的是,Thread類本身就已經(jīng)接入了Runnable接口,如果我們給一個自定義的已經(jīng)重寫run的Thread類的子類A傳入一個接入Runnable的類B作為參數(shù),那么該子類A會運行類B的run()函數(shù),原因為,Thread中的run()函數(shù)代碼如下:

  @Override
    public void run() {
        if (target != null) {
            target.run();
        }
    }

可見,如果target存在,即有Runnable類的子類作為參數(shù)傳遞,則不運行Thread提供的run()方法。

最基本的Thread和Runnable有一個共同的問題,即是run()函數(shù)沒有返回值,導(dǎo)致線程間交互會比較復(fù)雜,需要依賴引用的互相傳遞。因此,當(dāng)需要有返回值時,Java提供了另外的類。

2.3. Callable類替換Runnable類,實現(xiàn)返回值

Callable接口定義如下:

public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

對于Callablel類我們只需要知道以下幾點

  1. 需要實現(xiàn)重寫其call()函數(shù)來實現(xiàn)功能。

  2. 泛型的輸入類型即是call()的返回類型。

實現(xiàn)了call函數(shù),簡單的把Callable接口替換Runnable接口,我們就可以實現(xiàn)簡答的有返回值線程了,但Java提供了更多的線程狀態(tài)檢查,控制類來更好的使用這兩個接口。

2.4. Future接口對任務(wù)進行監(jiān)測

Future接口有5個方法,我們依次來進行介紹

public interface Future<V> {
    boolean cancel(boolean mayInterruptIfRunning);
    boolean isCancelled();
    boolean isDone();
    V get() throws InterruptedException, ExecutionException;
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}
  • cancel() 方法來表示取消任務(wù),如果取消成功則返回true,否則返回false。其參數(shù)mayInterruptIfRunning則表示正在執(zhí)行的任務(wù)。
  • isCanelled() 方法表示任務(wù)是否已經(jīng)取消,如果任務(wù)完成前成功取消則返回true。
  • isDone() 方法表示任務(wù)是否已經(jīng)完成,如果完成返回true。
  • get() 方法獲取執(zhí)行結(jié)果,這個方法會產(chǎn)生阻塞,一直到執(zhí)行完畢才返回。
  • get(long timeout, TimeUnit unit) 方法在獲取執(zhí)行結(jié)果同時給一個時間,如果超時仍被阻塞則返回Null。

2.5. FutureTask類:Future類的唯一實現(xiàn)

FutureTask類實現(xiàn)了RunnableFuture接口,而RunnableFuture接口如同名字一樣,接入了Runnable和Future兩個類。因此FutrueTask既可以作為Runnable被線程執(zhí)行(自動調(diào)用其run()方法),同時又可以視為Future類來得到Callable類的返回值。此處我們可以看到Java設(shè)置這個類的用意——讓用戶盡量使用這個類去管理線程。

使用FutureTask類托管Callable的一個例子:

class MyClass implements Callable {
    private volatile boolean isRunning = true;
    public boolean isRunning() {
        return isRunning;
    }
    public void setRunning(boolean isRunning) {
        this.isRunning = isRunning;
    }
    @Override
    public Object call() {
        System.out.println("進入到run方法中了");
        while (isRunning == true) {
        }
        System.out.println("線程執(zhí)行完成了");
        return isRunning;
    }
}
public class RunThread{
    public static void main(String[] args) {
        try {
            MyClass myClass=new MyClass();
            FutureTask futureTask=new FutureTask(myClass);
            Thread thread=new Thread(futureTask);
            thread.start();
            Thread.sleep(1000);
            myClass.setRunning(false);
            try{
                System.out.println(futureTask.get());
            }
            catch (Exception e){
                System.out.println("Type error");
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

3. Java多線程的調(diào)度

3.1. Java多線程的就緒,運行和死亡

如圖,需要注意的是CPU調(diào)度線程有隨機性,不排除線程調(diào)用yield()進入就緒狀態(tài)后CPU又去調(diào)用線程的情況。

3.2. Java線程的阻塞

3.2.1. Java線程的阻塞方法

  • join()方法:讓一個線程等待另一個線程完成才繼續(xù)執(zhí)行。如,在線程A的運行過程中調(diào)用線程B的join方法,則線程A被阻塞,等待線程B運行完成后才繼續(xù)運行。該方法通常用于主線程中。
public static void main(String[] args){
  ......
  threadA.join();
}
  • sleep()方法:讓當(dāng)前的線程進入阻塞狀態(tài),暫停指定時間,之后恢復(fù)就緒狀態(tài)。例如,在前面的例子里,我們讓主線程sleep 1000毫秒以顯示效果。需要注意的是,sleep()方法并不會釋放鎖,實際上,sleep()是一個與實例無關(guān)的方法。sleep()方法是一個靜態(tài)方法,也就是說他只對當(dāng)前線程有效,通過t.sleep()讓t對象進入sleep,這樣的做法是錯誤的,其效果和Thread.sleep()無異但造成了一定迷惑性。sleep()的目的在于讓當(dāng)前線程自主的暫停,因而是針對當(dāng)前線程生效的靜態(tài)方法,即是在子線程的run()函數(shù)中也可以調(diào)用。如果不是這樣的設(shè)計,sleep()要么和wait()同質(zhì),要么會造成嚴(yán)重的死鎖。與sleep()方法有一定共性但實質(zhì)上區(qū)別很大的wait()方法接下來我會單開文章去談。

3.3. 后臺線程

后臺線程主要為其他線程提供服務(wù),或“守護線程”。例如:JVM的GC線程。后臺線程與前臺線程生命周期有一定關(guān)聯(lián)。主要體現(xiàn)在:當(dāng)所有前臺線程死去時,后臺線程也會自動死去。

設(shè)置一個線程是否為后臺線程只需要調(diào)用該線程的setDaemon(boolean val)方法即可,創(chuàng)建一個線程后,默認(rèn)為非后臺線程。

public static void main(String[] args){
  ......
  threadA.setDaemon(true);
}

3.4. 線程的優(yōu)先級

考慮到下面的情景,5個線程競爭同一資源,資源目前被重要線程A鎖定,當(dāng)線程A執(zhí)行完畢,解鎖釋放資源后,我們希望重要線程E能先于BCD得到資源。此時,我們則需要優(yōu)先級這個屬性。每個線程都有一個優(yōu)先級變量,該變量的值可以是1到10之間的一個常數(shù)。數(shù)字越大,代表優(yōu)先級越高。Java給了其中三個常量名稱,分別是:

MAX_PRIORITY:10

MIN_PRIORITY:1

NORM_PRIORITY:5

默認(rèn)情況下,一個線程的優(yōu)先級是其父線程的優(yōu)先級。而主線程的優(yōu)先級是NORM_PRIORITY,即數(shù)字5,因此,我們可以看到很多資料里認(rèn)為現(xiàn)成的默認(rèn)優(yōu)先級是5,其實不是這樣的。我們可以通過調(diào)用線程的setPrority(short val)來設(shè)置線程的優(yōu)先級以及使用getPrority()來獲取優(yōu)先級。

class MyClass implements Callable {
    @Override
    public Object call() {
        System.out.println("線程執(zhí)行完成了,將返回其子線程的優(yōu)先級");
        return new Thread().getPriority();
    }
}

public class RunThread {
    public static void main(String[] args) {
        FutureTask futureTask = new FutureTask(new MyClass());
        Thread thread = new Thread(futureTask);
        thread.setPriority(3);
        System.out.println("設(shè)置該線程的優(yōu)先級為"+thread.getPriority());
        thread.start();
        try {
            System.out.println("該線程的子線程優(yōu)先級為" + futureTask.get());
        } catch (Exception e) {
            System.out.println("Type error");
        }
    }
}

上面這段代碼的返回內(nèi)容如下:

設(shè)置該線程的優(yōu)先級為3
線程執(zhí)行完成了,將返回其子線程的優(yōu)先級
該線程的子線程優(yōu)先級為3

然而,需要注意的是,優(yōu)先級無法保證線程的執(zhí)行順序,即在BCDE競爭資源的情境中,無法保證E線程一定先執(zhí)行。優(yōu)先級高則其獲取CPU資源的可能性比較大,而不是一定會先于其他線程獲取執(zhí)行優(yōu)先權(quán),優(yōu)先級低的線程仍有得到執(zhí)行權(quán)的可能。

同時我們不要忘記,Java的Thread類的核心方法是native方法,即是線程實現(xiàn)大量依賴宿主機的機制。Java有10個優(yōu)先級,如果宿主機是Linux系統(tǒng),有31個優(yōu)先級,顯然沒什么問題。但如果宿主機是Windows7,只有7個優(yōu)先級,那么Java的10個優(yōu)先級就會根據(jù)系統(tǒng)的優(yōu)先級進行映射。同時,線程的優(yōu)先級也會受到虛擬機的影響。

3.5. 線程讓步y(tǒng)ield()

前面多少也提到過了。yield()和sleep()類似,是靜態(tài)方法,作用于將當(dāng)前運行的線程,其作用于具體線程,調(diào)用方法是Thread.yield(),使用t.yield()同樣是能夠使用但邏輯不清晰的做法。具體來說。yield()是讓當(dāng)前線程從running態(tài)回到runnable態(tài)。之后,線程間重新進行資源競爭,由CPU進行調(diào)度,因此yield()實際上是一個與鎖不是很有關(guān)系的方法,和sleep()一樣,作為當(dāng)前線程的主動方法,不會引起死鎖。

4. 參考文章

Java總結(jié)篇系列:Java多線程

Java并發(fā)編程:Callable,Future和FutureTask

線程的狀態(tài)、分類及優(yōu)先級

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容