java並發編程實戰:第七章----取消與關閉

來源:互聯網
上載者:User

標籤: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、響應中斷

  • 傳遞異常(throws InterruptedException)

  • 恢複中斷狀態,從而事調用棧的上層代碼能夠對其進行處理。(Thread.currentThread().interrupt();)

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並發編程實戰:第七章----取消與關閉

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.