多線程中如何取消任務

大多數情況下,任務運行完後會自動結束。然而,有時我們希望提前結束任務或線程,可能是因爲用戶取消了操作,或者應用程序需要被快速關閉。但是,Java並沒有提供任務機制來安全地終止線程。雖然如此,但Java提供了線程中斷,中斷是一種協作機制,能使一個線程終止另一個線程的當前工作。

我們很少希望某個任務、線程或服務立即停止,因爲這種立即停止會使共享數據處於不一致的狀態,而使用協作機制的方式:當需要停止時,它們首先會清除當前正在執行的工作,然後再結束。這提供了更好的靈活性,因爲任務本身的代碼比發出取消請求的代碼更清楚如何執行清除工作。

一、任務取消

1.使用volatile狀態變量來控制

操作被取消的原因有很多,比如超時,異常,請求被取消等等。

一個可取消的任務要求必須設置取消策略,即如何取消,何時檢查取消命令,以及接收到取消命令之後如何處理。

最簡單的取消辦法就是利用取消標誌位,如下所示。這段代碼用於生成素數,並在任務運行一秒鐘之後終止。其取消策略爲:通過改變取消標誌位取消任務,任務在每次生成下一隨機素數之前檢查任務是否被取消,被取消後任務將退出。

複製代碼
@ThreadSafe
public class PrimeGenerator implements Runnable {
    private static ExecutorService exec = Executors.newCachedThreadPool();

    @GuardedBy("this") private final List<BigInteger> primes = new ArrayList<BigInteger>();
    //必須是volatile
    private volatile boolean cancelled;

    public void run() {
        BigInteger p = BigInteger.ONE;
        //檢查狀態,從而取消任務
        while (!cancelled) {
            p = p.nextProbablePrime();
            synchronized (this) {
                primes.add(p);
            }
        }
    }

    public void cancel() {
        cancelled = true;
    }

    public synchronized List<BigInteger> get() {
        return new ArrayList<BigInteger>(primes);
    }

    static List<BigInteger> aSecondOfPrimes() throws InterruptedException {
        PrimeGenerator generator = new PrimeGenerator();
        exec.execute(generator);
        try {
            SECONDS.sleep(1);
        } finally {
            generator.cancel();
        }
        return generator.get();
    }
}
複製代碼

 

然而,該機制最大的問題就是無法應用於阻塞方法,例如BlockingQueue.put()。可能會產生一個很嚴重的問題——任務可能因阻塞而永遠無法檢查取消標識,導致任務永遠不會結束。

複製代碼
class BrokenPrimeProducer extends Thread {
    private final BlockingQueue<BigInteger> queue;
    private volatile boolean cancelled = false;

    BrokenPrimeProducer(BlockingQueue<BigInteger> queue) {
        this.queue = queue;
    }

    public void run() {
        try {
            BigInteger p = BigInteger.ONE;
            while (!cancelled)
                //這裏可能產生阻塞,從而無法取消任務。
                queue.put(p = p.nextProbablePrime());
        } catch (InterruptedException consumed) {
        }
    }

    public void cancel() {
        cancelled = true;
    }
}
複製代碼

2.中斷 

線程中斷是一種協作機制,線程可以通過這種機制來通知另一個線程,告訴它在合適的或者可能情況下停止當前工作。

雖然線程的取消和中斷沒有必然聯繫,但是在實踐中發現:中斷是實現取消的最合理方式。

對中斷操作的正確理解是:它並不會真正的中斷線程,而是給線程發出中斷通知,告知目標線程有人希望你退出。目標線程收到通知後如何處理完全由目標線程自行決定,這是非常重要的。線程收到中斷通知後,通常會在下一個合適的時刻(被稱爲取消點)中斷自己。有些方法,如wait、sleep和join等將嚴格地處理這種請求,當它們收到中斷請求或者在開始執行時發現某個已被設置好的中斷狀態時,將拋出一個異常。

 

對於前面BrokenPrimeProducer的問題很容易解決(和簡化),使用中斷而不是boolean標識來請求取消。

複製代碼
public class PrimeProducer extends Thread {
    private final BlockingQueue<BigInteger> queue;

    PrimeProducer(BlockingQueue<BigInteger> queue) {
        this.queue = queue;
    }

    public void run() {
        try {
            BigInteger p = BigInteger.ONE;
            //檢查中斷
            while (!Thread.currentThread().isInterrupted())
                queue.put(p = p.nextProbablePrime());
        } catch (InterruptedException consumed) {
            /* 允許線程退出 */
        }
    }

    public void cancel() {
        interrupt();
    }
}
複製代碼

代碼中有兩次檢查中斷請求:

①第一次是在循環開始前,顯示檢查中斷請求;

②第二次是在put方法,該方法爲阻塞的,會隱式的檢測當前線程是否被中斷;

2.1中斷策略

正如任務中應該包含取消策略一樣,線程同樣也需要有中斷策略:發現中斷請求時應該做什麼(如果需要),以多快速度來響應中斷(立即響應還是推遲響應)。

最合理的中斷策略是某種形式的線程級別的取消操作或服務級別的取消操作:儘快退出,在必要時進行清理,通知某個所有者線程該線程已經退出。

此外,還可以建立其它的中斷策略,例如暫停服務或重新開始服務,但對於哪些包含非標準中斷策略的線程或者線程池,這些中斷策略只能用於知道這些策略的任務中。

區分任務和線程對中斷的反應是很重要的。一箇中斷請求可以有一個或多個接收者,例如,中斷線程池中的某個工作者線程, 則意味着取消了當前任務,同時也意味着關閉了工作者線程。

任務不會在其自己擁有的線程中執行,而是在某個服務(如線程池)擁有的線程中執行。對於非線程所有者的代碼來說(例如,對於線程池而言,任何在線程池實現以外的代碼),應該小心地保存中斷狀態,這樣擁有線程的代碼才能對中斷做出響應,即使非所有者代碼也可以做出響應。(當爲一戶人家打掃房屋時,即使主人不在,也不應該把這段時間內收到的郵件扔掉,而是應該收起來等主人回來後交給他們處理,儘管你可以閱讀它們的雜誌)。

這就是爲什麼大多數可阻塞的庫方法都是拋出中斷異常(InterruptedException)作爲中斷響應。它們永遠不會在某個由自己擁有的線程中運行,因此它們爲任務或庫代碼實現了最合理的取消策略:儘快退出執行流程,並把中斷信息傳遞給調用者,從而使調用棧中的上層代碼可以採取進一步的操作。

當檢測到中斷請求時,任務並不需要立刻放棄所有的操作,它可以推遲處理中斷請求到某個更適合的時刻。既然要推遲處理,就需要記住中斷請求,並在完成當前任務後拋出中斷異常,或者表示已收到中斷請求。

無論任務把中斷視爲取消,還是其它某個中斷響應操作,都應該小心地保存執行線程的中斷狀態。如果除了將中斷異常傳遞給調用者外,還需要進行其它操作,則因該在捕獲中斷異常之後回覆中斷狀態。

Thread.currentThread().interrupt();

 

切記,只有實現了線程中斷策略的代碼才能屏蔽中斷請求,在常規的任務和庫代碼中都不應該屏蔽中斷請求。

雖然有人質疑Java沒有提供搶佔式的中斷機制,但是開發人員通過處理中斷異常的方法,可以定製更爲靈活的中斷策略,從而在響應性和健壯性之間做出合理的平衡。

2.2響應中斷

當調用可中斷的阻塞函數時,例如Thread.sleep或者BlockingQueue.put等,有兩種實用策略可用於響應中斷

  • 傳遞異常(可能在執行某個特定於任務的清除操作之後),從而使你的方法也成爲可中斷的阻塞方法。
  • 恢復中斷狀態,從而使調用棧中的上層代碼能夠對其進行處理

①傳遞異常

將InterrupedException傳遞給調用者:

複製代碼
BlockingQueue<Task> queue;
    ……
    //拋出InterruptedException,從而將中斷傳遞給調用者
    public Task getNextTask() throws InterruptedException{
        return queue.take();
    }
複製代碼

②恢復中斷狀態

如果不想或無法傳遞InterruptedException(或者通過Runnable來定義任務),那麼需要尋找另外的方式來保存中斷請求。一種標準的方法就是通過再次調用interrupt來恢復中斷狀態。你不能屏蔽InterruptedException,例如在catch塊中捕獲到異常卻不做任務處理,除非在你的代碼中實現了線程的中斷策略。雖然PrimeProducer屏蔽了中斷,但這是因爲它已經知道線程將要結束,因此在調用棧中已經沒有上層代碼需要知道中斷信息。由於大多數代碼並不知道它們將在哪個線程中運行,因此應該保存中斷狀態。

對於一些不支持取消但仍可以調用可中斷阻塞方法的操作,它們必須在循環中調用這些方法,並在發現中斷後重新嘗試。在這種情況下,它們應該在本地保存中斷狀態,並在返回前恢復狀態而不是在捕獲InterruptedException時恢復狀態。如果過早地設置中斷狀態,就可能引起無限循環,因爲大多數可中斷的阻塞方法都會在入口處檢查中斷狀態,並且當發現該狀態已被設置時會立即拋出InterruptedException。(通常,可中斷的方法會在阻塞或進行重要的工作前首先檢查中斷,從而儘快地相應中斷)

不可取消的任務在退出前恢復中斷:

複製代碼
public TaskgetNextTask(BlockingQueue<Task>queue){
   booleaninterrupted=false;
   try{
      while(true){
          try{
              return queue.take();
          }
          catch (InterruptedException e){
             interrupted=true;
             // 重新嘗試
          }
      }
   }
   finaly{
      if (interrupted) 
          Thread.currentThread().interrupt();
   }
}
複製代碼

如果代碼不會調用可中斷的阻塞方法,那麼仍然可以通過在任務代碼中輪詢當前線程的中斷狀態來響應中斷。要選擇合適的輪詢頻率,就需要在效率和響應性之間進行權衡。如果響應性要求較高,那麼不應該調用那些執行時間較長並且不響應中斷的方法,從而對可調用的庫代碼進行一些限制。

3.通過Future來實現取消

在使用Future之前,我們還是先來看個計時運行的示例,來一步步引出Future。

3.1在外部線程中安排中斷(不要這麼做)

在下面的程序中,給出了在指定時間內運行一個任意的Runnable 的示例。它在調用線程中運行任務,並安排了一個取消任務,在運行指定的時間間隔後中斷它。這解決了從任務中拋出未檢查異常的問題,因爲該異常會被 timedRun 的調用者捕獲。

複製代碼
/**
 * 在外部線程中安排中斷(不要這麼做)
 */
public class TimedRun1 {
    private static final ScheduledExecutorService cancelExec = Executors.newScheduledThreadPool(1);

    public static void timedRun(Runnable r, long timeout, TimeUnit unit) {
        final Thread taskThread = Thread.currentThread();
        cancelExec.schedule(new Runnable() {//①安排一個取消任務,希望在運行指定的時間間隔後中斷②處的任務。
            public void run() {
                taskThread.interrupt();
            }
        }, timeout, unit);
        r.run();//②運行任務
    }
}
複製代碼

這是一種非常簡單的方法,但卻破壞了以下規則:在中斷線程之前,應該瞭解它的中斷策略。

由於timedRun可以從任意一個線程中調用,因此它無法知道這個調用線程的中斷策略。如果任務在超時之前完成,那麼中斷timedRun所在線程的取消任務將在 timedRun 返回到調用者之後啓動。我們不知道在這種情況下將運行什麼代碼,但結果一定是不好的。(可以使用schedule返回的ScheduleFuture來取消這個取消任務以避免這種風險,這種做法雖然可行,但是非常複雜。)

另外,如果任務不響應中斷,那麼timedRun會在任務結束時才返回,此時可能已經超過了指定的時限(或者還沒有超過時限),而限時運行的服務沒有在指定時間內返回,對調用者來說可能會帶來負面硬性。 

3.2在專門的線程中中斷任務(可解決問題,但也有缺陷)

在下面的程序中解決了最開始的異常處理問題以及上面解決方案中的問題。

執行任務的線程擁有自己的執行策略,即使任務不響應中斷,限時運行的方法仍能返回到它的調用者。在啓動任務線程之後,timedRun 將執行一個限時的 join 方法。在join返回後,它將檢查任務中是否有異常拋出,如果有的話,則會在調用timedRun 的線程中再次拋出該異常。

複製代碼
/**
 * 在專門的線程中中斷任務
 */
public class TimedRun2 {
    private static final ScheduledExecutorService cancelExec = newScheduledThreadPool(1);

    public static void timedRun(final Runnable r, long timeout, TimeUnit unit) throws InterruptedException {
        //方法內部類
        class RethrowableTask implements Runnable {
            //由於Throwable將在兩個線程之間共享,因此聲明爲volatile類型,從而確保安全地將其從任務線程發佈到timedRun線程。
            private volatile Throwable t;

            public void run() {
                try {
                    r.run();
                } catch (Throwable t) {
                    this.t = t;
                }
            }

            void rethrow() {
                if (t != null)
                    throw launderThrowable(t);
            }
        }

        RethrowableTask task = new RethrowableTask();
        final Thread taskThread = new Thread(task);
        taskThread.start();//①啓動任務
        cancelExec.schedule(new Runnable() {//
            public void run() {
                taskThread.interrupt();
            }
        }, timeout, unit);
        taskThread.join(unit.toMillis(timeout));//
        task.rethrow();//④join返回後,它將檢查任務中是否有異常拋出,如果有,則會在調用timedRun的線程中再次拋出該異常。
    }
}
複製代碼

這裏解決了前面示例中的問題,但由於它依賴一個限時的 join,因此存在着join的不足:無法知道執行控制是因爲線程正常退出而返回還是因爲join超時而返回。

3.3 通過Future來實現取消

Future是JDK庫中的類,可用來管理任務的生命週期,處理異常,也可以實現取消。通常,使用現有庫中的類比自行實現更好,所以我們可以直接使用Future來實現任務的取消。

看下面的實現:將任務提交給一個ExecutorSevice,並通過一個定時的Future.get來獲取結果。如果get返回時拋出了TimeoutException,那麼任務將通過它的Future來取消。(爲了簡化,這裏在finally中直接調用Future.cancel,因爲取消一個已完成的任務不會帶來任何影響)。如果任務在被取消前就被拋出一個異常,那麼該異常將被重新拋出以便由調用者來處理。

複製代碼
/**
 * 通過Future來取消任務
 */
public class TimedRun {
    private static final ExecutorService taskExec = Executors.newCachedThreadPool();

    public static void timedRun(Runnable r, long timeout, TimeUnit unit) throws InterruptedException {
        Future<?> task = taskExec.submit(r);
        try {
            task.get(timeout, unit);
        } catch (TimeoutException e) {
            // 因超時而取消任務
        } catch (ExecutionException e) {
            // 任務異常,重新拋出異常信息
            throw launderThrowable(e.getCause());
        } finally {
            // 如果該任務已經完成,將沒有影響
            // 如果任務正在運行,將因爲中斷而被取消
            task.cancel(true); // interrupt if running
        }
    }
}
複製代碼

這裏給出了一種良好的編程習慣:取消哪些不再需要結果的任務。

當Future.get拋出InterruptedException或TimeoutException時,如果不再需要結果,那麼就可以使用Future.cancel來取消任務。這是一種良好的編程習慣。

4.處理不可中斷的阻塞

參考: https://www.jianshu.com/p/613286f4245e

在java庫中,許多可阻塞的方法都是通過提前返回或者拋出InterruptedException來響應中斷請求的,從而使開發人員更容易構建出能響應取消請求的任務。然而並非所有的可阻塞方法或者阻塞機制都能響應中斷;如果一個線程由於執行同步的Socket I/O或者等待獲得內置鎖而阻塞,那麼中斷請求只能設置線程的中斷狀態,除此之外沒有其他任何作用。對於那些由於執行不可中斷操作而被阻塞的線程,可以使用類似於中斷的手段來停止這些線程,但這要求我們必須知道線程阻塞的原因。以下是不可中斷阻塞的情況:

       1. java.io包中的同步Socket I/O。如套接字中進行讀寫操作read, write方法。

       2. java.io包中的同步I/O。如當中斷或關閉正在InterruptibleChannel上等待的線程時,會對應拋出ClosedByInterruptException或AsynchronousCloseException。

       3. Selector的異步I/O。如果一個線程在調用Selector.select時阻塞了,那麼調用close, wakeup會使線程拋出ClosedSelectorException。

       4. 獲取某個鎖。當一個線程等待某個鎖而阻塞時,不會響應中斷。但Lock類的lockInterruptibly允許在等待鎖時響應中斷。

