首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

深入淺出 Java 多線程(一):徹底掌握 Thread.join() 方法原理與應用

     編輯:Java 相關知識

前言:為什麼要深入理解多線程?

對於 Java 開發者而言,多線程是必須精通的核心技能,尤其是在開發基於 Java 的產品時。本系列文章將以「深入淺出」的方式,帶領讀者攻克 Java 多線程的關鍵知識點。後續內容將重點剖析 Java 5 中 Doug Lea 教授主導的 java.util.concurrent 並行包的設計哲學與實戰應用。

所謂「深入淺出」,關鍵在於「帶著問題學習」而非泛泛而讀。本系列將以解決實際開發問題為導向,例如線程池如何實現、鎖機制如何應用等。基礎概念(如什麼是線程、如何創建線程)請自行查閱文檔,此處不再贅述。

核心問題:join() 方法究竟在做什麼?

簡單來說,join() 用於線程同步。但具體如何同步?底層又是如何實現的?我們逐一拆解。

查閱 JDK 文檔:

public final void join(long millis) throws InterruptedException
// Waits at most millis milliseconds for this thread to die.
// A timeout of 0 means to wait forever.

字面意思是「等待該線程終止,最多等待 millis 毫秒;0 代表永久等待」。但這裡的「該線程」指的是誰?是調用 join() 的線程,還是被調用的線程對象?我們通過代碼驗證:

案例 1:主線程等待子線程

public class JoinTest {
    public static void main(String[] args) {
        Thread t = new Thread(new RunnableImpl());
        t.start();
        try {
            t.join(1000); // 主線程最多等待 1 秒
            System.out.println("joinFinish");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class RunnableImpl implements Runnable {
    @Override
    public void run() {
        try {
            System.out.println("Begin sleep");
            Thread.sleep(1000);
            System.out.println("End sleep");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

輸出結果:

Begin sleep
End sleep
joinFinish

說明:主線程調用 t.join(1000) 後阻塞,等待線程 t 結束。因為 t 只睡眠 1 秒,所以主線程在 1 秒後繼續執行。

案例 2:超時場景

若子線程睡眠 2000 毫秒,而主線程只等待 1000 毫秒:

// 修改 RunnableImpl.run() 中的睡眠時間為 2000ms
Thread.sleep(2000);

輸出結果:

Begin sleep
joinFinish
End sleep

主線程等待 1 秒後超時返回,不再管子線程是否結束,直接打印 joinFinish。子線程繼續在後台運行,2 秒後打印 End sleep。

案例 3:無參 join() —— 永久等待?

public final void join() throws InterruptedException { join(0); }

JDK 說明:timeout of 0 means to wait forever。實際行為是等待直到目標線程終止,而非字面上的「永遠阻塞」。

底層實現原理:wait/notify 機制

查看 JDK 源碼(精簡版):

public final synchronized void join(long millis) throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;
    if (millis < 0) throw new IllegalArgumentException("timeout value is negative");
    if (millis == 0) {
        while (isAlive()) wait(0); // 永久等待,直到線程結束
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) break;
            wait(delay); // 定時等待
            now = System.currentTimeMillis() - base;
        }
    }
}

關鍵點總結:

  • join() 本質是調用 Object.wait(),因此必須持有目標線程對象的鎖(即 synchronized(this))。
  • 當目標線程終止時,JVM 會自動調用 notifyAll() 喚醒等待的線程。
  • 若其他線程長期持有目標線程對象的鎖,會導致 join() 無法進入 wait() 狀態,從而影響等待精度。

鎖競爭演示:被阻塞的 join()

public class JoinTest {
    public static void main(String[] args) {
        Thread t = new Thread(new RunnableImpl());
        new ThreadTest(t).start(); // 佔用 t 的鎖 9 秒
        t.start();
        try {
            t.join(); // 必須等待鎖釋放後才能 wait()
            System.out.println("joinFinish");
        } catch (InterruptedException e) { e.printStackTrace(); }
    }
}

class ThreadTest extends Thread {
    Thread thread;
    public ThreadTest(Thread thread) { this.thread = thread; }
    @Override public void run() { holdThreadLock(); }
    public void holdThreadLock() {
        synchronized (thread) { // 持有 t 的鎖
            System.out.println("getObjectLock");
            try { Thread.sleep(9000); } catch (InterruptedException ex) { ex.printStackTrace(); }
            System.out.println("ReleaseObjectLock");
        }
    }
}

輸出結果:

getObjectLock
Begin sleep
End sleep
ReleaseObjectLock
joinFinish

主線程雖然調用 t.join(),但必須等待 ThreadTest 釋放鎖(9 秒後)才能進入 wait(),實際等待時間遠超預期。

小結

join() 通過 synchronized(this) + wait()/notifyAll() 實現線程同步。使用時需注意:

  1. 調用 t.join() 會競爭 t 對象的鎖,避免在業務代碼中長時間 synchronized(t)。
  2. join(0) 等同於 join(),均為等待線程終止,非無限期阻塞。
  3. 超時參數 millis 是「最長等待時間」,而非「精確等待時間」。

掌握底層原理,才能在高併發場景下正確使用 join(),避免隱性 Bug。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved