1. 程式人生 > 程式設計 >執行緒最最基礎的知識

執行緒最最基礎的知識

Java 多執行緒系列文章第 5 篇。

什麼是執行緒

試想一下沒有執行緒的程式是怎麼樣的?百度網盤在上傳檔案時就無法下載檔案了,得等檔案上傳完成後才能下載檔案。這個我們現在看起來很反人性,因為我們習慣了一個程式同時可以進行執行多個功能,而這些都是執行緒的功勞。

之前的文章 程式知多少 中講到,為了實現多個程式並行執行,引入了程式概念。現在引入執行緒是為了讓一個程式能夠併發執行。

執行緒的組成

執行緒ID:執行緒識別符號。

當前指令指標(PC):指向要執行的指令。

暫存器集合:儲存單元暫存器的集合。

堆疊:暫時存放資料和地址,一般用來保護斷點和現場。

執行緒與程式區別

執行緒和程式之間的區別,我覺得可以用這個例子來看出兩者的不同,程式就是一棟房子,房子住著 3 個人,執行緒就是住在房子裡的人。程式是一個獨立的個體,有自己的資源,執行緒是在程式裡的,多個執行緒共享著程式的資源。

執行緒狀態

我們看到 Java 原始碼裡面,執行緒狀態的列舉有如下 6 個。

public enum State {

 //新建狀態
 NEW,//執行狀態
 RUNNABLE,//阻塞狀態
 BLOCKED,//等待狀態
 WAITING,//等待狀態(區別在於這個有等待的時間)
 TIMED_WAITING,//終止狀態
 TERMINATED;
}
複製程式碼

下面給這 6 個狀態一一做下解釋。

NEW:新建狀態。在建立完 Thread ,還沒執行 start() 之前,執行緒的狀態一直是 NEW。可以說這個時候還沒有真正的一個執行緒對映著,只是一個物件。

RUNNABLE:執行狀態。執行緒物件呼叫 start() 之後,就進入 RUNNABLE 狀態,該狀態說明在 JVM 中有一個真實的執行緒存在。

BLOCKED:阻塞狀態。執行緒在等待鎖的釋放,也就是等待獲取 monitor 鎖。

WAITING:等待狀態。執行緒在這個狀態的時候,不會被分配 CPU,而且需要被顯示地喚醒,否則會一直等待下去。

TIMED_WAITING:超時等待狀態。這個狀態的執行緒也一樣不會被分配 CPU,但是它不會無限等待下去,有時間限制,時間一到就停止等待。

TERMINATED:終止狀態。執行緒執行完成結束,但不代表這個物件已經沒有了,物件可能還是存在的,只是執行緒不存在了。

執行緒既然有這麼多個狀態,那肯定就有狀態機,也就是在什麼情況下 A 狀態會變成 B 狀態。下面就來簡單描述一下。

結合下圖,我們 new 出執行緒類的時候,就是 NEW

狀態,呼叫 start() 方法,就進入了 RUNNABLE 狀態,這時如果觸發等待,則進入了 WAITING 狀態,如果觸發超時等待,則進入 TIMED_WAITING 狀態,當訪問需要同步的資源時,則只有一個執行緒能訪問,其他執行緒就進入 BLOCKED 狀態,當執行緒執行完後,進入 TERMINATED 狀態。

圖片來源於網路,侵刪

其實在 JVM 中,執行緒是有 9 個狀態,如下所示,有興趣的同學可以深入瞭解一下。

javaClasses.hpp
enum ThreadStatus {
    NEW = 0,RUNNABLE = JVMTI_THREAD_STATE_ALIVE + // runnable / running
                               JVMTI_THREAD_STATE_RUNNABLE,SLEEPING = JVMTI_THREAD_STATE_ALIVE + // Thread.sleep()
                               JVMTI_THREAD_STATE_WAITING +
                               JVMTI_THREAD_STATE_WAITING_WITH_TIMEOUT +
                               JVMTI_THREAD_STATE_SLEEPING,IN_OBJECT_WAIT = JVMTI_THREAD_STATE_ALIVE + // Object.wait()
                               JVMTI_THREAD_STATE_WAITING +
                               JVMTI_THREAD_STATE_WAITING_INDEFINITELY +
                               JVMTI_THREAD_STATE_IN_OBJECT_WAIT,IN_OBJECT_WAIT_TIMED = JVMTI_THREAD_STATE_ALIVE + // Object.wait(long)
                               JVMTI_THREAD_STATE_WAITING +
                               JVMTI_THREAD_STATE_WAITING_WITH_TIMEOUT +
                               JVMTI_THREAD_STATE_IN_OBJECT_WAIT,PARKED = JVMTI_THREAD_STATE_ALIVE + // LockSupport.park()
                               JVMTI_THREAD_STATE_WAITING +
                               JVMTI_THREAD_STATE_WAITING_INDEFINITELY +
                               JVMTI_THREAD_STATE_PARKED,PARKED_TIMED = JVMTI_THREAD_STATE_ALIVE + // LockSupport.park(long)
                               JVMTI_THREAD_STATE_WAITING +
                               JVMTI_THREAD_STATE_WAITING_WITH_TIMEOUT +
                               JVMTI_THREAD_STATE_PARKED,BLOCKED_ON_MONITOR_ENTER = JVMTI_THREAD_STATE_ALIVE + // (re-)entering a synchronization block
                               JVMTI_THREAD_STATE_BLOCKED_ON_MONITOR_ENTER,TERMINATED = JVMTI_THREAD_STATE_TERMINATED
};
複製程式碼

Java 執行緒實現

下面講一講在 Java 中如何建立一個執行緒。眾所周知,實現 Java 執行緒有 2 種方式:繼承 Thread 類和實現 Runnable 介面。

繼承 Thread 類

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

class MyThread extends Thread {

    @Override
    public void run() {
        System.out.println("MyThread");
    }

}
複製程式碼

實現 Runnable 介面

實現 Runnable 介面,實現 run() 方法。

class MyRunnable implements Runnable {

    public void run() {
        System.out.println("MyRunnable");
    }

}
複製程式碼

這 2 種執行緒的啟動方式也不一樣。MyThread 是一個執行緒類,所以可以直接 new 出一個物件出來,接著呼叫 start() 方法來啟動執行緒;而 MyRunnable 只是一個普通的類,需要 new 出執行緒基類 Thread 物件,將 MyRunnable 物件傳進去。

下面是啟動執行緒的方式。

public class ThreadImpl {

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        Thread myRunnable = new Thread(new MyRunnable());
        System.out.println("main Thread begin");
        myThread.start();
        myRunnable.start();
        System.out.println("main Thread end");
    }

}
複製程式碼

列印結果如下:

main Thread begin
main Thread end
MyThread
MyRunnable
複製程式碼

看這結果,不像咱們之前的序列執行依次列印,主執行緒不會等待子執行緒執行完。

敲重點:不能直接呼叫 run(),直接呼叫 run() 不會建立執行緒,而是主執行緒直接執行 run() 的內容,相當於執行普通函式。這時就是序列執行的。看下面程式碼。

public class ThreadImpl {

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        Thread myRunnable = new Thread(new MyRunnable());
        System.out.println("main Thread begin");
        myThread.run();
        myRunnable.run();
        System.out.println("main Thread end");
    }

}
複製程式碼

列印結果:

main Thread begin
MyThread
MyRunnable
main Thread end
複製程式碼

從結果看出只是序列的,但看不出沒有執行緒,我們看下面例子來驗證直接呼叫 run() 方法沒有建立新的執行緒,使用 VisualVM 工具來觀察執行緒情況。

我們對程式碼做一下修改,加上 Thread.sleep(1000000) 讓它睡眠一段時間,這樣方便用工具檢視執行緒情況。

呼叫 run() 的程式碼:

public class ThreadImpl {

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.setName("MyThread");
        Thread myRunnable = new Thread(new MyRunnable());
        myRunnable.setName("MyRunnable");
        System.out.println("main Thread begin");
        myThread.run();
        myRunnable.run();
        System.out.println("main Thread end");
        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

class MyThread extends Thread {

    @Override
    public void run() {
        System.out.println("MyThread");
        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

class MyRunnable implements Runnable {

    public void run() {
        System.out.println("MyRunnable");
        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}
複製程式碼

執行結果:

main Thread begin
MyThread
複製程式碼

只打印出 2 句日誌,觀察執行緒時也只看到 main 執行緒,沒有看到 MyThreadMyRunnable 執行緒,印證了上面咱們說的:直接呼叫 run() 方法,沒有建立執行緒

下面我們來看看有 呼叫 start() 的程式碼:

public class ThreadImpl {

    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.setName("MyThread");
        Thread myRunnable = new Thread(new MyRunnable());
        myRunnable.setName("MyRunnable");
        System.out.println("main Thread begin");
        myThread.start();
        myRunnable.start();
        System.out.println("main Thread end");
        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
	
}
複製程式碼

執行結果:

main Thread begin
main Thread end
MyThread
MyRunnable
複製程式碼

所有日誌都打印出來了,並且通過 VisualVM 工具可以看到 MyThreadMyRunnable 執行緒。看到了這個結果,切記建立執行緒要呼叫 start() 方法。

今天就先講到這,繼續關注後面的內容。

推薦閱讀

執行緒最最基礎的知識

老闆叫你別阻塞了

吃個快餐都能學到序列、並行、併發

泡一杯茶,學一學同非同步

程式知多少?

設計模式看了又忘,忘了又看?

後臺回覆『設計模式』可以獲取《一故事一設計模式》電子書

覺得文章有用幫忙轉發&點贊,多謝朋友們!

LieBrother