標籤:ash getname ann mic 告訴 還需要 優先順序 應用程式 inter
Java沒有提供任何機制來安全地終止線程(雖然Thread.stop和suspend方法提供了這樣的機制,但由於存在缺陷,因此應該避免使用
中斷:一種協作機制,能夠使一個線程終止另一個線程的當前工作
立即停止會使共用的資料結構處於不一致的狀態,需要停止時,發出插斷要求,被要求中斷的線程處理完他當前的任務後會自己判斷是否停下來
一、任務取消
若外部代碼能在某個操作正常完成之前將其置入“完成”狀態,則還操作是可取消的。(使用者請求取消、有時間限制的操作<並發尋找結果,一個線程找到後可取消其他線程>、應用程式事件、錯誤、關閉)
取消策略:詳細地定義取消操作的“How”、“When”以及“What”,即其他代碼如何(How)請求取消該任務,任務在何時(When)檢查是否已經請求了取消,以及在響應取消請求時應該執行哪些(What)操作
舉例:設定volatile變數為取消標誌,每次執行前檢查
1 private volatile boolean canceled; 2 3 @Override 4 public void run() { 5 BigInteger p = BigInteger.ONE; 6 while (!canceled){ 7 p = p.nextProbablePrime(); 8 synchronized (this) { //同步添加素數 9 primes.add(p);10 }11 }12 }
注意:這是一個有問題的取消方式,若線程阻塞在add操作後,那麼即使設定了取消狀態,它也不會運行到檢驗阻塞狀態的代碼,因此會永遠阻塞
1、中斷
線程可以通過這種機制來通知另一個線程,告訴它在合適的或者可能的情況下停止當前工作,並轉而執行其他的工作。(在取消之外的其他動作使用中斷都是不合適的)
調用interrupt並不意味者立即停止目標線程進行中的工作,而只是傳遞了請求中斷的訊息。會在下一個取消點中斷自己,如wait, sleep,join等
1 public class Thread {2 public void interrupt() { ... }//中斷目標線程,恢複中斷狀態3 public boolean isInterrupted() { ... }//返回目標線程的中斷狀態4 public static boolean interrupted() { ... }//清除當前線程的中斷狀態,並返回它之前的值(用於已經設定了中斷狀態,但還尚未相應中斷)5 ...6 }
阻塞庫方法,例如Thread.sleep和Object.wait等,都會檢查線程何時中斷,並且在發現時提前返回。它們在響應中斷時執行的操作包括 : 清除中斷狀態,拋出InterruptedException,表示阻塞操作由於中斷而提前結束。
- 顯示的檢測中斷!Thread.currentThread().isInterrupted()後推出
- 阻塞方法中抓到InterruptedException後退出
2、中斷策略——規定線程如何解釋某個插斷要求——當發現插斷要求時,應該做哪些工作
由於每個線程擁有各自的中斷策略,因此除非你知道中斷對該線程的含義,否則就不應該中斷這個線程。
3、響應中斷
4、通過Future實現取消
boolean cancel(boolean mayInterruptIfRunning);
- 如果任務已完成、或已取消,或者由於某些其他原因而無法取消,則此嘗試將失敗,返回false
- 調用cancel時,如果調用成功,而此任務尚未啟動,則此任務將永不運行
- 如果任務已經執行,mayInterruptIfRunning參數決定了是否向執行任務的線程發出interrupt操作
5、處理不可中斷的阻塞——對於某些阻塞操作,只是設定了中斷狀態
- Java.io包中的同步Socket I/O。雖然InputStream和OutputStream中的read和write等方法都不會響應中斷,但通過關閉底層的通訊端,可以使得由於執行read或write等方法而被阻塞的線程拋出一個SocketException。
- Java.io包中的同步I/O。當中斷一個正在InterruptibleChannel上等待的線程時,將拋出ClosedByInterruptedException)並關閉鏈路(這還會使得其他在這條鏈路上阻塞的線程同樣拋出ClosedByInterruptException)。當關閉一個InterruptibleChannel時,將導致所有在鏈路操作上阻塞的線程拋出AsynchronousCloseException。大多數標準的Channel都實現了InterruptibleChannel。
- Selector的非同步I/O。如果一個線程在調用Selector.select方法(在java.nio.channels中)時阻塞了,那麼調用close或wakeup方法會使線程拋出ClosedSelectorException並提前返回。
- 擷取某個鎖。如果一個線程由於等待某個內建鎖而被阻塞,那麼將無法響應中斷,因為線程認為它肯定獲得鎖,所以將不會理會插斷要求。但是,在Lock類中提供了lockInterruptibly方法,該方法允許在等待一個鎖的同時仍能響應中斷。
1 //改寫interrupt方法發出插斷要求 2 @Override 3 public void interrupt() { 4 try { 5 socket.close(); //中斷前關閉socket 6 } catch (IOException e) { 7 8 } finally{ 9 super.interrupt();10 }11 }
6、採用newTaskFor來封裝非標準的取消
二、停止基於線程的服務
應用程式通常會建立基於線程的服務,如線程池。這些服務的時間一般比建立它的方法更長。
- 服務退出 -> 線程需要結束 無法通過搶佔式的方法來停止線程,因此它們需要自行結束
- 除非擁有某個線程,否則不能對該線程進行操控。例如,中斷線程或者修改線程的優先順序等
- 線程池是其工作者線程的所有者,如果要中斷這些線程,那麼應該使用線程池
- 應用程式可以擁有服務,服務也可以擁有工作者線程,但應用程式不能擁有工作者線程,因此應用程式不能直接停止工作者線程。
服務應該生命週期方法關閉它自己以及他擁有的線程
- 要服務的存在時間大於建立線程的方法的存在時間,那麼就應該提供生命週期方法
- ExecutorService提供的shutdown(), shutdownNow()
1、樣本:Log Service
1 // LogWriter就是一個基於線程的服務,但不是一個完成的服務 2 public class LogWriter { 3 //日誌緩衝 4 private final BlockingQueue<String> queue; 5 private final LoggerThread logger;//日誌寫線程 6 private static final int CAPACITY = 1000; 7 8 public LogWriter(Writer writer) { 9 this.queue = new LinkedBlockingQueue<String>(CAPACITY);10 this.logger = new LoggerThread(writer);11 }12 13 public void start() { logger.start(); }14 15 //應用程式向日誌緩衝中放入要記錄的日誌16 public void log(String msg) throws InterruptedException {17 queue.put(msg);18 }19 20 //日誌寫入線程,這是一個多生產者,單消費者的設計21 private class LoggerThread extends Thread {22 private final PrintWriter writer;23 public LoggerThread(Writer writer) {24 this.writer = new PrintWriter(writer, true); // autoflush25 }26 public void run() {27 try {28 while (true)29 writer.println(queue.take());30 } catch(InterruptedException ignored) {31 } finally {32 writer.close();33 }34 }35 }36 }
注意:可以中斷阻塞的take()方法停止日誌線程(消費者線程),但生產者沒有專門的線程,沒辦法取消
1 //Log Service,提供記錄日誌的服務,並有管理服務生命週期的相關方法 2 public class LogService { 3 private final BlockingQueue<String> queue; 4 private final LoggerThread loggerThread;// 日誌寫線程 5 private final PrintWriter writer; 6 private boolean isShutdown;// 服務關閉標示 7 // 隊列中的日誌訊息儲存數量。我們不是可以通過queue.size()來擷取嗎? 8 // 為什麼還需要這個?請看後面 9 private int reservations;10 11 public LogService(Writer writer) {12 this.queue = new LinkedBlockingQueue<String>();13 this.loggerThread = new LoggerThread();14 this.writer = new PrintWriter(writer);15 16 }17 18 //開機記錄服務19 public void start() {20 loggerThread.start();21 }22 23 //關閉Log Service24 public void stop() {25 synchronized (this) {26 /*27 * 為了線程可見度,這裡一定要加上同步,當然volatile也可,28 * 但下面方法還需要原子性,所以這裡就直接使用了synchronized,29 * 但不是將isShutdown定義為volatile30 */31 isShutdown = true;32 }33 //向日誌線程發出插斷要求34 loggerThread.interrupt();35 }36 37 //供應用程式調用,用來向日誌緩衝存放要記錄的日誌資訊38 public void log(String msg) throws InterruptedException {39 synchronized (this) {40 /*41 * 如果應用程式發出了服務關閉請求,則不存在接受日誌,而是直接42 * 拋出異常,讓應用程式知道43 */44 if (isShutdown)45 throw new IllegalStateException(/*Log Service已關閉*/);46 /*47 * 由於queue是安全執行緒的阻塞隊列,所以不需要同步(同步也可48 * 但並發效率會下降,所以將它放到了同步塊外)。但是這裡是的49 * 操作序列是由兩個操作組成的:即先判斷isShutdown,再向緩衝50 * 中放入訊息,如果將queue.put(msg)放在同步外,則在多線程環51 * 境中,LoggerThread中的 queue.size() == 0 將會不準確,所52 * 以又要想queue.put不同步,又要想queue.size()計算準確,所53 * 以就使用了一個變數reservations專用來記錄緩衝中日誌條數,54 * 這樣就即解決了同步queue效率低的問題,又解決了安全性問題,55 * 這真是兩全其美56 */57 //queue.put(msg);58 ++reservations;//儲存量加159 }60 queue.put(msg);61 }62 63 private class LoggerThread extends Thread {64 public void run() {65 try {66 while (true) {67 try {68 synchronized (LogService.this) {69 // 由於 queue 未同步,所以這裡不能使用queue.size70 //if (isShutdown && queue.size() == 0)71 72 // 如果已關閉,且緩衝中的日誌資訊都已寫入,則退出日誌線程73 if (isShutdown && reservations == 0)74 break;75 }76 String msg = queue.take();77 synchronized (LogService.this) {78 --reservations;79 }80 writer.println(msg);81 } catch (InterruptedException e) { /* 重試 */82 }83 }84 } finally {85 writer.close();86 }87 }88 }89 }
注意:通過原子方式來檢查關閉請求,並且有條件地遞增一個計數器來“保持”提提交訊息的權利
2、關閉ExecutorService
shutdown():啟動一次順序關閉,執行完以前提交的任務,沒有執行完的任務繼續執行完
shutdownNow():試圖停止所有正在執行的任務(向它們發出interrupt操作文法,無法保證能夠停止正在處理的任務線程,但是會儘力嘗試),並暫停處理正在等待的任務,並返回等待執行的工作清單。
ExecutorService已關閉,再向它提交任務時會拋RejectedExecutionException異常
3、“毒丸”對象——當得到這個對象時,立即停止
在提交“毒丸”對象之前提交的所有工作都會被處理,而生產者在提交了“毒丸”對象後,將不會再提交任何工作
4、只執行一次的服務
如果某個方法需要處理一批任務,並且當所有任務都處理完成後才返回,那麼可以通過一次私人的Executor來簡化服務的生命週期管理,其中該Executor的生命週期是由這個方法來控制的。
1 boolean checkMail(Set<String> hosts, long timeout, TimeUnit unit) 2 throws InterruptedException { 3 ExecutorService exec = Executors.newCachedThreadPool(); 4 //這裡不能使用 volatile hasNewMail,因為還需要在匿名內中修改 5 final AtomicBoolean hasNewMail = new AtomicBoolean(false); 6 try { 7 for (final String host : hosts)//迴圈檢索每台主機 8 exec.execute(new Runnable() {//執行任務 9 public void run() {10 if (checkMail(host))11 hasNewMail.set(true);12 }13 });14 } finally {15 exec.shutdown();//因為ExecutorService只在這個方法中服務,所以完成後即可關閉16 exec.awaitTermination(timeout, unit);//等待任務的完成,如果逾時還未完成也會返回17 }18 return hasNewMail.get();19 }
5、shutdown的局限性
我們無法通過常規方法來找出哪些任務已經開始但尚未結束。這意味著我們無法在關閉過程中知道正在執行的任務的狀態,除非任務本身會執行某種檢查
1 public class TrackingExecutor extends AbstractExecutorService { 2 private final ExecutorService exec; 3 private final Set<Runnable> tasksCancelledAtShutdown = 4 Collections.synchronizedSet(new HashSet<Runnable>()); 5 6 public TrackingExecutor(ExecutorService exec) { 7 this.exec = exec; 8 } 9 10 public List<Runnable> getCancelledTasks() {//返回被取消的任務11 if (!exec.isTerminated())//如果shutdownNow未調用或調用未完成時12 throw new IllegalStateException(/*...*/);13 return new ArrayList<Runnable>(tasksCancelledAtShutdown);14 }15 16 public void execute(final Runnable runnable) {17 exec.execute(new Runnable() {18 public void run() {19 try {20 runnable.run();21 /*參考:http://blog.csdn.net/coslay/article/details/4803879522 * 實質上在這裡會有執行緒安全性問題,存在著競爭條件,比如程式剛23 * 好運行到這裡,即任務任務(run方法)剛好運行完,這時外界調用24 * 了shutdownNow(),這時下面finally塊中的判斷會有出錯,明顯示25 * 任務已執行完成,但判斷給出的是被取消了。如果要想安全,就不26 * 應該讓shutdownNow在run方法運行完成與下面判斷前調用。我們要27 * 將runnable.run()與下面的if放在一個同步塊、而且還要將28 * shutdownNow的調用也放同步塊裡並且與前面要是同一個監視器鎖,29 * 這樣好像就可以解決了,不知道對不能。書上也沒有說能不能解決,30 * 只是說有這個問題!但反過來想,如果真的這樣同步了,那又會帶31 * 效能上的問題,因為什麼所有的任務都會串形執行,這樣還要32 * ExecutorService線程池幹嘛呢?我想這就是後面作者為什麼所說33 * 這是“不可避免的競爭條件”34 */35 } finally {36 //如果調用了shutdownNow且啟動並執行任務被中斷37 if (isShutdown()38 && Thread.currentThread().isInterrupted())39 tasksCancelledAtShutdown.add(runnable);//記錄被取消的任務40 }41 }42 });43 }44 // 將ExecutorService 中的其他方法委託到exec45 }
三、處理非正常的線程終止
在一個線程中啟動另一個線程,另一個線程中拋出異常,如果沒有捕獲它,這個異常也不會傳遞到父線程中
任何代碼都可能拋出一個RuntimeException。每當調用另一個方法時,都要對它的行為保持懷疑,不要盲目地認為它一定會正常返回,或者一定會拋出在方法原型中聲明的某個已檢查異常
1 //如果任務拋出了一個運行時異常,它將允許線程終結,但是會首先通知架構:線程已經終結 2 public void run() {//工作者線程的實現 3 Throwable thrown = null; 4 try { 5 while (!isInterrupted()) 6 runTask(getTaskFromWorkQueue()); 7 } catch (Throwable e) {//為了安全,捕獲的所有異常 8 thrown = e;//保留異常資訊 9 } finally {10 threadExited(this, thrown);// 重新將異常拋給架構後終結背景工作執行緒11 }12 }未捕獲異常的線程
在Thread API中提供了UncaughtExceptionHandler,它能檢測出某個線程由於未捕獲的異常而終結的情況
在已耗用時間較長的應用程式中,通常會為所有的未捕獲異常指定同一個異常處理器,並且該處理器至少會將異常資訊記錄到日誌中。
public class UEHLogger implements Thread.UncaughtExceptionHandler { public void uncaughtException(Thread t, Throwable e) { Logger logger = Logger.getAnonymousLogger(); logger.log(Level.SEVERE, "Thread terminated with exception: " + t.getName(), e); }}
四、JVM關閉
JVM既可通過正常手段來關閉,也可強行關閉。
- 正常關閉:當最後一個“正常(非守護)”線程結束時、當有人調用了System.exit時、或者通過其他特定於平台的方法關閉時
- 強行關閉:Runtime.halt,這種強行關閉方式將無法保證是否將運行關閉鉤子
1、關閉鉤子
- 關閉鉤子是指通過Runnable.addShutdownHook註冊的但尚未開始的線程
- JVM並不能保證關閉鉤子的調用順序
- 當所有的關閉鉤子都執行結束時,如果runFinalizersOnExit為true,那麼JVM將運行終結器(finalize),然後再停止
- JVM並不會停止或中斷任何在關閉時仍然啟動並執行應用程式線程。當JVM最終結束時,這些線程將被強行結束。如果關閉鉤子或終結器沒有執行完成,那麼正常關閉進程“掛起”並且JVM必須被強行關閉。當被強行關閉時,只是關閉JVM,而不會運行關閉鉤子
- 關閉鉤子應該是安全執行緒的
- 關閉鉤子必須儘快退出,因為它們會延遲JVM的結束時間
public void start()//通過註冊關閉鉤子,停止Log Service{ Runnable.getRuntime().addShutdownHook(new Thread(){ public void run() { try{LogService.this.stop();} catch(InterruptedException ignored){} } });}
2、守護線程——一個線程來執行一些輔助工作,但有不希望這個線程阻礙JVM的關閉
線程可分為兩種:普通線程和守護線程。在JVM啟動時建立的所有線程中,除了主線程以外,其他的線程都是守護線程
普通線程與守護線程之間的差異僅在於當線程退出時發生的操作。當一個線程退出時,JVM會檢查其他正在啟動並執行線程,如果這些線程都是守護線程,那麼JVM會正常退出操作。當JVM停止時,所有仍然存在的守護線程都將被拋棄——既不會執行finally代碼塊,也不會執行回卷棧,而JVM只是直接退出
3、終結器(清理檔案控制代碼或通訊端控制代碼等)——避免使用
記憶體回收行程對那些定義了finalize方法的對象會進行特殊處理:在回收器釋放它們後,調用它們的finalize方法,從而確保一些持久化的資源被釋放。
通過使用finally代碼塊和顯式的close方法,能夠比使用終結器更好地管理資源
例外:當需要管理對象時,並且該對象持有的資源是通過本地方法獲得的
java並發編程實戰:第七章----取消與關閉