在下面的示例中,ReaderThread給出瞭如何封裝非標準的取消操作。ReaderThread 管理了一個套接字連接, 它採用同步方式從該套接字中讀取數據, 並將接收到的數據傳遞給processBuffer。爲了結束某個用戶的連接或者關閉服務器, ReaderThread改寫了interrupt方法,使其既能處理標準的中斷, 也能關閉底層的套接字。因此, 無論ReaderThread線程是在read方法中阻塞還是在某個可中斷的阻塞方法中阻塞, 都可以被中斷並停止執行當前的工作。
複製代碼
/**
 * 通過改寫Interrupt方法將非標準的取消操作封裝在Thread中。 
 */
public class ReaderThread extends Thread {
    private static final int BUFSZ = 512;
    private final Socket socket;
    private final InputStream in;

    public ReaderThread(Socket socket) throws IOException {
        this.socket = socket;
        this.in = socket.getInputStream();
    }

    /**
     * ReaderThread改寫了interrupt方法,使其既能處理標準的中斷,也能關閉底層的socket
     */
    public void interrupt() {
        try {
            // 關閉socket。此時in.read會拋出異常
            socket.close();
        } catch (IOException ignored) {
        } finally {
            // 正常的中斷
            super.interrupt();
        }
    }

    public void run() {
        try {
            byte[] buf = new byte[BUFSZ];
            while (true) {
                int count = in.read(buf);
                if (count < 0)
                    break;
                else if (count > 0)
                    processBuffer(buf, count);
            }
        } catch (IOException e) { 
            // 如果socket關閉,in.read方法將會拋出異常。藉此機會,響應中斷,線程退出
        }
    }

    public void processBuffer(byte[] buf, int count) {
    }
}
複製代碼

5.採用newTaskFor來封裝非標準的取消

我們可以通過newTaskFor方法來進一步優化ReaderThread中封裝非標準取消的技術, 這是Java 6 在ThreadPoolExecutor 中的新增功能。當把一個Callable 提交給ExecutorService 時,submit 方法會返回一個Future, 我們可以通過這個Future 來取消任務。newTaskFor是一個方法, 它將創建Future 來代表任務。newTaskFor 還能返回一個RunnableFuture 接口, 該接口擴展了Future 和Runnable (並由FutureTask 實現)。

       通過定製表示任務的Future 可以改變Future.cancel 的行爲。例如, 定製的取消代碼可以實現日誌記錄或者收集取消操作的統計信息, 以及取消一些不響應中斷的操作。通過改寫interrupt 方法, ReaderThread 可以取消基於套接字的線程。同樣, 通過改寫任務的Future.cancel 方法也可以實現類似的功能。

在下面的示例中,CancellableTask中定義了一個CancellableTask 接口, 該接口擴展了Callable,並增加了一個cancel 方法和一個newTask 工廠方法來構造RunnableFuture 。CancellingExecutor擴展了ThreadPoolExecutor, 並通過改寫newTaskFor 使得CancellableTask 可以創建自己的Future 。
複製代碼
/**
 *  通過newTaskFor將非標準的取消操作封裝在一個任務中
 */

/**
 * 擴展了Callable,並增加了一個cancel 方法和一個newTask工廠方法來構造RunnableFuture
 */
interface CancellableTask <T> extends Callable<T> {
    void cancel();

    RunnableFuture<T> newTask();
}

/**
 * 擴展了ThreadPoolExecutor,並通過改寫newTaskFor使得CancellableTask可以創建自己的Future
 */
@ThreadSafe
class CancellingExecutor extends ThreadPoolExecutor {

    protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
        if (callable instanceof CancellableTask)
            return ((CancellableTask<T>) callable).newTask();
        else
            return super.newTaskFor(callable);
    }
}

/**
 * 實現了CancellableTask,並定義了Future.cancel來關閉socket和調用super.cancel。如果SocketUsingTask通過其自己的Future來取消,那麼底層的socket將被關閉時確保響應取消操作,而且還能調用可阻塞的socket I/O方法
 */
public abstract class SocketUsingTask <T> implements CancellableTask<T> {
    @GuardedBy("this") private Socket socket;

    protected synchronized void setSocket(Socket s) {
        socket = s;
    }

    public synchronized void cancel() {
        try {
            if (socket != null)
                socket.close();
        } catch (IOException ignored) {
        }
    }

