文章目錄
廢棄的做法-stop/suspend/resume
JDK1.0中定義了stop/suspend/resume方法,用於中止一個正在運行的線程;其中,stop用於徹底停止當前運行的線程,suspend是暫停當前運行線程,一直阻塞到其他線程調用resume方法。
但是,JDK1.2開始,這幾個方法都被廢棄了,原因如下:
-
stop方法會立即終止當前運行的線程,從而導致業務邏輯執行不完整。
-
stop方法會立即釋放獨佔鎖資源,但是無法保證鎖內代碼塊的原子性。
案例如下所示:
public class ThreadTest { private Object object = new Object(); private Integer i = 0; @Test public void test1() throws InterruptedException { Thread t1 = new Thread(()->{ synchronized (object) { i++; System.out.println("i = " + i); try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } i--; System.out.println("i = " + i); } }); t1.start(); TimeUnit.SECONDS.sleep(2); t1.stop(); } }
上述代碼的邏輯很簡單,即主線程中開啓新的線程t1,t1中休眠10s,主線程休眠2s後調用stop方法,輸出的結果爲:
i = 1;
這對於業務來說是無法忍受的,如果stop方法之後要執行的邏輯是釋放資源等清理性的工作,那麼這些工作將永遠無法被執行;而且對於使用同步機制的業務來說,自然是想保證共享數據的一致性,但是由於stop方法顯然破壞了這一點。
-
suspend和resume需要成對出現,否則極容易出現死鎖。線程在執行suspend方法後,並不會釋放鎖,此時如果另外一個線程需要先獲取該鎖資源再去resume目標線程,那麼就會發生死鎖。(如果想實現暫停-喚醒的邏輯,可以通過設置一個標誌位(volatile),表示該線程是應該運行還是掛起,如果是掛起,那麼就調用wait方法使其等待;如果該運行,即恢復,則調用notify重新啓動線程。wait方法與suspend不同的是,wait方法會釋放鎖資源,再被notify後需要重新參與鎖的競爭~)
中斷線程的方法
自定義標誌位
通過自定義變量作爲狀態位,定期檢查該變量,符合條件則繼續執行,否則退出。該方法適應於正在運行的線程
將一個大任務分割爲多個小任務,每次小任務執行完成後都會去校驗一個狀態標誌位是否爲true,如果非true,那麼線程終止,不再繼續執行任務。(狀態標誌位爲volatile,保證可見性)
public class ThreadTest2 {
private static volatile boolean isRun = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
int batch = 0;
while (isRun) {
System.out.println("==========" + batch++);
}
}).start();
TimeUnit.SECONDS.sleep(1);
//終止線程
stop();
TimeUnit.SECONDS.sleep(10000);
}
public static void stop() {
isRun = false;
}
}
Interrupt()方法
該方法適應於非正在運行線程的退出,對於大部分阻塞線程的方法,通過Thread.interrupt()可以立刻退出等待,拋出InterruptedException,包含sleep、join、wait和Lock.lockInterruptibly()、NIO阻塞等;
如果你用了線程池,並使用了Future對象,可以使用future.cancel(boolean)方法取消正在執行的任務,false會等待正在執行的任務執行完成,但是會取消還沒開始執行的任務;true表示會中斷正在執行且能夠響應中斷異常的任務!!
案例
case1:一直運行的線程無法被中斷
@Test
public void test0() throws InterruptedException {
Thread t1 = getThread0();
t1.start();
TimeUnit.SECONDS.sleep(2);
t1.interrupt();
System.out.println("thread isInterrupted = " + t1.isInterrupted());
TimeUnit.SECONDS.sleep(10);
}
public Thread getThread0() {
return new Thread(() -> {
int count = 0;
while (true) {
System.out.println("=======" + count++);
}
});
}
運行結果:一直運行,直至10s後test1方法結束;
case2:線程被中斷
@Test
public void test1() throws InterruptedException {
Thread t2 = getThread1();
t2.start();
TimeUnit.SECONDS.sleep(2);
t2.interrupt();
System.out.println("thread isInterrupted = " + t2.isInterrupted());
TimeUnit.SECONDS.sleep(10);
}
public Thread getThread1() {
return new Thread(() -> {
int count = 0;
while (!Thread.currentThread().isInterrupted()) {
System.out.println("==========" + count++);
}
System.out.println(Thread.currentThread().getName() + " is interrupted");
});
}
運行結果:調用完interrupt方法後,線程中斷結束;
Interrupt實現機制
探尋其實現機制,首先要從源碼入手~
public void interrupt() {
if (this != Thread.currentThread())
checkAccess();
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}
首先判斷執行中斷的線程是不是自身被中斷的線程,如果是由其他線程進行中斷,會調用checkAccess()方法進行校驗其他線程是否有權限對當前線程進行修改,源碼如下:
public final void checkAccess() {
SecurityManager security = System.getSecurityManager();
if (security != null) {
security.checkAccess(this);
}
}
SecurityManager是java提供的安全管理器,目的是在運行階段檢查應用訪問資源的權限,保護系統免受惡意操作攻擊。可以通過jvm配置或者編碼方式啓動安全管理器,若沒有指定policy文件,那麼會加載默認的策略文件。在策略文件中,我們可以定義系統文件讀取權限、序列化權限等。
正常情況下,應用是沒有啓動安全管理器的,所以System.getSecurityManager() == null,直接跳過檢查;
根據Interruptible b 是否爲null,分爲兩部分邏輯處理,但是都會調用本地方法interrupt0;下面分開說明兩部分的邏輯。
Interruptible b != null
blocker爲Thread的成員變量,並提供了blockedOn()方法設置blocker的值;
void blockedOn(Interruptible b) {
synchronized (blockerLock) {
blocker = b;
}
}
傳統IO在讀寫時,雖然是阻塞的,但是無法被中斷;NIO支持在讀寫操作時進行中斷,channel若實現了InterruptiableChannel接口,則表示支持中斷。如常用的FileChannel,SocketChannel,DatagramChannel等都實現了該接口。
在其子類AbstractInterruptibleChannel中,定義了實現可中斷IO機制的方法begin和end;NIO規定,在阻塞IO的語句前後,需要調用begin和end方法,爲了保證end方法一定被調用,要求放在finally塊中;
begin方法如下:
protected final void begin() {
if (interruptor == null) {
//初始化中斷處理對象,中斷處理對象中提供回調機制
interruptor = new Interruptible() {
public void interrupt(Thread target) {
synchronized (closeLock) {
if (!open)
return;
//設置標誌位
open = false;
interrupted = target;
try {
//關閉channel
AbstractInterruptibleChannel.this.implCloseChannel();
} catch (IOException x) { }
}
}};
}
//將中斷處理對象註冊到當前線程
blockedOn(interruptor);
Thread me = Thread.currentThread();
//當前線程如果被中斷,則註冊的中斷處理對象可能沒有被執行,手動觸發一下
if (me.isInterrupted())
interruptor.interrupt(me);
}
總的來說,就是在Thread的中斷邏輯中,掛載自定義的中斷處理對象,這樣Thread對象被中斷時,會執行中斷處理對象的回調,在回調中執行關閉channel操作,這樣就實現了對線程中斷的響應。
Thread添加中斷處理邏輯 是依賴blockedOn方法,如下:
static void blockedOn(Interruptible intr) { // package-private
sun.misc.SharedSecrets.getJavaLangAccess().blockedOn(Thread.currentThread(),
intr);
}
這裏用到了SharedSecrets類,通過SharedSecrets能夠訪問JDK類庫中因爲作用域的限制而無法訪問的類或者方法(和反射實現的效果一樣),該方法實際上就調用thread.blockedOn(interruptible)方法。
回過來看看interrupt方法,在 b != null 時,首先調用interrupt0()方法,該方法只是設置interrupt標誌位,然後調用b.interrupt方法,該方法就是上述說明的回調方法,一清二楚啦~
Interruptible b == null
對於b==null時,會直接調用本地方法interrupt0(),該方法能夠中斷wait,sleep和join等阻塞等待的方法;
對於java線程來說,最終都會映射爲操作系統的線程。當執行interrupt()方法後,如果操作系統線程沒有被中斷,那麼會設置操作系統線程的interrupt標誌位爲true,並喚醒線程的SleepEvent,隨後喚醒線程的parker和ParkEvent。
ParkEvent包含了_ParkEvent變量和_SleepEvent變量,其中,_ParkEvent變量用於synchronized同步塊和Object.wait()方法,_SleepEvent變量用於Thead.sleep();ParkEvent中包含了一把mutex互斥鎖和一個cond條件變量,線程的阻塞和喚醒(park和unpark)通過它們實現的。
-
PlatformEvent::park() 方法會調用庫函數pthread_cond_wait(_cond, _mutex)實現線程等待
- synchronized塊的進入和Object.wait()的線程等待都是通過PlatformEvent::park()方法實現
- 注:Thread.join()是使用的Object.wait()實現的
-
PlatformEvent::park(jlong millis)方法會調用庫函數pthread_cond_timedwait(_cond, _mutex, _abstime)實現計時條件等待
- Thread.sleep(millis)就是通過PlatformEvent::park(jlong millis)實現
-
PlatformEvent::unpark()方法會調用庫函數pthread_cond_signal (_cond)喚醒上述等待的條件變量
- Thread.interrupt()就會觸發其子類SleepEvent和ParkEvent的unpark()方法
- synchronized塊的退出也會觸發unpark()。其所在對象ObjectMonitor維護了ParkEvent數組作爲喚醒隊列,synchronized同步塊退出時,會觸發ParkEvent::unpark()方法來喚醒等待進入同步塊的線程,或等待在Object.wait()的線程。
對於Synchronized等待事件,被喚醒後會嘗試獲取鎖,如果失敗則會通過循環繼續park()等待,因此synchronized等待實際上不會被中斷的;如果是Object.wait()事件,則會通過標記從而判斷是否爲notify()喚醒,如果不是則拋出InterruptedExcetion進行中斷。
Parker與上述的ParkEvent類似,也持有一把mutex互斥鎖和一個cond條件變量;凡是在java代碼中通過unsafe.park()/unpark()的調用都會映射到Thread的_parker變量去執行。而unsafe.park()/unpark()正是由LockSupport類調用,如ReentrantLock,CountDownLatch,Semaphore,ThreadPoolExecutor,ArrayBlockingQueue等。(LockSupport.park()和unpark()類似於Object.wait和notify方法,不同的是,它不需要在同步代碼塊中,且unpark即便在park方法前進行調用,依然能夠喚醒線程)
並非所有的阻塞方法都拋出 InterruptedException
。輸入和輸出流類會阻塞等待 I/O 完成,但是它們不拋出 InterruptedException
,而且在被中斷的情況下也不會提前返回。然而,對於套接字 I/O,如果一個線程關閉套接字,則那個套接字上的阻塞 I/O 操作將提前結束,並拋出一個 SocketException
。java.nio
中的非阻塞 I/O 類也不支持可中斷 I/O,但是同樣可以通過關閉通道或者請求 Selector
上的喚醒來取消阻塞操作。類似地,嘗試獲取一個內部鎖的操作(進入一個 synchronized
塊)是不能被中斷的,但是 ReentrantLock
支持可中斷的獲取模式。
如何處理中斷異常-InterruptedException
- 如果自己無法處理異常,可以在方法聲明中向外拋出異常(throws InterruptedException),交給上層具體的業務來處理;
- 先捕獲異常,做一些清理工作,然後再向外拋出異常;比如,玩家匹配遊戲,當一個玩家匹配已經到來,但是另外一個玩家未到來,發生了中斷,此時需要把已經到來的玩家重新放回隊列中,然後再拋出異常;
public class PlayerMatcher {
private PlayerSource players;
public PlayerMatcher(PlayerSource players) {
this.players = players;
}
public void matchPlayers() throws InterruptedException {
try {
Player playerOne, playerTwo;
while (true) {
playerOne = playerTwo = null;
// Wait for two players to arrive and start a new game
playerOne = players.waitForPlayer(); // could throw IE
playerTwo = players.waitForPlayer(); // could throw IE
startNewGame(playerOne, playerTwo);
}
}
catch (InterruptedException e) {
// If we got one player and were interrupted, put that player back
if (playerOne != null)
players.addFirst(playerOne);
// Then propagate the exception
throw e;
}
}
}
- 如果自己知道發生異常後如何進行處理,那麼不應該向外拋出異常。比如當由Runnable定義的任務調用了一個可中斷的方法時,那麼當發生中斷異常時是ok的,但是並不意味着捕獲異常然後什麼都不做,因爲java中在檢測到中斷並拋出InterruptedException時,會清除中斷狀態(保證只能被中斷一次);因此需要保留中斷髮生的證據,以便調用棧中更高層的代碼能夠知道中斷,並對中斷做出響應;可以通過interrupt()方法進行 重新中斷 來完成。
public class TaskRunner implements Runnable {
private BlockingQueue<Task> queue;
public TaskRunner(BlockingQueue<Task> queue) {
this.queue = queue;
}
public void run() {
try {
while (true) {
Task task = queue.take(10, TimeUnit.SECONDS);
task.execute();
}
}
catch (InterruptedException e) {
// Restore the interrupted status
Thread.currentThread().interrupt();
}
}
}
- 吞掉中斷異常或者log一下,這也是最常見的處理方式,不推薦;
public class TaskRunner implements Runnable {
private BlockingQueue<Task> queue;
public TaskRunner(BlockingQueue<Task> queue) {
this.queue = queue;
}
public void run() {
try {
while (true) {
Task task = queue.take(10, TimeUnit.SECONDS);
task.execute();
}
}
catch (InterruptedException swallowed) {
/* DON'T DO THIS - RESTORE THE INTERRUPTED STATUS INSTEAD */
}
}
}
- 如果不能重新拋出
InterruptedException
,不管您是否計劃處理中斷請求,仍然需要重新中斷當前線程,因爲一箇中斷請求可能有多個 “接收者”。標準線程池 (ThreadPoolExecutor
)worker 線程實現負責中斷,因此中斷一個運行在線程池中的任務可以起到雙重效果,一是取消任務,二是通知執行線程線程池正要關閉。如果任務生吞中斷請求,則 worker 線程將不知道有一個被請求的中斷,從而耽誤應用程序或服務的關閉。 - 對於不可取消的任務,需要在合適的時候重新設置中斷狀態。有些任務拒絕被中斷,這使得它們是不可取消的。但是,即使是不可取消的任務也應該嘗試保留中斷狀態,以防在不可取消的任務結束之後,調用棧上更高層的代碼需要對中斷進行處理。清單 6 展示了一個方法,該方法等待一個阻塞隊列,直到隊列中出現一個可用項目,而不管它是否被中斷。爲了方便他人,它在結束後在一個 finally 塊中恢復中斷狀態,以免剝奪中斷請求的調用者的權利。(它不能在更早的時候恢復中斷狀態,因爲那將導致無限循環 ——
BlockingQueue.take()
將在入口處立即輪詢中斷狀態,並且,如果發現中斷狀態集,就會拋出InterruptedException
。)
public Task getNextTask(BlockingQueue<Task> queue) {
boolean interrupted = false;
try {
while (true) {
try {
return queue.take();
} catch (InterruptedException e) {
interrupted = true;
// fall through and retry
}
}
} finally {
if (interrupted)
Thread.currentThread().interrupt();
}
}
Interrupted()和isInterrupted()
public static boolean interrupted() {
return currentThread().isInterrupted(true);//本地方法,true清除中斷狀態
}
public boolean isInterrupted() {
return isInterrupted(false); //本地方法,false不會清除中斷狀態
}
兩個方法都能查看線程的中斷狀態,區別在於,interrupted()是static方法,調用後會清除線程的中斷狀態;而isInterrupted()是普通方法,調用後不會清除線程的中斷狀態。
線程池優雅關閉
該部分內容會在下個章節-優雅停機 部分進行講解,敬請期待~
參考