併發程式設計Thread的常用API有哪些?

落叶微风發表於2024-03-08

引言

在JDK17(或以上版本)中,Thread類提供了一組常用的API,用於管理執行緒的建立、啟動、暫停、恢復和銷燬等操作。本文從api、原始碼、程式設計示例等方面詳細說明Thread常用函式的使用和注意事項。

Thread

執行緒 sleep

  • 使當前正在執行的執行緒暫停(掛起)指定的毫秒數。但受系統計時器和排程程式的精度和準確性限制。
  • 執行緒不會失去任何monitor(監視器)的所有權。
  • 每個執行緒的休眠互不影響,Thread.sleep 只會導致當前執行緒進入指定時間的休眠。
    public static native void sleep(long millis) throws InterruptedException;
    public static void sleep(long millis, int nanos)
    throws InterruptedException {
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }

        if (nanos > 0 && millis < Long.MAX_VALUE) {
            millis++;
        }

        sleep(millis);
    }

透過測試發現 Thread.sleep 之間互不影響。程式碼如下:

/**
 * 每個執行緒的休眠互不影響,`Thread.sleep` 只會導致當前執行緒進入指定時間的休眠。
 */
public class ThreadSleepTest {
    public static void main(String[] args) throws Exception{
        Thread thread1 = new Thread(()->{
            int i = 0;
            while(i<10){
                System.out.println("thread demo start "+i);
                i++;
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        thread1.start();
        System.out.println("thread main start ");
        Thread.sleep(2000);
        System.out.println("thread main end ");
    }
}

輸出結果如下:

thread main start 
thread demo start 0
thread demo start 1
thread main end 
thread demo start 2
thread demo start 3

除此之外可以使用 java.util.concurrent.TimeUnit 類來更簡單的實現指定時間的休眠,後續原始碼使用該類來進行休眠執行緒。例子程式碼如下:

package engineer.concurrent.battle.abasic;

import java.util.concurrent.TimeUnit;

/**
 * TimeUnit工具類替代Thread.sleep方法。
 * @author r0ad
 * @since 1.0
 */
public class ThreadSleepTimeUnitTest {
    public static void main(String[] args) throws Exception{
        System.out.println("thread main start ");
        TimeUnit.SECONDS.sleep(1);
        TimeUnit.MILLISECONDS.sleep(500);
        TimeUnit.MINUTES.sleep(1);
        System.out.println("thread main end ");
    }
}

/**
 * java.util.concurrent.TimeUnit#sleep 原始碼,底層實現也是Thread.sleep。
 * Performs a {@link Thread#sleep(long, int) Thread.sleep} using
 * this time unit.
 * This is a convenience method that converts time arguments into the
 * form required by the {@code Thread.sleep} method.
 *
 * @param timeout the minimum time to sleep. If less than
 * or equal to zero, do not sleep at all.
 * @throws InterruptedException if interrupted while sleeping
 */
public void sleep(long timeout) throws InterruptedException {
    if (timeout > 0) {
        long ms = toMillis(timeout);
        int ns = excessNanos(timeout, ms);
        Thread.sleep(ms, ns);
    }
}

執行緒 yield

Thread.yield()是一個提示,用於告訴排程程式當前執行緒願意放棄使用處理器。排程程式可以選擇忽略這個提示。Yield是一種試圖改善執行緒之間相對程序的啟發式方法,否則它們會過度利用CPU。它的使用應該與詳細的分析和基準測試結合起來,以確保它確實產生了預期的效果。

這種方法適用場景很少。它有助於除錯或測試,以幫助重現由於競態條件而引起的錯誤。在設計併發控制結構時,例如java.util.concurrent.locks包中的結構,它也可能有用。

呼叫Thread.yield()函式會將當前執行緒從RUNNING狀態切換到RUNNABLE狀態。

    public static native void yield();

測試程式碼如下,在cpu資源不緊張的情況下輸出仍然是亂序的。

package engineer.concurrent.battle.abasic;

import java.util.stream.IntStream;

/**
 * ThreadYield測試
 * @author r0ad
 * @since 1.0
 */
public class ThreadYieldTest {
    public static void main(String[] args) throws Exception{
        System.out.println("thread main start ");
        IntStream.range(0, 2).mapToObj(ThreadYieldTest::create).forEach(Thread::start);
        System.out.println("thread main end ");
    }
    private static Thread create(int i) {
        return new Thread(() -> {
            if(i == 0 ){
                Thread.yield();
            }
            System.out.println("thread " + i + " start ");
        });
    }
}

輸出結果:

thread main start 
thread main end 
thread 0 start 
thread 1 start 

Thread.yield()Thread.sleep() 方法之間的聯絡和差異如下:

聯絡:

  • Thread.yield() 和 Thread.sleep() 都會使當前執行緒暫停執行,讓出CPU資源給其他執行緒。
  • Thread.yield() 和 Thread.sleep() 都不會釋放當前執行緒所佔用的鎖。

差異:

  • Thread.yield() 方法只是暫停當前執行緒的執行,讓出CPU資源給其他執行緒,但不會進入阻塞狀態。可能導致CPU進行上下文切換。
  • Thread.sleep() 方法會使當前執行緒暫停指定的時間,並進入阻塞狀態,直到休眠時間結束或者被其他執行緒打斷。
  • Thread.sleep()具有較高的可靠性,可以確保至少暫停指定的時間。Thread.yield()則不能保證暫停。

設定執行緒的優先順序

  • java.lang.Thread#setPriority 修改執行緒的優先順序
  • java.lang.Thread#getPriority 獲取執行緒的優先順序

java.lang.Thread#setPriority 修改執行緒的優先順序實現過程如下:

  • 呼叫此執行緒的checkAccess方法,不帶任何引數。這可能會導致丟擲一個SecurityException異常。
  • 執行緒的優先順序被設定為指定的newPriority和執行緒所屬執行緒組允許的最大優先順序中較小的值。

    /**
     * `java.lang.Thread#setPriority` 修改執行緒的優先順序原始碼
     */
    public final void setPriority(int newPriority) {
        ThreadGroup g;
        // 呼叫此執行緒的`checkAccess`方法,不帶任何引數。這可能會導致丟擲一個`SecurityException`異常。
        checkAccess();
        if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
            throw new IllegalArgumentException();
        }
        if((g = getThreadGroup()) != null) {
            // 執行緒的優先順序被設定為指定的`newPriority`和執行緒所屬執行緒組允許的最大優先順序中較小的值。
            if (newPriority > g.getMaxPriority()) {
                newPriority = g.getMaxPriority();
            }
            setPriority0(priority = newPriority);
        }
    }

    /**
     * `java.lang.Thread#getPriority` 獲取執行緒的優先順序
     */
    public final int getPriority() {
        // 返回Thread的priority屬性
        return priority;
    }
    /* 原生優先順序設定方法 */
    private native void setPriority0(int newPriority);

程序有程序的優先順序,執行緒同樣也有優先順序,理論上是優先順序比較高的執行緒會獲取優先被 CPU 排程的機會,但是事實上設定執行緒的優先順序同樣也是一個 hint 操作,具體如下。

  • 對於 root 使用者,它會 hint 作業系統你想要設定的優先順序別,否則它會被忽略。
  • 如果 CPU 比較忙,設定優先順序可能會獲得更多的 CPU 時間片,但是閒時優先順序的高低幾乎不會有任何作用。

所以不要使用執行緒的優先順序進行某些特定業務的繫結,業務執行的順序應該還是要使用同步執行方法來保證。

測試例子如下,執行緒之間會交替輸出:

package engineer.concurrent.battle.abasic;

/**
 * ThreadPriorityTest測試
 * @author r0ad
 * @since 1.0
 */
public class ThreadPriorityTest {
    public static void main(String[] args) throws Exception{
        Thread t1 = ThreadPriorityTest.create("t1");
        t1.setPriority(1);
        Thread t2 = ThreadPriorityTest.create("t2");
        t2.setPriority(10);
        t1.start();
        t2.start();
    }
    private static Thread create(String name) {
        return new Thread(() -> {
            while (true) {
                System.out.println("thread " + name );
            }
        });
    }
}

獲取執行緒ID

返回此執行緒的識別符號。執行緒ID是一個正的long數字,在建立此執行緒時生成。執行緒ID是唯一的,並在其生命週期內保持不變。當一個執行緒終止時,該執行緒ID可能會被重新使用。

    public long getId() {
        return tid;
    }

獲取當前執行緒

java.lang.Thread#currentThread 方法被大多數框架使用,像是SpringMVC、MyBatis這些。呼叫該函式會返回當前正在執行的執行緒物件。

    @IntrinsicCandidate
    public static native Thread currentThread();

測試程式碼如下:

package engineer.concurrent.battle.abasic;

/**
 * ThreadCurrentTest測試
 * @author r0ad
 * @since 1.0
 */
public class ThreadCurrentTest {
    public static void main(String[] args) throws Exception{
        Thread t1 = new Thread() {
            @Override
            public void run() {
                System.out.println(this == Thread.currentThread());
            }
        };
        t1.start();
        System.out.println(Thread.currentThread().getName());
    }
}

設定執行緒上下文類載入器

  • java.lang.Thread#getContextClassLoader 返回該執行緒的上下文ClassLoader。上下文ClassLoader由建立執行緒的物件提供,用於在此執行緒中執行的程式碼在載入類和資源時使用。如果未設定(透過setContextClassLoader()方法),則預設為父執行緒的ClassLoader上下文。原始執行緒的上下文ClassLoader通常設定為用於載入應用程式的類載入器。
  • java.lang.Thread#setContextClassLoader 設定此執行緒的上下文ClassLoader。上下文ClassLoader可以在建立執行緒時設定,並允許執行緒的建立者透過getContextClassLoader方法為線上程中執行的程式碼提供適當的類載入器,用於載入類和資源。如果存在安全管理器,則會使用其checkPermission方法,傳入RuntimePermissionsetContextClassLoader許可權,以檢查是否允許設定上下文ClassLoader。
    @CallerSensitive
    public ClassLoader getContextClassLoader() {
        if (contextClassLoader == null)
            return null;
        @SuppressWarnings("removal")
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            ClassLoader.checkClassLoaderPermission(contextClassLoader,
                                                   Reflection.getCallerClass());
        }
        return contextClassLoader;
    }