    public RunnableFuture<T> newTask() {
        return new FutureTask<T>(this) {
            public boolean cancel(boolean mayInterruptIfRunning) {
                try {
                    SocketUsingTask.this.cancel();
                } finally {
                    return super.cancel(mayInterruptIfRunning);
                }
            }
        };
    }
} 
複製代碼

二、停止基於線程的服務

與其它封裝對象一樣,線程的所有權是不可傳遞的:應用程序可以擁有服務,服務也可以擁有工作者線程,但應用程序並不能擁有工作者線程。用通俗易懂的話講就是應用程序管理服務,服務管理工作者線程,而應用程序不能直接管理工作者線程,因此應用程序不能直接停止工作者線程,而是應該由服務提供生命週期方法來關閉它自己以及它所擁有的線程。這樣,當應用程序關閉該服務時,服務就可以關閉所有的線程了。比如,在ExecutorService中就提供了shutdown和shutdownNow等方法。同樣,在其它擁有線程的服務中也應該提供類似的關閉機制。

1.關閉ExecutorService

ExecutorService提供了兩種關閉方法:

  • 使用shutdown正常關閉。
  • 使用shutdownNow強行關閉。

在進行強行關閉時, shutdownNow 首先關閉當前正在執行的任務,然後返回所有尚未啓動的任務清單。

這兩種關閉方式的差別在於各自的安全性和響應性:

強行關閉的速度更快,但風險也更大,因爲任務很可能在執行到一半時被結束;而正常關閉雖然速度慢,但卻更安全,因爲 ExecutorService會一直等到隊列中的所有任務都執行完成後才關閉。在其他擁有線程的服務中也應該考慮提供類似的關閉方式以供選擇。

 

簡單的程序可以直接在main 函數中啓動和關閉全局的 ExecutorService。而在複雜程序中,通常會將ExecutorService封裝在某個更高級別的服務中,並且該服務能提供自己的生命週期方法。例如下面程序清單中,它將管理線程的工作委託給一個ExecutorService,而不是由其自行管理。通過封裝 ExecutorService,可以將所有權鏈從應用程序擴展到服務以及線程,所有權鏈上的各個成員都將管理它所擁有的服務或線程的生命週期。

複製代碼
/**
 * 封裝ExecutorService實現日誌服務
 */
public class LogService {
    private final ExecutorService exec = Executors.newSingleThreadExecutor();
    private final PrintWriter writer;

    public LogService(PrintWriter writer) {
        this.writer = writer;
    }

    public void start(){

    }

    public void log(String msg) {
        try {
            exec.execute(new WriteTask(msg));
        } catch (RejectedExecutionException ignored) {
        }
    }

    public void stop(long timeout, TimeUnit unit) throws InterruptedException {
        try {
            exec.shutdown();
            // 關閉服務後, 阻塞到所有任務被執行完畢或者超時發生,或當前線程被中斷
            exec.awaitTermination(timeout, unit);
        } finally {
            writer.close();
        }
    }
}
複製代碼

2."毒丸"對象

“毒丸”是指一個放在隊列上的對象,其含義是:“當得到這個對象時,立即停止。”在FIFO 隊列中,“毒丸”對象將確保消費者在關閉之前首先完成隊列中的所有工作,在提交“毒丸”對象之前提交的所有工作都會被處理,而生產者在提交了“毒丸”對象後,將不會在提交任何工作。在下面的程序清單中給出了一個單生產者——單消費者的桌面搜索示例,使用了“毒丸”對象來關閉服務。

複製代碼
/**
 *  通過“毒丸”對象來關閉服務
 */
public class IndexingService {
    private static final int CAPACITY = 1000;
    //毒丸
    private static final File POISON = new File("");
    private final IndexerThread consumer = new IndexerThread();
    private final CrawlerThread producer = new CrawlerThread();
    private final BlockingQueue<File> queue;
    //private final FileFilter fileFilter;
    private final File root;

    public IndexingService(File root) {
        this.root = root;
        this.queue = new LinkedBlockingQueue<File>(CAPACITY);
        
    }

    private boolean alreadyIndexed(File f) {
        return false;
    }

    // IndexingService的生產者線程
    class CrawlerThread extends Thread {
        public void run() {
            try {
                crawl(root);
            } catch (InterruptedException e) { /* 發生異常 */
            } finally {
                while (true) {
                    try {
                        System.out.println("放入“毒丸”");
                        queue.put(POISON);//放入毒丸
                        break;
                    } catch (InterruptedException e1) { /* 重試 */
                    }
                }
            }
        }

