日韩av黄I国产麻豆传媒I国产91av视频在线观看I日韩一区二区三区在线看I美女国产在线I麻豆视频国产在线观看I成人黄色短片

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 >

Java并发 正确终止与恢复线程

發布時間:2025/3/20 44 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Java并发 正确终止与恢复线程 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

為什么80%的碼農都做不了架構師?>>> ??

前面提到了stop()、suspend()等方法在終止與恢復線程的弊端,那么問題來了,應該如何正確終止與恢復線程呢?這里可以使用兩種方法:interrupt()方法和使用boolean變量進行控制。

在使用interrupt方法之前,有必要介紹一下中斷以及與interrupt相關的方法。中斷可以理解為線程的一個標志位屬性,表示一個運行中的線程是否被其他線程進行了中斷操作。這里提到了其他線程,所以可以認為中斷是線程之間進行通信的一種方式,簡單來說就是由其他線程通過執行interrupt方法對該線程打個招呼,讓起中斷標志位為true,從而實現中斷線程執行的目的。

其他線程調用了interrupt方法后,該線程通過檢查自身是否被中斷進行響應,具體就是該線程需要調用isInterrupted方法進行判斷是否被中斷或者調用Thread類的靜態方法interrupted對當前線程的中斷標志位進行復位(變為false)。需要注意的是,如果該線程已經處于終結狀態,即使該線程被中斷過,那么調用isInterrupted方法返回仍然是false,表示沒有被中斷。

那么是不是線程調用了interrupt方法對該線程進行中斷,該線程就會被中斷呢?答案是否定的。因為Java虛擬機對會拋出InterruptedException異常的方法進行了特別處理:Java虛擬機會將該線程的中斷標志位清除,然后跑出InterruptedException,這個時候調用isInterrupted方法返回的也是false

下面的代碼首先創建了兩個線程,一個線程內部不停睡眠,另一個則不斷執行,然后對這兩個線程執行中斷操作。