    public void setContextClassLoader(ClassLoader cl) {
        @SuppressWarnings("removal")
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            sm.checkPermission(new RuntimePermission("setContextClassLoader"));
        }
        contextClassLoader = cl;
    }

執行緒 interrupt

  • java.lang.Thread#interrupt

中斷此執行緒。除非當前執行緒自己中斷自己,這是始終允許的,否則會呼叫該執行緒的checkAccess方法,可能會引發SecurityException異常。

如果此執行緒在Object類的wait()wait(long)wait(long, int)方法的呼叫中被阻塞,或者在此類的join()join(long)join(long, int)sleep(long)sleep(long, int)方法的呼叫中被阻塞,則它的中斷狀態將被清除,並且它將收到一個InterruptedException異常。
如果此執行緒在對InterruptibleChannel的I/O操作中被阻塞,則通道將被關閉,執行緒的中斷狀態將被設定,並且執行緒將收到一個ClosedByInterruptException異常。

如果此執行緒在Selector中被阻塞,則執行緒的中斷狀態將被設定,並且它將立即從選擇操作中返回,可能帶有非零值,就像呼叫了選擇器的wakeup方法一樣。

如果以上條件都不滿足,則將設定此執行緒的中斷狀態。

中斷一個未啟動的執行緒可能不會產生任何效果。在JDK參考實現中,中斷一個未啟動的執行緒仍然記錄了中斷請求的發出,並透過interruptedisInterrupted()方法報告它。

  • java.lang.Thread#interrupted