        private void crawl(File root) throws InterruptedException {
            File[] entries = root.listFiles();
            if (entries != null) {
                for (File entry : entries) {
                    if (entry.isDirectory())
                        crawl(entry);
                    else if (!alreadyIndexed(entry)){
                        System.out.println("放入生產者隊列文件:"+entry.getName()+" 來自線程:"+Thread.currentThread().getName());
                        queue.put(entry);
                    }
                }
            }
        }
    }

    // IndexingService的消費者線程
    class IndexerThread extends Thread {
        public void run() {
            try {
                while (true) {
                    File file = queue.take();
                    // 遇到毒丸,終止
                    if (file == POISON){
                        System.out.println("遇到“毒丸”,終止");
                        break;
                    }   
                    else
                        indexFile(file);
                }
            } catch (InterruptedException consumed) {
            }
        }

        public void indexFile(File file) {
            System.out.println("消費者取出文件:"+file.getName()+" 來自線程:"+Thread.currentThread().getName());
            /* ... */
        };
    }

    public void start() {
        producer.start();
        consumer.start();
    }

    public void stop() {
        producer.interrupt();
    }

    public void awaitTermination() throws InterruptedException {
        consumer.join();
    }

}
複製代碼

只有在生產者和消費者的數量都已知的情況下,纔可以使用”毒丸“對象。

這種解決方案可以擴展到多個生產者的情況只需每個生產者都想隊列放入一個”毒丸“對象,並且消費者僅當在接收到N個生產者的”毒丸“對象時才停止。

這種解決方案也可以擴展到多個消費者的情況:只需生產者將N個”毒丸“對象放入隊列。

然而,當生產者和消費者的數量較大時,這種方法將變的難以使用。只有在無界隊列中,”毒丸“對象才能可靠地工作。

三、處理非正常的線程終止

導致線程提前死亡的最主要原因就是RuntimeException。由於這些異常表示出現了某種編程錯誤或者其它不可修復的錯誤,因此它們通常不會被捕獲。它們不會再調用棧中逐層傳遞,而是默認在控制檯中輸出棧追蹤信息,並終止線程。

線程非正常退出的後果是否嚴重要取決於線程的作用。比如,線程池中丟失一個線程可能會對性能帶來一定影響,但如果程序能在包含了50個線程的線程池上運行良好,那麼在包含了49個線程的線程池上通常也能運行良好。然而,對於GUI程序,如果丟失了事件分配線程,那麼造成的影響會非常顯著——應用程序將停止處理事件並且GUI會因此失去響應。

如果run()方法中的代碼拋出NullPointerException而失敗,可以將代碼放在try-catch(或try-finally)代碼塊中,這樣就能捕獲未檢查的異常了。

未捕獲異常的處理

上面介紹了是一種主動方法來解決未檢查異常。在Thread中同樣也提供了UncaughtExceptionHandler,它能檢測出某個由於未捕獲的異常而終結的情況。這兩種方法是互補的,通過將二者結合在一起,就能有效地防止線程泄露問題。

當一個線程由於未捕獲異常而退出時,JVM會把這個事件報告給UncaughtExceptionHandler異常處理器,如果沒有提供異常處理器,那麼默認的行爲是將追蹤信息輸出到Sytem.err,即控制檯。

異常處理器該如何處理未捕獲的異常呢?最常見的方式是將錯誤信息及相應的棧追蹤信息寫入日誌中,如下所示。異常處理器還可以採取更直接的響應,例如嘗試重啓線程,關閉應用程序,或執行其它修復或診斷等操作。

複製代碼
/**
 * 將異常寫入日誌的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);
    }

}
複製代碼

則在使用時只需要爲線程設置一個UncaughtExceptionHandler即可。

Thread thread=new Thread();
thread.setUncaughtExceptionHandler(new UEHLogger()); 
thread.start();

 

在運行時間較長的應用程序中,通常會爲所有線程的未捕獲異常指定同一個異常處理器,並且該處理器異常至少會將異常信息記錄到日誌中。

要爲線程池中的所有線程設置一個UncaughtExceptionHandler,只需要爲ThreadPoolExecutor的構造函數提供一個ThreadFactory。這部分可以參考另一篇博客:多線程異常處理

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章