java安全編碼指南之:ThreadPool的使用

flydean發表於2020-10-20

簡介

在java中,除了單個使用Thread之外,我們還會使用到ThreadPool來構建執行緒池,那麼在使用執行緒池的過程中需要注意哪些事情呢?

一起來看看吧。

java自帶的執行緒池

java提供了一個非常好用的工具類Executors,通過Executors我們可以非常方便的建立出一系列的執行緒池:

Executors.newCachedThreadPool,根據需要可以建立新執行緒的執行緒池。執行緒池中曾經建立的執行緒,在完成某個任務後也許會被用來完成另外一項任務。

Executors.newFixedThreadPool(int nThreads) ,建立一個可重用固定執行緒數的執行緒池。這個執行緒池裡最多包含nThread個執行緒。

Executors.newSingleThreadExecutor() ,建立一個使用單個 worker 執行緒的 Executor。即使任務再多,也只用1個執行緒完成任務。

Executors.newSingleThreadScheduledExecutor() ,建立一個單執行緒執行程式,它可安排在給定延遲後執行命令或者定期執行。

提交給執行緒池的執行緒要是可以被中斷的

ExecutorService執行緒池提供了兩個很方便的停止執行緒池中執行緒的方法,他們是shutdown和shutdownNow。

shutdown不會接受新的任務,但是會等待現有任務執行完畢。而shutdownNow會嘗試立馬終止現有執行的執行緒。

那麼它是怎麼實現的呢?我們看一個ThreadPoolExecutor中的一個實現:

    public List<Runnable> shutdownNow() {
        List<Runnable> tasks;
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            advanceRunState(STOP);
            interruptWorkers();
            tasks = drainQueue();
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
        return tasks;
    }

裡面有一個interruptWorkers()方法的呼叫,實際上就是去中斷當前執行的執行緒。

所以我們可以得到一個結論,提交到ExecutorService中的任務一定要是可以被中斷的,否則shutdownNow方法將會失效。

先看一個錯誤的使用例子:

    public void wrongSubmit(){
        Runnable runnable= ()->{
            try(SocketChannel  sc = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8080))) {
            ByteBuffer buf = ByteBuffer.allocate(1024);
            while(true){
                sc.read(buf);
            }
            } catch (IOException e) {
                e.printStackTrace();
            }
        };
        ExecutorService pool =  Executors.newFixedThreadPool(10);
        pool.submit(runnable);
        pool.shutdownNow();
    }

在這個例子中,執行的程式碼無法處理中斷,所以將會一直執行。

下面看下正確的寫法:

    public void correctSubmit(){
        Runnable runnable= ()->{
            try(SocketChannel  sc = SocketChannel.open(new InetSocketAddress("127.0.0.1", 8080))) {
                ByteBuffer buf = ByteBuffer.allocate(1024);
                while(!Thread.interrupted()){
                    sc.read(buf);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        };
        ExecutorService pool =  Executors.newFixedThreadPool(10);
        pool.submit(runnable);
        pool.shutdownNow();
    }

我們需要在while迴圈中加上中斷的判斷,從而控制程式的執行。

正確處理執行緒池中執行緒的異常

如果線上程池中的執行緒發生了異常,比如RuntimeException,我們怎麼才能夠捕捉到呢? 如果不能夠對異常進行合理的處理,那麼將會產生不可預料的問題。

看下面的例子:

    public void wrongSubmit() throws InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(10);
        Runnable runnable= ()->{
            throw new NullPointerException();
        };
        pool.execute(runnable);
        Thread.sleep(5000);
        System.out.println("finished!");
    }

上面的例子中,我們submit了一個任務,在任務中會丟擲一個NullPointerException,因為是非checked異常,所以不需要顯式捕獲,在任務執行完畢之後,我們基本上是不能夠得知任務是否執行成功了。

那麼,怎麼才能夠捕獲這樣的執行緒池異常呢?這裡介紹大家幾個方法。

第一種方法就是繼承ThreadPoolExecutor,重寫

 protected void afterExecute(Runnable r, Throwable t) { }

protected void terminated() { }

這兩個方法。

其中afterExecute會在任務執行完畢之後被呼叫,Throwable t中儲存的是可能出現的執行時異常和Error。我們可以根據需要進行處理。

而terminated是線上程池中所有的任務都被呼叫完畢之後才被呼叫的。我們可以在其中做一些資源的清理工作。

第二種方法就是使用UncaughtExceptionHandler。

Thread類中提供了一個setUncaughtExceptionHandler方法,用來處理捕獲的異常,我們可以在建立Thread的時候,為其新增一個UncaughtExceptionHandler就可以了。

但是ExecutorService執行的是一個個的Runnable,怎麼使用ExecutorService來提交Thread呢?

別怕, Executors在構建執行緒池的時候,還可以讓我們傳入ThreadFactory,從而構建自定義的Thread。

    public void useExceptionHandler() throws InterruptedException {
        ThreadFactory factory =
                new ExceptionThreadFactory(new MyExceptionHandler());
        ExecutorService pool =
                Executors.newFixedThreadPool(10, factory);
        Runnable runnable= ()->{
            throw new NullPointerException();
        };
        pool.execute(runnable);
        Thread.sleep(5000);
        System.out.println("finished!");
    }

    public static class ExceptionThreadFactory implements ThreadFactory {
        private static final ThreadFactory defaultFactory =
                Executors.defaultThreadFactory();
        private final Thread.UncaughtExceptionHandler handler;

        public ExceptionThreadFactory(
                Thread.UncaughtExceptionHandler handler)
        {
            this.handler = handler;
        }

        @Override
        public Thread newThread(Runnable run) {
            Thread thread = defaultFactory.newThread(run);
            thread.setUncaughtExceptionHandler(handler);
            return thread;
        }
    }

    public static class MyExceptionHandler implements Thread.UncaughtExceptionHandler {
        @Override
        public void uncaughtException(Thread t, Throwable e) {

        }
    }

上面的例子有點複雜了, 有沒有更簡單點的做法呢?

有的。ExecutorService除了execute來提交任務之外,還可以使用submit來提交任務。不同之處是submit會返回一個Future來儲存執行的結果。

    public void useFuture() throws InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(10);
        Runnable runnable= ()->{
            throw new NullPointerException();
        };
        Future future = pool.submit(runnable);
        try {
            future.get();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
        Thread.sleep(5000);
        System.out.println("finished!");
    }

當我們在呼叫future.get()來獲取結果的時候,異常也會被封裝到ExecutionException,我們可以直接獲取到。

執行緒池中使用ThreadLocal一定要注意清理

我們知道ThreadLocal是Thread中的本地變數,如果我們線上程的執行過程中用到了ThreadLocal,那麼當執行緒被回收之後再次執行其他的任務的時候就會讀取到之前被設定的變數,從而產生未知的問題。

正確的使用方法就是線上程每次執行完任務之後,都去呼叫一下ThreadLocal的remove操作。

或者在自定義ThreadPoolExecutor中,重寫beforeExecute(Thread t, Runnable r)方法,在其中加入ThreadLocal的remove操作。

本文的程式碼:

learn-java-base-9-to-20/tree/master/security

本文已收錄於 http://www.flydean.com/java-security-code-line-threadpool/

最通俗的解讀,最深刻的乾貨,最簡潔的教程,眾多你不知道的小技巧等你來發現!

歡迎關注我的公眾號:「程式那些事」,懂技術,更懂你!

相關文章