引言
在JDK17(或以上版本)中,Thread
類提供了一組常用的API,用於管理執行緒的建立、啟動、暫停、恢復和銷燬等操作。本文從api、原始碼、程式設計示例等方面詳細說明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
方法,傳入RuntimePermission
的setContextClassLoader
許可權,以檢查是否允許設定上下文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參考實現中,中斷一個未啟動的執行緒仍然記錄了中斷請求的發出,並透過interrupted
和isInterrupted()
方法報告它。
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中,執行緒停止的情況和函式有以下幾種:
- 自然結束:執行緒執行完run()方法後,執行緒會自然結束並進入終止狀態。
- 執行緒被中斷:可以使用Thread類的interrupt()方法來中斷執行緒。當一個執行緒呼叫另一個執行緒的interrupt()方法時,被呼叫執行緒會收到一箇中斷訊號,並且中斷狀態會被設定為true。中斷狀態可以透過Thread類的isInterrupted()方法來查詢。執行緒可以在適當的時機檢查中斷狀態,如果中斷狀態為true,則可以選擇安全地終止執行緒的執行。
- 使用標誌位停止執行緒:可以在多執行緒程式中定義一個標誌位,當標誌位為true時,執行緒停止執行。執行緒可以週期性地檢查該標誌位,如果標誌位為true,則主動結束執行緒的執行。
- 使用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(備註技術)。