Java多線程拾遺(一)——重新簡單認識線程

前言

針對Java多線程,其實一直都些零散的學習,面試中多線程也是經常被問到的一塊,這次還真想認證的總結好這個,實在不想在這塊栽跟頭了。這次根據《Java 高併發編程詳解》一書進行學習和總結。

先來個簡單實例

@Slf4j
public class StepIn {

    public static void main(String[] args) {
    	//啓動一個線程
        new Thread(){
            @Override
            public void run() {
                enjoyMusic();
            }
        }.start();
        browseNews();
    }

    private static void browseNews(){
        while(true){
            log.info("Uh-hh,the good news.");
            sleep(1);
        }
    }

    private static void enjoyMusic(){
        while(true){
            log.info("Uh-hh,the nice music.");
            sleep(1);
        }
    }

    private static void sleep(int seconds){
        try {
            TimeUnit.SECONDS.sleep(seconds);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

運行起來之後,利用JConsole連接到指定的JVM進程,查看結果如下

在這裏插入圖片描述

可以很正常的看到會有一個main線程和自己啓動的Thread-0線程,還有其他的一些守護線程(什麼是守護線程後續會詳談)。每一個線程都有自己的局部變量表,程序計數器,以及生命週期

線程的生命週期

線程執行了Thread的start方法就代表該線程已經開始執行了麼?線程的生命週期大體可以分爲如下5個主要的階段,具體如下圖所示

在這裏插入圖片描述

一個線程的生命週期大體可以分爲5個主要的階段:NEW,RUNNABLE,RUNNING,BLOCKED,TERMINATED。

NEW狀態

當通過new Thread()創建一個Thread對象的時候,並沒有做線程啓動的操作,這個時候線程並不處於執行狀態,因爲沒有調用start啓動線程,線程這個時候只能爲NEW狀態。

RUNNABLE狀態

線程對象進入RUNNABLE狀態必須調用start方法,此時纔是真正地在JVM中創建了一個線程,線程啓動之後並不能立即執行,必須得到CPU選中,如果被CPU選中,則會進入Running狀態。

順便說一下,線程無法從RUNNABLE狀態直接進入TERMINATED狀態。

RUNNING狀態

CPU選中了該線程,該線程才真正進入到了運行狀態,此時它才能真正的執行自己的代碼邏輯,一個在RUNNING狀態的線程,線程可能發送如下轉換

由RUNNING直接進入TERMINATED狀態,比如調用stop方法(雖然不建議使用)

由RUNNING狀態進入BLOCKED狀態,比如調用sleep或者wait方法,或者進行某個阻塞的IO狀態,獲取某個鎖資源,

由RUNNING狀態進入RUNNABLE狀態,比如CPU的調度器輪詢使該線程放棄執行,或由於主動調用了yield方法,放棄CPU的執行權。

BLOCKED狀態

由RUNNING進入到BLOCKED的狀態我們已經介紹過了。BLOCKED狀態切換成其他狀態的可能如下:

1、直接進入TERMINATED狀態,比如調用stop方法

2、直接進入RUNNABLE狀態,線程阻塞的操作結束,比如讀取到了想要的字節,或者完成了指定時間的休眠,或者獲取到了某個鎖資源,或者在阻塞過程中被打斷(比如其他線程調用interrupt方法)。

TERMINATED狀態

這是一個線程的最終狀態,該狀態不會有任何切換。線程運行出錯,或者JVM Crash都會導致所有線程直接進入該狀態

爲什麼要有Runnable接口

談到有幾種方式創建線程,我們的第一反應都是兩種(其實還有其他的)——繼承Thread類,實現Runnable接口。但是,爲啥會有兩種方式,爲啥不直接實現一個Runnable接口就完事了?

其實有兩個原因:其中一個是如果一個類已經繼承了某個類,再想繼承Thread類就很難了,所以有了一個Runnable接口,但是還有另外一個原因,這個原因需要從下面一段程序說起,這個我們後面會討論。

這裏先看看start方法中的源碼

/**
 * Causes this thread to begin execution; the Java Virtual Machine
 * calls the <code>run</code> method of this thread.
 * <p>
 * The result is that two threads are running concurrently: the
 * current thread (which returns from the call to the
 * <code>start</code> method) and the other thread (which executes its
 * <code>run</code> method).
 * <p>
 * It is never legal to start a thread more than once.
 * In particular, a thread may not be restarted once it has completed
 * execution.
 *
 * @exception  IllegalThreadStateException  if the thread was already
 *               started.
 * @see        #run()
 * @see        #stop()
 */
public synchronized void start() {
    /**
     * This method is not invoked for the main method thread or "system"
     * group threads created/set up by the VM. Any new functionality added
     * to this method in the future may have to also be added to the VM.
     *
     * A zero status value corresponds to state "NEW".
     */
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    /* Notify the group that this thread is about to be started
     * so that it can be added to the group's list of threads
     * and the group's unstarted count can be decremented. */
    group.add(this);

    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
              it will be passed up the call stack */
        }
    }
}

private native void start0();

可以看出,start源碼是比較簡單的,核心無非就是調用了一個start0()的本地方法。但是我們繼承Thread的時候,複寫的是run方法,而不是start方法?其實從註釋中可以看到這一點:Causes this thread to begin execution; the Java Virtual Machine calls the run method of this thread.這句話的翻譯就是在開始執行這個線程時,JVM將會調用該線程的run方法,換言之,run 方法是被JNI方法start0() 調用的(這一點和模板方法模式有點像),仔細閱讀start的源碼將會總結出如下幾點。

1、Thread被new之後,Thread內部的threadStatus爲0,這個時候只表示線程被初始化,並沒有啓動

2、不能兩次啓動線程,否則會出現IllegalThreadStateException。

3、group.add(this);線程啓動之後會被加入一個線程組中。

4、線程已經結束(即線程運行結束)或者啓動,threadStatus不爲0,是不能再次啓動的。

從模板方法說起

來一個簡單的模板方法的實例

有一個抽象類

@Slf4j
public abstract class AbstractTemplate {

