java 執行緒學習

weiweiyi發表於2023-05-07

感覺自己java基礎知識學的太差,記錄一下自己學習 深入淺出多執行緒 的筆記。

筆記連線:
https://flowus.cn/share/d4a68486-0162-4759-816e-551b275218f3
【FlowUs 息流】

1.程式和執行緒基本概念

程式產生的背景: 最初計算機只能輸入一次指令然後執行一次,效率較低。後來有了批處理系統,使用者可以將一串的指令交給計算機,由計算機依次處理。但是還是序列處理,隨程式的阻塞而阻塞。

為了解決這個問題,出現了程式的概念:指正在執行的一個程式或應用程式,記憶體中可以存在多個程式。CPU採用時間片輪轉的方式執行程式。 每個程式的時間片結束後,把CPU分配給下一個程式,假如當前程式任務沒結束,則儲存當前程式的資訊,等待下一次分配;

然而,人們並不滿足於此。如果一個程式包含多個子任務,程式也只能逐個執行,效率不高。

那麼能不能讓這些子任務同時執行呢?於是人們又提出了執行緒的概念,那麼能不能讓這些子任務同時執行呢?於是人們又提出了執行緒的概念,讓一個執行緒執行一個子任務,這樣一個程式就包含了多個執行緒,每個執行緒負責一個單獨的子任務。
例如: 一個下載軟體可以使用多個執行緒同時下載多個檔案,從而大大縮短下載時間。

執行緒和程式:
image.png

2. Thread類和Runnable介面

自定義Thread類:

public class Demo {
    public static class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("MyThread");
        }
    }

    public static void main(String[] args) {
        Thread myThread = new MyThread();
        myThread.start();
    }
}
  • start()方法後,該執行緒才算啟動
  • 呼叫了start()方法後,虛擬機器會建立一個執行緒,然後等到這個執行緒第一次得到時間片時再呼叫run()方法。
  • 第二次呼叫start()方法會丟擲IllegalThreadStateException異常。、

Thread 類是一個 Runnable 介面的實現類。

@FunctionalInterface
public interface Runnable {
    public abstract void run();
}

Runnable介面只有一個 run 方法, 並且有 @FunctionalInterface 註解。意思是:函式式介面, 介面裡面只能有一個抽象方法

說明我們可以使用 Lambda 表示式

  public static void main(String[] args) {

        new Thread(new MyThread()).start();

        // Java 8 函數語言程式設計,可以省略MyThread類
        new Thread(() -> {
            System.out.println("Java 8 匿名內部類");
        }).start();
    }

2.1 執行緒優先順序

  • 每個Java程式都有一個預設的主執行緒,就是透過JVM啟動的第一個執行緒main執行緒。
  • Java中執行緒優先順序可以指定,範圍是1~10。Java預設的執行緒優先順序為5,
  • Java中的優先順序來說不是特別的可靠,Java程式中對執行緒所設定的優先順序只是給作業系統一個建議,作業系統不一定會採納。而真正的呼叫順序,是由作業系統的執行緒排程演算法決定的

例如我們可以使用如下的程式碼測試一下:

    public static class T1 extends Thread {
        @Override
        public void run() {
            super.run();
            System.out.println(String.format("當前執行的執行緒是:%s,優先順序:%d",
                    Thread.currentThread().getName(),
                    Thread.currentThread().getPriority()));
        }
    }

    @Test
    void contextLoads() throws ExecutionException, InterruptedException {
        IntStream.range(1, 10).forEach(i -> {
            Thread thread = new Thread(new T1());
            thread.setPriority(i);
            thread.start();
        });
    }

可以看到不完全按照優先順序來, 但是大致差不多。
image.png

3. 執行緒組(ThreadGroup)

Java中用ThreadGroup來表示執行緒組,我們可以使用執行緒組對執行緒進行批次控制。

ThreadGroup是一個標準的向下引用的樹狀結構。
image.png

頂層的 ThreadGroup 被稱為系統執行緒組(system thread group),由 JVM 執行時建立並擁有它。

除了系統執行緒組外,每個 ThreadGroup 物件都有一個父 ThreadGroup ,即直接包含它的那個 ThreadGroup 。

為什麼這麼設計呢?

節省資源:根據樹形結構,可以很容易遞迴回收該 ThreadGroup 中的執行緒或執行緒組,避免了記憶體洩漏和資源浪費等問題。

確保執行緒安全:透過 ThreadGroup 可以限制某些執行緒的執行許可權,比如可以設定特定的許可權來防止執行緒意外獲得系統特權,確保執行緒不會危及系統安全。可以透過重寫 ThreadGroup 的 checkAccess 方法來實現限制某些執行緒的執行許可權。

方便監控和除錯:透過 ThreadGroup 可以方便地檢視和操縱整個執行緒組中的所有執行緒,從而便於進行監控和除錯。

例如透過下面的方法就可以獲取當前執行緒組執行的所有執行緒:

IntStream.range(1, 10).forEach(i -> {
            Thread thread = new Thread(() -> {
                try {
                    Thread.sleep(1000L);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
            thread.start();
        });
ThreadGroup threadGroup = Thread.currentThread().getThreadGroup();

Thread[] threads = new Thread[threadGroup.activeCount()];

threadGroup.enumerate(threads);

結果:
image.png

4. Java執行緒的狀態

在現在的作業系統中,執行緒是被視為輕量級程式的,所以作業系統執行緒的狀態其實和作業系統程式的狀態比較相似的。

image.png

4.1 Java執行緒的6個狀態

// Thread.State 原始碼
public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}

4.1.1 NEW

文件中含義為: Thread state for a thread which has not yet started. 表示尚未啟動的執行緒

Thread thread = new Thread(() -> {});
System.out.println(thread.getState()); // 輸出 NEW 

現在有兩個問題:

  1. 反覆呼叫同一個執行緒的start()方法是否可行?
  2. 假如一個執行緒執行完畢(此時處於TERMINATED狀態),再次呼叫這個執行緒的start()方法是否可行?

從原始碼分析:

 public synchronized void start() {
      
        if (threadStatus != 0)
            throw new IllegalThreadStateException();

        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 */
            }
        }
    }

從start0()看不出任何對state的操作。 我們只能從threadStatus 入手

可以看到 threadStatus 的變數。如果它不等於0,呼叫start()會直接丟擲異常的。

下面用程式碼測試一下:

@Test
public void testStartMethod() {
    Thread thread = new Thread(() -> {}, "myThread");
    thread.start(); // 第一次呼叫
    thread.start(); // 第二次呼叫
}

第一次:
image.png

第二次:
image.png

而前面說到:如果它不等於0,呼叫start()會直接丟擲異常的。故答案是否,不能start兩次

image.png


那第二個問題: 假如一個執行緒執行完畢(此時處於 TERMINATED 狀態),再次呼叫這個執行緒的start()方法是否可行?

從獲取執行緒狀態原始碼分析:

    // 返回執行緒的狀態
    public State getState() {
        // get current thread state
        return sun.misc.VM.toThreadState(threadStatus);
    }
    
     public static Thread.State toThreadState(int var0) {
        if ((var0 & 4) != 0) {
            return State.RUNNABLE;
        } else if ((var0 & 1024) != 0) {
            return State.BLOCKED;
        } else if ((var0 & 16) != 0) {
            return State.WAITING;
        } else if ((var0 & 32) != 0) {
            return State.TIMED_WAITING;
        } else if ((var0 & 2) != 0) {
            return State.TERMINATED;
        } else {
            return (var0 & 1) == 0 ? State.NEW : State.RUNNABLE;
        }
    }

獲取執行緒的狀態是透過 toThreadState(int var0)函式獲取的, 引數就是 threadStatus

透過 var0和 數字按位運算得到結果。

如果 (var0 & 4) != 0 成立,表示 var0 的第 3 位(從右往左數,從 0 開始計數)是 1,即表示執行緒狀態為 RUNNABLE;如果 (var0 & 1024) != 0 成立,表示 var0 的第 11 位是 1,即表示執行緒狀態為 BLOCKED。

回到剛才的問題,當執行緒處於 TERMINATED 狀態, 說明 var0 的第3位為0, 即 threadStatus ! = 0

從之前的程式碼就可以看出會丟擲 IllegalThreadStateException 異常。故答案也是否

4.1.2 RUNNABLE

Java執行緒的RUNNABLE狀態其實是包括了傳統作業系統執行緒的readyrunning兩個狀態。

4.1.3 BLOCKED

阻塞狀態。處於BLOCKED狀態的執行緒正等待鎖的釋放以進入同步區。

可以類比為一個人正在使用一部電話,而另一個人也想要使用電話。由於電話只有一個,因此另一個人必須等待當前使用者結束後才能使用。在此期間,他就處於blocked狀態。因為他知道那部電話已經被佔用了,不能立即使用。

4.1.4 WAITING

等待狀態。處於等待狀態的執行緒變成RUNNABLE狀態需要其他執行緒喚醒。

呼叫如下3個方法會使執行緒進入等待狀態:

  • Object.wait():使當前執行緒處於等待狀態直到另一個執行緒喚醒它;
  • Thread.join():等待執行緒執行完畢,底層呼叫的是Object例項的wait方法;
  • LockSupport.park():除非獲得呼叫許可,否則禁用當前執行緒進行執行緒排程。

4.1.5 TIMED_WAITING

超時等待狀態。執行緒等待一個具體的時間,時間到後會被自動喚醒。自動喚醒後,擁有了去爭奪鎖的資格。

呼叫如下方法會使執行緒進入超時等待狀態:

  • Thread.sleep(long millis):使當前執行緒睡眠指定時間;
  • Object.wait(long timeout):執行緒休眠指定時間,等待期間可以透過notify()/notifyAll()喚醒;
  • Thread.join(long millis):等待當前執行緒最多執行millis毫秒,如果millis為0,則會一直執行;
  • LockSupport.parkNanos(long nanos): 除非獲得呼叫許可,否則禁用當前執行緒進行執行緒排程指定時間;
  • LockSupport.parkUntil(long deadline):同上,也是禁止執行緒進行排程指定時間;

4.1.6 TERMINATED

終止狀態。此時執行緒已執行完畢。

image.png

相關文章