測試當前執行緒是否已被中斷。此方法將清除執行緒的"中斷狀態"。換句話說,如果連續兩次呼叫此方法,第二次呼叫將返回false(除非在第一次呼叫清除了執行緒的中斷狀態之後,而第二次呼叫在檢查之前再次中斷了當前執行緒)。

  • java.lang.Thread#isInterrupted

測試此執行緒是否已被中斷。此方法不會影響執行緒的"中斷狀態"。

    public void interrupt() {
        if (this != Thread.currentThread()) {
            checkAccess();

            // thread may be blocked in an I/O operation
            synchronized (blockerLock) {
                Interruptible b = blocker;
                if (b != null) {
                    interrupted = true;
                    interrupt0();  // inform VM of interrupt
                    b.interrupt(this);
                    return;
                }
            }
        }
        interrupted = true;
        // inform VM of interrupt
        interrupt0();
    }

    public static boolean interrupted() {
        Thread t = currentThread();
        boolean interrupted = t.interrupted;
        // We may have been interrupted the moment after we read the field,
        // so only clear the field if we saw that it was set and will return
        // true; otherwise we could lose an interrupt.
        if (interrupted) {
            t.interrupted = false;
            clearInterruptEvent();
        }
        return interrupted;
    }

    public boolean isInterrupted() {
        return interrupted;
    }

