Stories

Detail Return Return

Java 多線程揭秘:徹底掌握線程狀態轉換與控制方法 - Stories Detail

stateDiagram-v2
    [*] --> NEW: 創建線程對象
    NEW --> RUNNABLE: 調用start()
    RUNNABLE --> BLOCKED: 等待synchronized鎖
    BLOCKED --> RUNNABLE: 獲得鎖
    RUNNABLE --> WAITING: 調用wait()/join()/park()
    WAITING --> RUNNABLE: 調用notify()/notifyAll()/unpark()或目標線程結束
    RUNNABLE --> TIMED_WAITING: 調用sleep(time)/wait(time)/join(time)
    TIMED_WAITING --> RUNNABLE: 時間到期或被中斷
    RUNNABLE --> TERMINATED: 運行結束
    TERMINATED --> [*]

引言

各位開發者好!在上一篇文章中,我們詳細介紹了 Java 多線程的四種創建方式。今天,我們將深入探討線程的生命週期和基礎操作方法,這些知識對於理解多線程程序的行為和調試線程問題至關重要。

很多初學者在多線程編程中遇到的困惑,往往源於對線程狀態轉換和控制方法的理解不足。比如:為什麼我的線程沒有執行?為什麼線程無法停止?如何優雅地結束一個線程?今天,我們就來一一解答這些問題。

一、線程的六大狀態詳解

Java 中的 Thread 類定義了線程的六種狀態,這些狀態定義在 Thread 類的內部枚舉 State 中:

public enum State {
    NEW,
    RUNNABLE,
    BLOCKED,
    WAITING,
    TIMED_WAITING,
    TERMINATED;
}

下面我們詳細解析每一種狀態及其轉換過程:

1. NEW(新建)

當你創建一個 Thread 對象但還沒有調用 start()方法時,線程處於 NEW 狀態。

Thread thread = new Thread(() -> {
    System.out.println("線程任務執行");
});
thread.setName("worker-thread"); // 給線程起一個有意義的名稱
System.out.println("創建後線程狀態:" + thread.getState()); // 輸出: NEW

2. RUNNABLE(可運行)

調用 start()方法後,線程進入 RUNNABLE 狀態。在 Java 中,RUNNABLE 狀態包含了操作系統層面的"就緒"和"運行中"兩個狀態:

  • 就緒:線程已經準備好運行,但等待 CPU 分配時間片
  • 運行中:線程正在 CPU 上執行

重要説明:Java 中的 RUNNABLE 狀態是一個複合狀態,不區分線程是"就緒"還是"正在運行",這與操作系統的線程狀態模型不同。即使線程獲得了 CPU 時間片正在執行,在 Java API 看來它仍然是 RUNNABLE 狀態,無法通過 Thread.getState()區分線程是否正在 CPU 上執行。

thread.start();
System.out.println("啓動後線程狀態:" + thread.getState()); // 輸出: RUNNABLE
graph TD
    A[操作系統線程狀態] --> B[就緒Ready]
    A --> C[運行中Running]
    A --> D[阻塞Blocked]
    B --> J[Java RUNNABLE]
    C --> J
    D --> K[Java BLOCKED/WAITING/TIMED_WAITING]

    style J fill:#9cf,stroke:#333,stroke-width:2px
    style K fill:#f9a,stroke:#333,stroke-width:2px

3. BLOCKED(阻塞)

線程被阻塞,等待獲取一個 synchronized 內置鎖(也稱 monitor 鎖)。當線程嘗試進入一個 synchronized 塊/方法,但該鎖被其他線程持有時,就會進入這個狀態。

Object lock = new Object();