    //具體的實現交給子類
    public abstract String sayHello(String name);

    public void printHelloMessage(String name){
        log.info("print hello message : {}",sayHello(name));
    }

}

兩個實現類

public class TemplateOne extends AbstractTemplate {
    public String sayHello(String name) {
        return "中文的問候方法:你好"+name;
    }
}
public class TemplateTwo extends AbstractTemplate {
    public String sayHello(String name) {
        return "English say Hello : "+name;
    }
}

運行類

public class TemplateTest {
    public static void main(String[] args) {
        AbstractTemplate template = new TemplateOne();
        template.printHelloMessage("liman");

        AbstractTemplate templateTwo = new TemplateTwo();
        templateTwo.printHelloMessage("liman");
    }
}

運行結果:

在這裏插入圖片描述
其實這個比較簡單,無法就是真正的邏輯交給子類去是實現,然後在通過父類調用具體的方法的時候,會自動調用到子類的實現邏輯,這其實充分利用了多態的特點。

繼承Thread與實現Runnable接口

現在回到我們之前的問題,這兩者有何區別,除了方便繼承之外還有沒有其他值得考量的地方?這個我們先從Thread類中的run方法來看看

@Override
public void run() {
    //如果構造Thread的時候傳遞了Runnable接口,則會調用Runnable接口對應的run方法
    if (target != null) {
        target.run();
    }
    //如果沒有傳遞一個實現了Runnable接口的實例,則需要自己複寫run方法
}

在看看Thread其中的一個構造函數

public Thread(Runnable target) {
    init(null, target, "Thread-" + nextThreadNum(), 0);
}

可以看到這裏的Thread的構造方法接受一個Runnable參數。

之前流傳着一種說法——問:創建線程有幾種方式,有些人答:兩種,一種是實現Runnable接口,另一種是繼承Thread類。至少現在看來,這句話還是有些膚淺的。前面的start方法中我們看到,run只是一個模板方法,本地方法start0中回去調用這個對應子類的run方法。其實這兩種方式都是殊途同歸。從實質上看,創建線程只有一種方式就是構造Thread類,創建線程執行單元的方式有兩種,一種是重寫Thread的run方法,另一種是實現Runnable接口的run方法,第二種方法將Runnab實例作爲Thread的構造參數。

繼承Thread

@Slf4j
public class TicketWindow extends Thread {

    private final String name;
    private static final int MAX = 500;
    private static int index = 1; //用static修飾

    public TicketWindow(String name){
        this.name = name;
    }

    @Override
    public void run() {
        while(index<=MAX){
            log.info("櫃檯名稱:{},當前編號:{}",name,index++);
        }
    }

    public static void main(String[] args) {
        TicketWindow ticketWindow0 = new TicketWindow("一號叫號機");
        TicketWindow ticketWindow1 = new TicketWindow("二號叫號機");
        TicketWindow ticketWindow2 = new TicketWindow("三號叫號機");
        TicketWindow ticketWindow3 = new TicketWindow("四號叫號機");
        TicketWindow ticketWindow4 = new TicketWindow("五號叫號機");
        ticketWindow0.start();
        ticketWindow1.start();
        ticketWindow2.start();
        ticketWindow3.start();
        ticketWindow4.start();
    }
}

這種情況下會存在差別,如果index用static修飾,纔在一定程度上避免了一個號被多個TicketWindow消費的情況,如果去掉了static修飾,則會出現多個TicketWindow打印同一個index值的情況。

實現runnable接口

@Slf4j
public class TicketWindowRunnable implements Runnable {

    private final String name;
    private static final int MAX = 500;
    private int index = 1;//未用static修飾
    
    public TicketWindowRunnable(String name){
        this.name = name;
    }

    public void run() {
        while(index<=MAX){
            log.info("櫃檯名稱:{},當前編號:{}",name,index++);
        }
    }
}

實現Runnable接口之後就會好得多,index不需要static修飾就能在一定程度上做到index共享,這做到了業務數據和業務操作的分離。但是index的值如果過大同樣還是會存在線程安全的問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章