測試程式碼如下:

package engineer.concurrent.battle.abasic;

import java.util.concurrent.TimeUnit;

/**
 * ThreadInterruptTest
 * @author r0ad
 * @since 1.0
 */
public class ThreadInterruptTest {
    public static void main(String[] args) {
        Thread t1 = new Thread() {
            @Override
            public void run() {
                int i = 0;
                while(i<10){
                    i++;
                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
        };
        t1.start();
        // 中斷該執行緒
        t1.interrupt();
        System.out.println("t1 interrupt status " + t1.isInterrupted());
        System.out.println("t1 is interrupted and I can work still. ");
        // 修改中斷狀態,但是執行緒不會繼續執行
        t1.isInterrupted();
        System.out.println("t1 interrupt status " + t1.isInterrupted());
    }
}

執行緒 join

Thread 的 join 方法同樣是一個非常重要的方法,與 sleep 一樣它也是一個可中斷的方法。Thread類透過過載實現了三個函式供多執行緒開發使用。

  • java.lang.Thread#join(long)

等待最多millis毫秒,讓此執行緒死亡。0的超時時間意味著永久等待。此實現使用了一個基於this.isAlive條件的this.wait呼叫迴圈。當執行緒終止時,將呼叫this.notifyAll方法。建議應用程式不要在Thread例項上使用wait、notify或notifyAll。

  • java.lang.Thread#join(long, int)

等待最多 millis 毫秒加上 nanos 納秒以使此執行緒死亡。如果兩個引數都是 0,那麼意味著永遠等待。此實現使用一個迴圈的 this.wait 呼叫,條件為 this.isAlive。當一個執行緒終止時,會呼叫 this.notifyAll 方法。建議應用程式不要在 Thread 例項上使用 wait、notify 或 notifyAll。

  • java.lang.Thread#join()

等待此執行緒終止。呼叫此方法的行為與呼叫 join(0) 完全相同。

原始碼實現如下:

    public final synchronized void join(final long millis)
    throws InterruptedException {
        if (millis > 0) { 
            if (isAlive()) {
                final long startTime = System.nanoTime();
                long delay = millis;
                do {
                    wait(delay);
                } while (isAlive() && (delay = millis -
                        TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)) > 0);
            }
        } else if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            throw new IllegalArgumentException("timeout value is negative");
        }
    }

    public final synchronized void join(long millis, int nanos)
    throws InterruptedException {

        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }

        if (nanos > 0 && millis < Long.MAX_VALUE) {
            millis++;
        }

        join(millis);
    }

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