Thread thread1 = new Thread(() -> {
    synchronized (lock) {
        try {
            Thread.sleep(3000); // 持有鎖3秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
});
thread1.setName("lock-holder");

Thread thread2 = new Thread(() -> {
    synchronized (lock) {
        System.out.println("線程2獲取到了鎖");
    }
});
thread2.setName("lock-waiter");

thread1.start(); // 先啓動線程1
Thread.sleep(100); // 確保線程1先獲取到鎖
thread2.start(); // 再啓動線程2
Thread.sleep(100); // 給線程2一點時間嘗試獲取鎖

System.out.println("線程2狀態:" + thread2.getState()); // 輸出: BLOCKED

4. WAITING(等待)

線程進入無限期等待狀態,需要其他線程執行特定操作後才能繼續。
主要由以下方法導致:

  • Object.wait()
  • Thread.join()
  • LockSupport.park()

特別説明:Object.wait()方法會釋放持有的 monitor 鎖(也就是 synchronized 鎖),而 LockSupport.park()不會釋放任何鎖,這是一個重要區別。

性能考慮:在高併發環境中,過多線程進入 WAITING 狀態可能導致系統資源浪費。建議使用帶超時參數的 wait(timeout),避免由於通知丟失導致線程永久等待。

Object lock = new Object();

Thread waitingThread = new Thread(() -> {
    synchronized (lock) {
        try {
            lock.wait(); // 進入WAITING狀態,同時釋放鎖
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
});
waitingThread.setName("waiting-thread");

waitingThread.start();
Thread.sleep(100); // 確保waitingThread進入等待狀態
System.out.println("等待中的線程狀態:" + waitingThread.getState()); // 輸出: WAITING

5. TIMED_WAITING(計時等待)

與 WAITING 類似,但有超時時間。以下方法會導致這個狀態:

  • Thread.sleep(long)
  • Object.wait(long)
  • Thread.join(long)
  • LockSupport.parkNanos()
  • LockSupport.parkUntil()
Thread sleepingThread = new Thread(() -> {
    try {
        Thread.sleep(5000); // 休眠5秒,進入TIMED_WAITING狀態
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
});
sleepingThread.setName("sleeping-thread");

sleepingThread.start();
Thread.sleep(100); // 確保sleepingThread進入休眠狀態
System.out.println("休眠中的線程狀態:" + sleepingThread.getState()); // 輸出: TIMED_WAITING

6. TERMINATED(終止)

線程執行完畢或因異常結束。

Thread terminatedThread = new Thread(() -> {
    // 執行一些簡短的任務
    System.out.println("任務執行完畢");
});
terminatedThread.setName("terminated-thread");

terminatedThread.start();
Thread.sleep(100); // 確保線程有足夠時間完成任務
System.out.println("任務完成後線程狀態:" + terminatedThread.getState()); // 輸出: TERMINATED

三種等待狀態的對比

狀態 觸發條件 是否釋放鎖 恢復條件 典型使用場景
BLOCKED 等待進入 synchronized 同步塊/方法 不持有鎖 獲得鎖 多線程競爭共享資源
WAITING Object.wait()
Thread.join()
LockSupport.park()
wait()釋放鎖
join/park 不涉及鎖釋放
notify/notifyAll
目標線程結束
unpark
線程協作,等待條件滿足
TIMED_WAITING Thread.sleep(time)
Object.wait(time)
Thread.join(time)
sleep 不釋放鎖
wait 釋放鎖
join 不涉及鎖釋放
時間到期或上述對應條件 超時等待,避免無限阻塞
graph TD
    A[運行中的線程] -->|嘗試獲取被佔用的synchronized鎖| B[BLOCKED]
    A -->|"調用wait()"| C[WAITING]
    A -->|"調用sleep(time)"| D[TIMED_WAITING]
    B -->|獲得鎖| A
    C -->|"其他線程調用notify()"| A
    D -->|時間到期| A

    style A fill:#9cf,stroke:#333
    style B fill:#f9a,stroke:#333
    style C fill:#ffc,stroke:#333
    style D fill:#cfc,stroke:#333

二、start()與 run()的本質區別

初學者常犯的一個錯誤是直接調用線程的 run()方法,而不是 start()方法。這兩者有本質區別:

調用 run()

Thread thread = new Thread(() -> {
    System.out.println("當前線程: " + Thread.currentThread().getName());
});
thread.run(); // 直接調用run方法

輸出:當前線程: main

調用 start()

Thread thread = new Thread(() -> {
    System.out.println("當前線程: " + Thread.currentThread().getName());
});
thread.start(); // 調用start方法

輸出:當前線程: Thread-0

區別分析

  • run(): 普通方法調用,在當前線程(通常是 main 線程)執行線程體,沒有創建新線程
  • start(): 啓動新線程,在新線程中執行 run()方法,實現了多線程併發執行

底層原理:start()方法會調用 native 方法 start0(),該方法會在 JVM 層面創建一個新的操作系統線程,並設置線程狀態,最終導致 run()方法在新線程中執行。

graph TD
    A[Thread對象創建] --> B{調用方法?}
    B -->|"thread.run()"| C[在當前線程中執行run方法]
    B -->|"thread.start()"| D[創建新的操作系統線程]
    D --> E[在新線程中執行run方法]
    C --> F[無併發效果]
    E --> G[實現多線程併發]

    style C fill:#f9a,stroke:#333
    style E fill:#9cf,stroke:#333
    style F fill:#f9a,stroke:#333
    style G fill:#9cf,stroke:#333

常見錯誤案例

// 錯誤用法:重複調用start()
Thread thread = new Thread(() -> System.out.println("任務執行"));
thread.start();
thread.start(); // 拋出IllegalThreadStateException

// 錯誤理解:以為run()會啓動線程
Thread thread2 = new Thread(() -> {
    for(int i=0; i<1000; i++) {
        System.out.println(i);
    }
});
thread2.run(); // 在主線程中順序執行,沒有併發效果

三、線程控制方法詳解

Java 提供了幾個重要的線程控制方法,下面我們來詳細解析:

1. sleep() - 線程休眠

使當前線程暫停執行指定的時間,進入 TIMED_WAITING 狀態,但不會釋放鎖。

public static void sleepDemo() {
    Object lock = new Object();

    Thread thread = new Thread(() -> {
        synchronized (lock) {
            System.out.println("線程獲取到鎖");
            try {
                System.out.println("線程開始休眠5秒");
                Thread.sleep(5000);
                System.out.println("線程休眠結束");
            } catch (InterruptedException e) {
                System.out.println("線程被中斷");
            }
        }
    });
    thread.setName("sleeping-thread");

    thread.start();

    // 給點時間讓線程啓動並獲取鎖
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    // 嘗試獲取鎖,會被阻塞直到上面的線程釋放鎖
    synchronized (lock) {
        System.out.println("主線程獲取到鎖");
    }
}

要點

  • sleep 是 Thread 類的靜態方法,會暫停當前正在執行的線程
  • 不會釋放鎖資源
  • 可以被 interrupt()方法中斷,拋出 InterruptedException
  • sleep 結束後線程會自動回到 RUNNABLE 狀態
  • 長時間的 sleep()會佔用線程資源而不執行工作,在線程池環境中可能導致性能下降
  • 建議配合合理的超時機制使用,避免無限期阻塞

2. yield() - 線程讓步

提示調度器當前線程願意放棄 CPU 使用權,但調度器可以忽略這個提示。

public static void yieldDemo() {
    Thread thread1 = new Thread(() -> {
        for (int i = 0; i < 100; i++) {
            System.out.println("線程1: " + i);
            if (i % 10 == 0) {
                System.out.println("線程1讓步");
                Thread.yield();
            }
        }
    });
    thread1.setName("yield-thread");

    Thread thread2 = new Thread(() -> {
        for (int i = 0; i < 100; i++) {
            System.out.println("線程2: " + i);
        }
    });
    thread2.setName("normal-thread");

    thread1.start();
    thread2.start();
}

要點

  • yield 是 Thread 類的靜態方法
  • 只是提示調度器,沒有強制性
  • 從運行狀態到就緒狀態的轉變(仍然是 RUNNABLE)
  • 實際效果取決於操作系統的實現,不可靠

yield()在現代系統中的實際效果

儘管 yield()方法的理論目的是讓出 CPU 時間片,但在現代操作系統和 JVM 實現中,它的實際效果往往不可預測:

  1. 大多數現代 CPU 調度器已經非常智能,能夠有效分配時間片
  2. 不同 JVM 實現和操作系統對 yield()的處理方式不同
  3. 在某些系統上,yield()可能完全沒有效果
  4. 在其他系統上,yield()可能導致當前線程被過度懲罰,長時間無法獲得 CPU

在實際開發中

  • 避免使用 yield()來解決線程協作問題
  • 如需控制線程執行順序,應使用顯式同步機制(如 CountDownLatch、CyclicBarrier 等)
  • 如需控制執行時間分配,考慮使用線程優先級或更高級的調度框架

3. join() - 線程等待

讓當前線程等待另一個線程執行完畢後再繼續執行。

public static void joinDemo() {
    Thread worker = new Thread(() -> {
        System.out.println("工作線程開始執行...");
        try {
            // 模擬耗時操作
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("工作線程執行完畢");
    });
    worker.setName("worker-thread");

    System.out.println("主線程啓動工作線程");
    worker.start();

    System.out.println("主線程等待工作線程完成");
    try {
        worker.join(); // 主線程在這裏等待worker線程執行完畢
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    System.out.println("主線程繼續執行");
}

join()方法的重載版本

  • join(): 等待線程終止
  • join(long millis): 等待指定的毫秒數
  • join(long millis, int nanos): 等待指定的毫秒數加納秒數

join()方法的內部實現原理

// join()方法的內部實現原理(簡化版)
public final synchronized void join(long millis) throws InterruptedException {
    if (millis > 0) {
        if (isAlive()) {
            // 當前線程在目標線程對象上等待
            wait(millis);
        }
    } else if (millis == 0) {
        while (isAlive()) {
            // 無限期等待
            wait(0);
        }
    }
    // 注意:當目標線程終止時,JVM會調用notifyAll()喚醒所有等待線程
}

深入理解:當線程 A 調用線程 B 的 join()方法時,線程 A 會在線程 B 對象的監視器上等待。當線程 B 執行完畢(無論正常結束還是異常結束),JVM 會調用線程 B 對象的 notifyAll()方法,從而喚醒在其上等待的線程 A。這種設計確保了線程 A 能夠在線程 B 結束後繼續執行。

性能考慮

  • 無限期的 join()可能導致調用線程長時間等待,降低系統吞吐量
  • 在線程池環境中,線程長時間阻塞會降低線程池效率
  • 推薦使用 join(timeout)設置合理的等待超時時間
sequenceDiagram
    participant 主線程
    participant 工作線程對象
    participant 工作線程

    主線程->>工作線程對象: 創建
    主線程->>工作線程: start()
    工作線程->>工作線程: 執行run()方法
    主線程->>工作線程對象: join()
    工作線程對象->>主線程: wait()
    Note over 主線程: 主線程進入WAITING狀態
    工作線程->>工作線程: 完成任務
    Note over 工作線程: 進入TERMINATED狀態
    工作線程->>工作線程對象: JVM調用notifyAll()
    工作線程對象->>主線程: 喚醒
    Note over 主線程: 繼續執行

4. interrupt() - 線程中斷

這是一種協作式的線程中斷機制,用於通知線程應該停止或中斷當前工作。

public static void interruptDemo() {
    Thread sleepingThread = new Thread(() -> {
        try {
            System.out.println("線程開始休眠10秒");
            Thread.sleep(10000);
            System.out.println("休眠完成"); // 如果被中斷,這行不會執行
        } catch (InterruptedException e) {
            System.out.println("線程被中斷: " + e.getMessage());
        } finally {
            System.out.println("線程結束");
        }
    });
    sleepingThread.setName("interrupted-thread");

    sleepingThread.start();

    // 主線程休眠2秒後中斷sleepingThread
    try {
        Thread.sleep(2000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    System.out.println("主線程發出中斷請求");
    sleepingThread.interrupt();
}

中斷機制相關方法

  • interrupt(): 請求中斷線程
  • isInterrupted(): 檢查線程是否被中斷(不清除中斷狀態)
  • Thread.interrupted(): 靜態方法,檢查當前線程是否被中斷(清除中斷狀態)

非阻塞場景的中斷處理

在非阻塞狀態下,interrupt()方法只會設置線程的中斷標誌,不會導致線程拋出 InterruptedException。此時,線程需要主動檢查中斷狀態並作出響應:

public static void nonBlockingInterruptDemo() {
    Thread worker = new Thread(() -> {
        // 給線程起一個有意義的名稱
        Thread.currentThread().setName("worker-thread");

        // 執行計算密集型任務,定期檢查中斷狀態
        long sum = 0;
        System.out.println(Thread.currentThread().getName() + " 開始計算");

        while (!Thread.currentThread().isInterrupted()) {
            // 執行非阻塞計算
            for (int i = 0; i < 1_000_000; i++) {
                sum += i;
            }

            System.out.println("計算結果: " + sum);
            sum = 0;
        }

        System.out.println(Thread.currentThread().getName() + " 檢測到中斷信號,優雅退出");
    });

    worker.start();

    try {
        Thread.sleep(100); // 讓worker線程有時間開始工作
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    System.out.println("主線程發送中斷請求");
    worker.interrupt();
}

自定義阻塞方法的中斷處理

當使用顯式鎖(如 ReentrantLock)或自定義阻塞機制時,需要特別注意中斷處理:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.locks.Condition;

public static void customBlockingInterruptDemo() {
    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();

    Thread waitingThread = new Thread(() -> {
        try {
            // 可中斷的鎖獲取
            lock.lockInterruptibly();
            try {
                System.out.println("線程獲取到鎖,等待條件");
                // 等待條件,可被中斷
                condition.await();
                System.out.println("條件滿足,繼續執行");
            } finally {
                lock.unlock();
            }
        } catch (InterruptedException e) {
            System.out.println("線程在等待鎖或條件時被中斷: " + e.getMessage());
        }
    });
    waitingThread.setName("custom-waiting-thread");

    waitingThread.start();

    try {
        Thread.sleep(1000); // 確保等待線程已經進入等待狀態
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    System.out.println("主線程發送中斷請求");
    waitingThread.interrupt();
}

中斷機制的核心概念

  • interrupt()方法是一種協作式而非強制式的線程中斷機制
  • 線程可以選擇如何響應中斷請求,甚至可以完全忽略它
  • 良好的設計應確保線程能夠及時檢查並響應中斷請求
flowchart TD
    A[線程正常執行] -->|"調用interrupt()"| B{線程狀態?}
    B -->|阻塞狀態\nsleep/wait/join| C[拋出InterruptedException\n中斷標誌被清除]
    B -->|運行狀態| D[設置中斷標誌位]
    C -->|捕獲異常| E{是否需要退出?}
    D -->|"週期性檢查\nisInterrupted()"| F{標誌為true?}
    E -->|是| G[重設中斷標誌\n線程退出]
    E -->|否| H[繼續執行]
    F -->|是| I[執行清理\n線程退出]
    F -->|否| J[繼續執行]

    style C fill:#f9a,stroke:#333
    style D fill:#9cf,stroke:#333
    style G fill:#cfc,stroke:#333
    style I fill:#cfc,stroke:#333

isInterrupted() vs Thread.interrupted():

public static void interruptFlags() {
    Thread thread = new Thread(() -> {
        while (!Thread.currentThread().isInterrupted()) {
            System.out.println("線程工作中...");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // sleep()方法被中斷會清除中斷標誌
                // 需要重新設置中斷標誌位以便外層循環檢測
                Thread.currentThread().interrupt();
                System.out.println("中斷髮生,退出循環");
                break;
            }
        }
    });

    thread.start();

    // 主線程休眠3秒後中斷工作線程
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    thread.interrupt();
}

正確處理 InterruptedException 的模式

public void run() {
    try {
        while (!Thread.currentThread().isInterrupted()) {
            // 執行任務

            // 執行可能被中斷的阻塞操作
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // 重新設置中斷標誌並退出循環
                Thread.currentThread().interrupt();
                break;
            }
        }
    } finally {
        // 執行清理工作
        System.out.println("線程結束,執行清理");
    }
}

四、守護線程(Daemon Thread)

守護線程是一種特殊的線程,它在後台為其他非守護線程提供服務。當所有非守護線程結束時,無論守護線程是否完成工作,JVM 都會退出。

守護線程的特點

  1. 當 JVM 中只剩下守護線程時,JVM 會退出
  2. 必須在線程啓動前設置守護狀態
  3. 典型應用:垃圾回收器、監控線程、心跳線程等

守護線程示例

public static void daemonDemo() {
    Thread daemonThread = new Thread(() -> {
        while (true) {
            try {
                System.out.println("守護線程工作中...");
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                break;
            }
        }
    });
    daemonThread.setName("daemon-thread");

    // 設置為守護線程(必須在start之前)
    daemonThread.setDaemon(true);
    daemonThread.start();

    // 主線程工作3秒
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    System.out.println("主線程結束,程序將退出");
    // 程序退出,守護線程也會終止
}

為何守護線程的 finally 塊不保證執行?

當 JVM 準備退出時,如果只剩下守護線程,JVM 會強制終止這些線程而不等待它們完成當前工作。這意味着:

public static void daemonFinalizationDemo() {
    Thread daemon = new Thread(() -> {
        try {
            System.out.println("守護線程開始執行");
            Thread.sleep(5000); // 模擬長時間操作
            System.out.println("守護線程完成工作"); // 可能不會執行
        } catch (InterruptedException e) {
            System.out.println("守護線程被中斷");
        } finally {
            System.out.println("守護線程的finally塊"); // 可能不會執行
            // 關鍵資源清理如文件關閉、連接釋放可能不會發生
        }
    });

    daemon.setDaemon(true);
    daemon.start();

    // 主線程很快結束
    System.out.println("主線程結束,JVM準備退出");
}

守護線程應用場景

  1. 日誌收集線程:週期性地將日誌刷新到磁盤
  2. 緩存清理線程:後台清理過期緩存
  3. 心跳檢測線程:定期發送心跳包
  4. 服務監控線程:監控服務健康狀態

守護線程注意事項

  1. 守護線程創建的線程也是守護線程
  2. 守護線程不應該用於執行 I/O 操作,因為它們可能在操作完成前被強制終止
  3. finally 塊在守護線程中不保證一定會執行
  4. 不要將連接池或事務性操作放在守護線程中

實踐建議:不要在守護線程中執行以下操作:

  • 文件或數據庫操作(可能導致數據損壞)
  • 網絡連接管理(可能導致連接泄漏)
  • 事務處理(可能導致事務不完整)
graph TD
    A[JVM啓動] --> B[創建用户線程]
    A --> C[創建守護線程]
    B --> D[用户線程運行]
    C --> E[守護線程運行]
    D --> F[用户線程結束]
    E --> G[守護線程繼續]
    F -->|所有用户線程結束| H[JVM準備退出]
    G --> G
    H --> I[強制終止所有守護線程]
    I --> J[JVM退出]

    style B fill:#9cf,stroke:#333
    style C fill:#cfc,stroke:#333
    style D fill:#9cf,stroke:#333
    style E fill:#cfc,stroke:#333
    style F fill:#9cf,stroke:#333
    style G fill:#cfc,stroke:#333
    style I fill:#f9a,stroke:#333

五、線程常見問題及解決方案

1. 線程執行不成功

問題描述:創建了線程對象,但沒有執行任務

常見原因

  • 調用 run()而不是 start()
  • 線程對象被垃圾回收
  • 主線程結束太快

解決方案

// 正確啓動線程
Thread thread = new Thread(() -> {
    System.out.println("任務執行");
});
thread.start(); // 不是thread.run()

// 保持對線程的引用
// 如果需要,可以使用join等待線程完成

2. 線程無法停止

問題描述:嘗試停止一個正在運行的線程

常見錯誤:使用已廢棄的 stop()、suspend()方法

為什麼不應使用 stop()方法?

Thread.stop()方法在 Java 1.2 版本就被標記為廢棄,主要原因包括:

  1. 不安全的資源釋放:stop()會立即終止線程,不給線程任何機會執行清理工作,可能導致:
  • 文件句柄未關閉
  • 數據庫連接未釋放
  • 網絡套接字未關閉
  • 臨時文件未刪除
  1. 數據不一致:強制終止可能使對象處於不一致狀態
// 假設有轉賬操作
synchronized void transfer(Account from, Account to, int amount) {
    from.debit(amount); // 假如在這行之後被stop()
    to.credit(amount);  // 這行永遠不會執行
    // 結果:錢從from賬户扣除了,但沒有加到to賬户
}
  1. 鎖狀態不確定:線程被 stop()時會釋放所有鎖,但可能導致受保護數據的不一致狀態暴露給其他線程

推薦解決方案:使用中斷機制或狀態標誌位

// 通過標誌位控制線程結束
class StoppableTask implements Runnable {
    private volatile boolean stopRequested = false;

    public void requestStop() {
        stopRequested = true;
    }

    @Override
    public void run() {
        while (!stopRequested) {
            // 執行任務
            System.out.println("線程工作中...");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                // 響應中斷
                Thread.currentThread().interrupt(); // 重設中斷標誌
                break;
            }
        }
        System.out.println("線程正常退出");
    }
}

// 使用示例
public static void stoppableThreadDemo() {
    StoppableTask task = new StoppableTask();
    Thread thread = new Thread(task);
    thread.start();

    // 主線程工作3秒
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    // 請求線程停止
    task.requestStop();
}

3. 死鎖問題

問題描述:兩個或多個線程互相等待對方持有的鎖,形成環路等待

死鎖示例

public static void deadlockDemo() {
    Object resource1 = new Object();
    Object resource2 = new Object();

    Thread thread1 = new Thread(() -> {
        synchronized (resource1) {
            System.out.println("線程1獲取資源1");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("線程1等待資源2");
            synchronized (resource2) {
                System.out.println("線程1獲取資源2");
            }
        }
    });
    thread1.setName("deadlock-thread-1");

    Thread thread2 = new Thread(() -> {
        synchronized (resource2) {
            System.out.println("線程2獲取資源2");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("線程2等待資源1");
            synchronized (resource1) {
                System.out.println("線程2獲取資源1");
            }
        }
    });
    thread2.setName("deadlock-thread-2");

    thread1.start();
    thread2.start();
}

其他常見併發問題

除了死鎖外,多線程編程中還存在以下併發問題:

線程飢餓(Starvation)

當線程長時間無法獲取所需資源而無法執行時,就會發生線程飢餓:

public static void starvationDemo() {
    Object lock = new Object();

    // 創建一個高優先級線程,它會長時間佔用鎖
    Thread highPriorityThread = new Thread(() -> {
        synchronized (lock) {
            System.out.println("高優先級線程獲取到鎖");
            while (true) {
                // 持續佔用鎖不釋放
            }
        }
    });
    highPriorityThread.setPriority(Thread.MAX_PRIORITY);
    highPriorityThread.setName("high-priority");

    // 創建低優先級線程,它很難獲取到鎖
    Thread lowPriorityThread = new Thread(() -> {
        synchronized (lock) {
            //
            System.out.println("低優先級線程獲取到鎖");
        }
    });
    lowPriorityThread.setPriority(Thread.MIN_PRIORITY);
    lowPriorityThread.setName("low-priority");

    highPriorityThread.start();
    lowPriorityThread.start();
}

避免飢餓的策略

  • 使用公平鎖(如 ReentrantLock(true))
  • 避免長時間持有鎖
  • 適當調整線程優先級

注意:在現代 JVM 中,線程優先級的效果可能不如預期。優先級調整是一種建議而非強制執行的機制,不同操作系統實現差異很大。推薦優先通過合理的鎖使用策略(減少鎖持有時間、使用公平鎖等)來避免線程飢餓,而不是過度依賴優先級調整。

活鎖(Livelock)

當線程不斷相互禮讓,都無法向前推進時,就會發生活鎖:

public static void simpleLivelockDemo() {
    final AtomicBoolean firstThreadGiveWay = new AtomicBoolean(true);
    final AtomicBoolean secondThreadGiveWay = new AtomicBoolean(true);

    Thread thread1 = new Thread(() -> {
        while (true) {
            // 第一個線程檢查"對方是否在讓路"
            if (secondThreadGiveWay.get()) {
                // 如果對方在讓路,自己也讓路
                System.out.println("線程1説:對方在讓路,我也讓路");
                firstThreadGiveWay.set(true);
            } else {
                // 如果對方不讓路,自己也不讓了,開始工作
                System.out.println("線程1説:對方不讓路了,我開始工作");
                break;
            }

            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                return;
            }
        }
    });

    Thread thread2 = new Thread(() -> {
        while (true) {
            // 第二個線程檢查"對方是否在讓路"
            if (firstThreadGiveWay.get()) {
                // 如果對方在讓路,自己也讓路
                System.out.println("線程2説:對方在讓路,我也讓路");
                secondThreadGiveWay.set(true);
            } else {
                // 如果對方不讓路,自己也不讓了,開始工作
                System.out.println("線程2説:對方不讓路了,我開始工作");
                break;
            }

            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                return;
            }
        }
    });

    thread1.start();
    thread2.start();

    // 這個活鎖將永遠持續下去,因為兩個線程都在互相禮讓
    // 在實際應用中,可以通過隨機等待或優先級解決
}

避免活鎖的策略

  • 引入隨機因素(如隨機等待時間)
  • 使用優先級或資源排序
  • 設置超時機制
graph TD
    A[正常執行] --> B{併發問題類型}
    B -->|線程無法前進| C{問題具體類型}
    C -->|互相持有對方需要的鎖| D[死鎖Deadlock]
    C -->|互相禮讓導致無法前進| E[活鎖Livelock]
    C -->|低優先級線程長期得不到執行| F[飢餓Starvation]
    D --> G[環路等待線程完全阻塞]
    E --> H[線程持續運行但無法完成任務]
    F --> I[部分線程幾乎不被調度]

    style D fill:#f9a,stroke:#333
    style E fill:#9cf,stroke:#333
    style F fill:#fcf,stroke:#333

4. 線程異常處理

線程中未捕獲的異常不會被傳遞到主線程,需要特殊處理:

public static void exceptionHandlerDemo() {
    Thread thread = new Thread(() -> {
        System.out.println("線程開始執行");
        throw new RuntimeException("線程執行異常");
    });
    thread.setName("exception-thread");

    // 設置未捕獲異常處理器
    thread.setUncaughtExceptionHandler((t, e) -> {
        System.out.println("捕獲到線程 " + t.getName() + " 的異常:" + e.getMessage());
        // 可以記錄日誌、發送告警等
    });

    thread.start();
}

六、總結

類別 方法/狀態 特點 注意事項
線程狀態 NEW 線程已創建未啓動 使用 start()方法啓動
RUNNABLE 包含就緒和運行中 線程獲得 CPU 時間片才真正運行
BLOCKED 等待獲取 synchronized 鎖 避免長時間阻塞
WAITING 無限期等待其他線程操作 可能導致程序掛起
TIMED_WAITING 有超時時間的等待 設置合理的超時時間
TERMINATED 線程執行完畢 可以用 isAlive()檢查
線程控制 start() 啓動新線程 不能重複調用
run() 普通方法調用,不創建新線程 不要直接調用 run()方法
sleep() 線程休眠,不釋放鎖 處理 InterruptedException
yield() 線程讓步,提示調度器 效果不可靠,依賴系統實現
join() 等待其他線程完成 可能導致當前線程阻塞
interrupt() 中斷線程,協作式機制 結合 isInterrupted()使用
線程類型 用户線程 默認線程類型 JVM 等待用户線程結束
守護線程 為其他線程服務的後台線程 不執行重要任務,finally 塊不保證執行
併發問題 死鎖 線程互相等待對方持有的鎖 按固定順序獲取鎖,使用 tryLock()
活鎖 線程不斷相互禮讓 引入隨機因素,設置優先級
飢餓 線程長期無法獲取資源 使用公平鎖,避免長時間持有鎖

通過本文,我們深入瞭解了 Java 線程的生命週期和基礎操作方法。掌握這些知識對於編寫高質量的多線程程序至關重要。在實際開發中,請記住:理解線程狀態轉換、合理使用線程控制方法、妥善處理線程異常、選擇適當的線程通信機制,這些都是構建穩定多線程應用的基石。

在下一篇文章中,我們將探討線程安全問題與基本解決方案,敬請期待!


感謝您耐心閲讀到這裏!如果覺得本文對您有幫助,歡迎點贊 👍、收藏 ⭐、分享給需要的朋友,您的支持是我持續輸出技術乾貨的最大動力!

如果想獲取更多 Java 技術深度解析,歡迎點擊頭像關注我,後續會每日更新高質量技術文章,陪您一起進階成長~

user avatar greatsql Avatar RCJL Avatar jump_and_jump Avatar lfree Avatar zhangfeidezhu Avatar winfacter Avatar zhaoqianglaoshi Avatar renxingdebenma Avatar 5n7qfpo1 Avatar
Favorites 9 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.