package com.rhwayfun.concurrency;/*** Created by rhwayfun on 16-4-2.*/ public class Interrupted {public static void main(String[] args){//創建一個休眠線程Thread sleepThread = new Thread(new SleepThread(),"SleepThread");//設為守護線程sleepThread.setDaemon(true);//創建一個忙線程Thread busyThread = new Thread(new BusyThread(),"BusyThread");//把該線程設為守護線程//守護線程只有當其他前臺線程全部退出之后才會結束busyThread.setDaemon(true);//啟動休眠線程sleepThread.start();//啟動忙線程busyThread.start();//休眠5秒,讓兩個線程充分運行SleepUtil.second(5);//嘗試中斷線程//只需要調用interrupt方法sleepThread.interrupt();busyThread.interrupt();//查看這兩個線程是否被中斷了System.out.println("SleepThread interrupted is " + sleepThread.isInterrupted());System.out.println("BusyThread interrupted is " + busyThread.isInterrupted());//防止sleepThread和busyThread立刻退出SleepUtil.second(2);}/*** 不斷休眠*/static class SleepThread implements Runnable{public void run() {while (true){try {TimeUnit.SECONDS.sleep(10);} catch (InterruptedException e) {e.printStackTrace();}}}}/*** 不斷等待*/static class BusyThread implements Runnable{public void run() {while (true){//忙等待}}} }

執行結果:

可以發現內部不停睡眠的方法執行執行中斷后,其中斷標志位返回的是false,而一直運行的線程的中斷標志位則為true。這里主要由于Sleep方法會拋出InterruptedException異常,所以Java虛擬機把SleepThread的中斷標志位復位了,所以才會顯示false。

那么使用interrupt方法正確終止線程已經很明顯了,代碼如下:

package com.rhwayfun.concurrency;import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.concurrent.TimeUnit;/*** Created by rhwayfun on 16-4-2.*/ public class SafeShutdownThread {public static void main(String[] args) throws InterruptedException {DateFormat format = new SimpleDateFormat("HH:mm:ss");Runner one = new Runner();//創建第一個計數線程,該線程使用jdk自帶的中斷方法執行中斷Thread threadOne = new Thread(one,"ThreadOne");//執行第一個線程threadOne.start();//threadOne休眠一秒,然后由main thread執行中斷TimeUnit.SECONDS.sleep(1);threadOne.interrupt();System.out.println("ThreadOne is interrupted ? " + threadOne.isInterrupted());System.out.println("main thread interrupt ThreadOne at " + format.format(new Date()));//創建第二個線程,該線程使用cancel方法執行中斷Runner two = new Runner();Thread threadTwo = new Thread(two,"ThreadTwo");threadTwo.start();//休眠一秒,然后調用cancel方法中斷線程TimeUnit.SECONDS.sleep(1);two.cancel();System.out.println("ThreadTwo is interrupted ? " + threadTwo.isInterrupted());System.out.println("main thread interrupt ThreadTwo at " + format.format(new Date()));}/*** 該線程是一個計數線程*/private static class Runner implements Runnable{//變量iprivate long i;//是否繼續運行的標志//這里使用volatile關鍵字可以保證多線程并發訪問該變量的時候//其他線程都可以感知到該變量值的變化。這樣所有線程都會從共享//內存中取值private volatile boolean on = true;public void run() {while (on && !Thread.currentThread().isInterrupted()){i++;}System.out.println("Count i = " + i);}//讓線程終止的方法public void cancel(){on = false;}} }

在計數線程中通過使用一個boolean變量成功終止了線程。這種通過標志位或者中斷操作的方式能夠使得線程在終止的時候有機會去清理資源,而不是武斷地將線程終止,因此這種終止線程的做法更優雅和安全。

上面的程序只是正確地終止了線程,卻沒有給出正確恢復的方法。可能有人會想到:再寫一個方法讓on變量為true不就行了。事實并如此,因為在CountThread中,由于已經調用cancel方法,這時on變量已經是false了,線程按照順序執行原則繼續執行,所以即使改變on為true也是沒用的,因為CountThread已經終止了。具體的解決方法將在下一篇關于等待通知機制的文章給出詳細的解決措施。

?

重新認識中斷

之前在正確終止與恢復線程一文中介紹了使用Thread類的interrupt方法和使用標志位實現線程的終止。由于之前只是簡單介紹了jdk默認中斷方法的問題,對線程的中斷機制沒有深入介紹。為了正確終止線程,深刻理解線程中斷的本質是很有必要的。Java沒有提供可搶占的安全的中斷機制,但是Java提供了線程協作機制(之前說的interrupt方法和標志位本質上都屬于線程之間協作的手段),但是提供了中斷機制,中斷機制允許一個線程終止另一個線程的當前工作,所以需要在程序設計的時候考慮到中斷的位置和時機。

回到之前使用volatile類型的標志位來終止線程的例子,在代碼中調用cancel方法來取消i的自增請求,如果Runner線程在下次執行,或者正要執行下一次自增請求時判斷on的時是否變為了false,如果是則終止執行。

根據運行結果,Runner的計數任務最終會被取消,然后退出。在Runner線程最終取消執行之前,會有一定的時間,如果在在這個時間內,調用此方法的任務調用了一個會阻塞的方法,比如BlockingQueue的put方法,那么可能該任務一直違法檢測到on的值變為false,因而Runner線程不會終止。

一個例子

比如下面的代碼就說明了這一點:

package com.rhwayfun.patchwork.concurrency.r0411;import java.math.BigInteger; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit;/*** Created by rhwayfun on 16-4-11.*/ public class BrokenShutdownThread extends Thread {//是否繼續運行的標志private static volatile boolean on = true;//阻塞隊列private final BlockingQueue<BigInteger> queue;public BrokenShutdownThread(BlockingQueue<BigInteger> queue) {this.queue = queue;}public void run() {try {BigInteger p = BigInteger.ONE;while (on) {//生產者一次可以放40個數for (int i = 0; i < 40; i++){queue.put(p = p.nextProbablePrime());System.out.println(Thread.currentThread().getName() + ": put value " + p);}}} catch (InterruptedException e) {}}public void cancel() {on = false;}/*** 消費者線程*/static class Consumer extends Thread{//阻塞隊列private final BlockingQueue<BigInteger> queue;public Consumer(BlockingQueue<BigInteger> queue) {this.queue = queue;}@Overridepublic void run() {try {while (on) {//消費者一次只能消費1個數System.out.println(Thread.currentThread().getName() + ": get value " + queue.take());}System.out.println("work done!");} catch (InterruptedException e) {e.printStackTrace();}}}public static void main(String[] args) throws InterruptedException {BlockingQueue<BigInteger> queue = new LinkedBlockingQueue<>(5);BrokenShutdownThread producer = new BrokenShutdownThread(queue);//啟動計數線程producer.start();TimeUnit.SECONDS.sleep(1);new Consumer(queue).start();TimeUnit.SECONDS.sleep(1);producer.cancel();} }

運行上面的程序,發現雖然控制臺輸出了work done!的信息,但是程序仍然沒有停止,仔細分析就會發現生產者的速度(40個數/次)遠大于消費者的速度(1個數/次),造成隊列被填滿,put方法被阻塞。雖然在運行一秒后調用cancel方法將volatile變量on設為了false,但是由于生產者線程的put方法被阻塞,所以無法從阻塞的put方法中恢復,自然程序就無法終止了。

重新認識中斷

每個線程都有一個boolean類型的中斷狀態。當中斷線程時,中斷狀態被設為true。通過Thread的三個方法可以進行不同的中斷操作:

public void interrupt() {...} public static boolean interrupted() {...} public boolean isInterrupted() {...}

執行interrupt方法能夠中斷線程,interrupted可以清除線程的中斷狀態,isInterrupted方法可以返回當前線程的中斷狀態。

當線程調用會阻塞的方法,比如wait()、sleep()等方法時,線程會檢查自己的中斷狀態,并且在發生中斷時提前返回。這些阻塞的方法響應中斷的操作是清除中斷狀態,拋出InterruptedException。拋出InterruptedException的作用是表示線程由于中斷需要提前結束。調用interrupt方法執行中斷的本質是調用interrupt方法并不會立即停止目標線程正在執行的工作,只是傳遞了請求中斷的消息。然后線程會在下一個時刻中斷自己。當收到中斷請求時拋出InterruptedException,讓線程有選擇中斷策略的自由。一般而言,調用代碼需要對拋出的InterruptedException進行額外的處理,直接屏蔽該異常是不正確的(也就是直接調用printStackTrace()方法)。屏蔽中斷異常的后果是調用棧的上層無法對中斷請求做出響應。

對上面代碼的修正

根據以上的分析只需要對代碼做如下的修改就能正確終止線程:

public void run() {try {BigInteger p = BigInteger.ONE;while (on && !Thread.currentThread().isInterrupted()) {//生產者一次可以放40個數for (int i = 0; i < 40; i++){queue.put(p = p.nextProbablePrime());System.out.println(Thread.currentThread().getName() + ": put value " + p);}}} catch (InterruptedException e) {//讓線程退出return;}}public void cancel() {on = false;interrupt();} static class Consumer extends Thread{//阻塞隊列private final BlockingQueue<BigInteger> queue;public Consumer(BlockingQueue<BigInteger> queue) {this.queue = queue;}@Overridepublic void run() {try {while (on && !Thread.currentThread().isInterrupted()) {//消費者一次只能消費1個數System.out.println(Thread.currentThread().getName() + ": get value " + queue.take());}System.out.println("work done!");} catch (InterruptedException e) {Thread.currentThread().interrupt();}}}

而其他代碼保持不變,再次運行以上的程序,發現能夠正確終止了。主要就是使用中斷機制完成了線程之間的協作,從而達到正確終止線程的目的。

實際上在調用可阻塞的方法時拋出的InterruptedException是為了讓調用者能夠注意到中斷信息,使得調用者可以就中斷做出自己的操作。往往在將中斷信息傳給調用者之前需要執行其他操作,如果在線程中使用中斷機制完成線程之間的協作,那么就應該調用Thread.currentThread().intrrupt()恢復當前線程的中斷狀態,這樣當前線程就能夠繼續其他操作了。正常情況下,都需要對中斷進行響應,除非自己實現了中斷所應該進行的操作。

為了取消線程的執行,除了之前的方法,還可以使用Future.get(Long time,TimeUnit unit)的帶超時限制的方法取消線程的執行,如果沒有在指定的時間內完成任務,那么可以在代碼中直接調用Future.cancel()方法取消任務的執行。取消任務的時候有兩種情況:一是任務在指定的時間完成了,這個時候調用取消操作沒有什么影響;二是任務沒有在指定的時間完成,那么調用cancel方法后任務將被中斷。

偽代碼如下:

Future task = threadPool.submit(runnable); try{}catch(TimeOutException e){//會取消任務的執行 }catch(ExecutionException e){//如果在任務中拋出了執行異常,則重新拋出該異常throw(new Throwable(e.getCause())); }finally{//true表示正在執行的任務能夠接收中斷,如果在執行則線程能被中斷//如果為false,則表示若任務還沒有啟動則不要啟動該任務task.cancel(true); }

實現線程取消的完整例子

這里以日志服務作為例子,業務場景是這樣的:前臺會有多個生產者調用日志服務輸出程序的日志,生產者將需要輸出的日志信息放入一個隊列中,后臺服務器有一個消費者線程,負責從隊列中取出日志信息并輸出(目的地可能不同)。顯然這是一個典型的生產者-消費者問題,不過這里出現了多個生產者,但是只有一個消費者。顯然如果生產者的速度遠遠大于消費者的處理速度的話,很可能造成阻塞,不過這點已經再上面的分析中得到了解決。現在需要實現的是,提供可靠的關閉日志服務的方法,在前臺調用服務接口可以正確停止日志服務,而不會出現任何問題。

實現代碼如下:

package com.rhwayfun.patchwork.concurrency.r0411;import java.io.PrintWriter; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit;/*** Created by rhwayfun on 16-4-11.*/ public class LoggerService {// 存放日志消息的阻塞隊列private final BlockingQueue<String> logQueue;// 打印日志的消費者線程private final LoggerThread loggerThread;// 打印日志的打印器private PrintWriter writer;// 日志服務是否關閉的標志private boolean isShutdown;// 執行log方法的調用者的計數器private int reservations;public LoggerService(PrintWriter writer) {this.logQueue = new LinkedBlockingQueue<>(5);this.loggerThread = new LoggerThread(writer);}/*** 啟動日志服務*/public void start() {loggerThread.start();}/*** 記錄日志** @param msg* @throws InterruptedException*/public void recordLog(String msg) throws InterruptedException {// 有條件保持對日志的添加// 并且在接收到關閉請求時停止往隊列中填入日志synchronized (this) {if (isShutdown) throw new IllegalStateException("LoggerService is shutdown!");++reservations;}// 由生產者將消息放入隊列// 這里不放入synchronized塊是因為put方法有阻塞的作用logQueue.put(msg);}/*** 停止日志服務*/public void stop() {// 以原子方式檢查關閉請求synchronized (this) {isShutdown = true;}// 讓消費者線程停止從隊列取日志loggerThread.interrupt();}/*** 消費者線程*/private class LoggerThread extends Thread {private PrintWriter writer;public LoggerThread(PrintWriter writer) {this.writer = writer;}@Overridepublic void run() {try {while (true) {try {// 持有的鎖與之前的相同// 如果接收到應用程序的關閉請求并且沒有生產者線程繼續往隊列填入日志// 那么就結束循環,消費者線程終止synchronized (LoggerService.this) {if (isShutdown && reservations == 0) break;}// 從隊列獲取生產者的日志String msg = logQueue.take();// 每輸出一條日志就減少一個線程synchronized (LoggerService.this) {--reservations;}writer.println("Read: " + msg);} catch (InterruptedException e) {//恢復中斷狀態Thread.currentThread().interrupt();}}} finally {writer.close();}}}/*** 生產者線程*/private static class LoggerWriter implements Runnable {private LoggerService service;private final DateFormat format = new SimpleDateFormat("HH:mm:ss");public LoggerWriter(LoggerService service) {this.service = service;}@Overridepublic void run() {try {String msg = "time is " + format.format(new Date());System.out.println("Write: " + msg);service.recordLog(msg);} catch (InterruptedException e) {Thread.currentThread().interrupt();}}}public static void main(String[] args) throws InterruptedException {LoggerService service = new LoggerService(new PrintWriter(System.out));//創建多個生產者線程負責創建日志for (int i = 0; i < 5; i++) {new Thread(new LoggerWriter(service)).start();TimeUnit.SECONDS.sleep(1);}//啟動日志服務service.start();//休眠10秒TimeUnit.SECONDS.sleep(10);//關閉日志服務service.stop();} }

?

小結

  • Java沒有提供搶占式安全終止線程的機制,但是使用線程的中斷機制可以很好實現線程的終止
  • 除了標志位使用FutureTask和Executor框架也能實現線程的終止,這里主要使用的是FutureTask的cancel方法
  • 除非在程序中自己實現中斷策略,不然不要對中斷異常進行屏蔽
  • 拋出InterruptedException的目的可以使得上層調用者可以接收中斷信息,并對中斷做出自己的操作
  • 如果需要在將中斷信息傳遞給上層調用者之前做其他的操作,需要調用Thread.currentThread().interrupt()恢復當前線程的中斷狀態
  • 如果使用線程池執行任務,那么可以時使用其shutdown方法或者shutdownNow方法完成線程的終止。
  • 轉載于:https://my.oschina.net/oosc/blog/1622660

    總結

    以上是生活随笔為你收集整理的Java并发 正确终止与恢复线程的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。