當呼叫join函式之後主執行緒和子執行緒的狀態切換如下:

  • 當呼叫join()方法時,主執行緒會進入等待狀態,直到子執行緒執行完畢後才會繼續執行。此時主執行緒的狀態為WAITING。
  • 如果呼叫帶引數的join()方法,主執行緒會在等待一段時間後繼續執行,而不必一直阻塞。在這種情況下,主執行緒的狀態為TIMED_WAITING。
  • 如果子執行緒已經執行完畢,但是主執行緒還沒有呼叫join()方法,則子執行緒的狀態為TERMINATED,而主執行緒的狀態為RUNNABLE。
  • 如果主執行緒呼叫join()方法等待子執行緒完成執行,而子執行緒丟擲了異常,則主執行緒會收到異常資訊並丟擲InterruptedException異常。

測試程式碼如下:

package engineer.concurrent.battle.abasic;

import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;

/**
 * ThreadJoinTest
 * @author r0ad
 * @since 1.0
 */
public class ThreadJoinTest {
    public static void main(String[] args) throws InterruptedException{
        List<Thread> threadList = IntStream.range(1, 10).mapToObj(ThreadJoinTest::create).toList();
        threadList.forEach(Thread::start);
        for(Thread thread : threadList){
            thread.join();
        }
        IntStream.range(1, 10).forEach((i)-> {
            System.out.println("thread " + Thread.currentThread().getName() + " # "+ i );
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        });
    }

    /**
     * 執行緒建立函式
     * @param index
     * @return
     */
    private static Thread create(int index) {
        return new Thread(() -> {
            int i = 0;
            while (i++<10) {
                System.out.println("thread " + Thread.currentThread().getName() + " # "+ i );
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
    }
}

透過觀察輸出結果發現,join之後的執行緒全部結束後才會執行輸出main執行緒的內容。

thread Thread-0 # 10
thread Thread-1 # 10
thread Thread-7 # 10
thread Thread-6 # 10
thread main # 1
thread main # 2
thread main # 3

關閉執行緒

在JDK 17中,執行緒停止的情況和函式有以下幾種:

  1. 自然結束:執行緒執行完run()方法後,執行緒會自然結束並進入終止狀態。
  2. 執行緒被中斷:可以使用Thread類的interrupt()方法來中斷執行緒。當一個執行緒呼叫另一個執行緒的interrupt()方法時,被呼叫執行緒會收到一箇中斷訊號,並且中斷狀態會被設定為true。中斷狀態可以透過Thread類的isInterrupted()方法來查詢。執行緒可以在適當的時機檢查中斷狀態,如果中斷狀態為true,則可以選擇安全地終止執行緒的執行。
  3. 使用標誌位停止執行緒:可以在多執行緒程式中定義一個標誌位,當標誌位為true時,執行緒停止執行。執行緒可以週期性地檢查該標誌位,如果標誌位為true,則主動結束執行緒的執行。
  4. 使用Thread類的stop()方法(已廢棄):Thread類提供了一個stop()方法,可以立即停止執行緒的執行。但是這個方法已經被標記為不安全和不推薦使用,因為它可能導致執行緒在不可預料的位置停止,造成資料不一致或其他問題。

tips native函式

Java中的native關鍵字用於表示某個方法的實現是由原生代碼(C、C++等)提供的。這些本地方法可以直接在Java程式中呼叫,而無需瞭解其底層實現。

在Java中,使用native關鍵字定義本地方法時,不需要提供方法體。例如:

public native void myNativeMethod();

在上面的示例中,myNativeMethod()被定義為本地方法,並且沒有提供方法體。在執行時,Java虛擬機器將查詢本地方法的實現,如果找不到,則會丟擲UnsatisfiedLinkError異常。

要呼叫本地方法,需要使用native方法的外部實現。這通常涉及到將Java程式碼與原生代碼庫進行連結。可以使用Java本機介面(JNI)來實現這一點。

參考

  • 《Java高併發程式設計詳解:多執行緒與架構設計》
  • Java Thread Doc

關於作者

來自一線全棧程式設計師nine的八年探索與實踐,持續迭代中。歡迎關注“雨林尋北”或新增個人衛星codetrend(備註技術)。

相關文章