Java實戰001-執行緒池ExecutorService

姚根深發表於2019-01-19

執行緒池ExecutorService

一. new Thread的弊端

執行一個非同步任務你還只是如下new Thread嗎?

new Thread(new Runnable() {
    @Override
    public void run() {
        // TODO Auto-generated method stub
    }
}).start();

那你就太out了,new Thread的弊端如下:

  1. 每次new Thread新建物件效能差.
  2. 執行緒缺乏統一管理,可能無限制新建執行緒,相互之間競爭,及可能佔用過多系統資源導致當機或oom.
  3. 缺乏更多功能,如定時執行、定期執行、執行緒中斷.

相比new Thread,Java提供的四種執行緒池的好處在於:

  1. 重用存在的執行緒,減少物件建立、消亡的開銷,效能佳.
  2. 可有效控制最大併發執行緒數,提高系統資源的使用率,同時避免過多資源競爭,避免堵塞.
  3. 提供定時執行、定期執行、單執行緒、併發數控制等功能.  

二. ExecutorService詳解

Java通過Executors提供四種執行緒池,分別為:

  1. newCachedThreadPool建立一個可快取執行緒池,如果執行緒池長度超過處理需要,可靈活回收空閒執行緒,若無可回收,則新建執行緒.
  2. newFixedThreadPool 建立一個定長執行緒池,可控制執行緒最大併發數,超出的執行緒會在佇列中等待.
  3. newScheduledThreadPool 建立一個定長執行緒池,支援定時及週期性任務執行.
  4. newSingleThreadExecutor 建立一個單執行緒化的執行緒池,它只會用唯一的工作執行緒來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先順序)執行.

01. submit()與execute()區別

  1. 接收的引數不一樣 submit()可以接受runnable和callable 有返回值execute()接受runnable 無返回值
  2. submit有返回值,而execute沒有

Method submit extends base method Executor.execute by creating and returning a Future that can be used to cancel execution and/or wait for completion.
用到返回值的例子,比如說我有很多個做validation的task,我希望所有的task執行完,然後每個task告訴我它的執行結果,是成功還是失敗,如果是失敗,原因是什麼.

  1. submit方便Exception處理

There is a difference when looking at exception handling. If your tasks throws an exception and if it was submitted with execute this exception will Go to the uncaught exception handler (when you don’t have provided one explicitly, the default one will just print the stack trace to System.err). If you submitted the task with submit any thrown exception, checked or not, is then part of the task’s return status. For a task that was submitted with submit and that terminates with an exception, the Future.get will rethrow this exception, wrapped in an ExecutionException.
意思就是如果你在你的task裡會丟擲checked或者unchecked exception,而你又希望外面的呼叫者能夠感知這些exception並做出及時的處理,那麼就需要用到submit,通過捕獲Future.get丟擲的異常.

import java.util.ArrayList;  
import java.util.List;  
import java.util.Random;  
import java.util.concurrent.Callable;  
import java.util.concurrent.ExecutionException;  
import java.util.concurrent.ExecutorService;  
import java.util.concurrent.Executors;  
import java.util.concurrent.Future;  

public class ExecutorServiceTest {  
    public static void main(String[] args) {  
        ExecutorService executorService = Executors.newCachedThreadPool();  
        List<Future<String>> resultList = new ArrayList<Future<String>>();  

        // 建立10個任務並執行  
        for (int i = 0; i < 10; i++) {  
            // 使用ExecutorService執行Callable型別的任務,並將結果儲存在future變數中  
            Future<String> future = executorService.submit(new TaskWithResult(i));  
            // 將任務執行結果儲存到List中  
            resultList.add(future);  
        }  
        executorService.shutdown();  

        // 遍歷任務的結果  
        for (Future<String> fs : resultList) {  
            try {  
                System.out.println(fs.get()); // 列印各個執行緒(任務)執行的結果  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            } catch (ExecutionException e) {  
                executorService.shutdownNow();  
                e.printStackTrace();  
                return;  
            }  
        }  
    }
}
class TaskWithResult implements Callable<String> {  
    private int id;  

    public TaskWithResult(int id) {  
        this.id = id;  
    }  

    /** 
     * 任務的具體過程,一旦任務傳給ExecutorService的submit方法,則該方法自動在一個執行緒上執行. 
     *  
     * @return 
     * @throws Exception 
     */  
    public String call() throws Exception {  
        System.out.println("call()方法被自動呼叫,幹活!!!             " + Thread.currentThread().getName());  
        if (new Random().nextBoolean())  
            throw new TaskException("Meet error in task." + Thread.currentThread().getName());  
        // 一個模擬耗時的操作  
        for (int i = 999999999; i > 0; i--)  
            ;  
        return "call()方法被自動呼叫,任務的結果是:" + id + "    " + Thread.currentThread().getName();  
    }  
}  

class TaskException extends Exception {  
    public TaskException(String message) {  
        super(message);  
    }  
}  

執行的結果類似於:

call()方法被自動呼叫,幹活!!!             pool-1-thread-1 
call()方法被自動呼叫,幹活!!!             pool-1-thread-2 
call()方法被自動呼叫,幹活!!!             pool-1-thread-3 
call()方法被自動呼叫,幹活!!!             pool-1-thread-5 
call()方法被自動呼叫,幹活!!!             pool-1-thread-7 
call()方法被自動呼叫,幹活!!!             pool-1-thread-4 
call()方法被自動呼叫,幹活!!!             pool-1-thread-6 
call()方法被自動呼叫,幹活!!!             pool-1-thread-7 
call()方法被自動呼叫,幹活!!!             pool-1-thread-5 
call()方法被自動呼叫,幹活!!!             pool-1-thread-8 
call()方法被自動呼叫,任務的結果是:0    pool-1-thread-1 
call()方法被自動呼叫,任務的結果是:1    pool-1-thread-2 
java.util.concurrent.ExecutionException: com.cicc.pts.TaskException: Meet error in task.pool-1-thread-3 
    at java.util.concurrent.FutureTask$Sync.innerGet(FutureTask.java:222) 
    at java.util.concurrent.FutureTask.get(FutureTask.java:83) 
    at com.cicc.pts.ExecutorServiceTest.main(ExecutorServiceTest.java:29) 
Caused by: com.cicc.pts.TaskException: Meet error in task.pool-1-thread-3 
    at com.cicc.pts.TaskWithResult.call(ExecutorServiceTest.java:57) 
    at com.cicc.pts.TaskWithResult.call(ExecutorServiceTest.java:1) 
    at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:303) 
    at java.util.concurrent.FutureTask.run(FutureTask.java:138) 
    at java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886) 
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908) 
    at java.lang.Thread.run(Thread.java:619) 

  
可以看見一旦某個task出錯,其它的task就停止執行.

02. shutdown() shutdownNow()區別

可以關閉 ExecutorService,這將導致其拒絕新任務.提供兩個方法來關閉 ExecutorService.
shutdown() 方法在終止前允許執行以前提交的任務,
shutdownNow() 方法阻止等待任務啟動並試圖停止當前正在執行的任務.在終止時執行程式沒有任務在執行,也沒有任務在等待執行,並且無法提交新任務.關閉未使用的 ExecutorService 以允許回收其資源.
一般分兩個階段關閉 ExecutorService.第一階段呼叫 shutdown 拒絕傳入任務,然後呼叫 shutdownNow(如有必要)取消所有遺留的任務

// 啟動一次順序關閉,執行以前提交的任務,但不接受新任務.

threadPool.shutdown();

 

03. Runnable()與Callable()區別

如果是一個多執行緒協作程式,比如菲波拉切數列,1,1,2,3,5,8…使用多執行緒來計算.
但後者需要前者的結果,就需要用callable介面了.
callable用法和runnable一樣,只不過呼叫的是call方法,該方法有一個泛型返回值型別,你可以任意指定.
runnable介面實現的沒有返回值的併發程式設計.
callable實現的存在返回值的併發程式設計.(call的返回值String受泛型的影響) 使用Future獲取返回值.
這裡寫圖片描述

三. 執行緒池使用例項

01. newCachedThreadPool

建立一個可快取執行緒池,如果執行緒池長度超過處理需要,可靈活回收空閒執行緒,若無可回收,則新建執行緒.示例程式碼如下:

ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
    final int index = i;
    try {
        Thread.sleep(index * 1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
 
    cachedThreadPool.execute(new Runnable() {
 
        @Override
        public void run() {
            System.out.println(index);
        }
    });
}

執行緒池為無限大,當執行第二個任務時第一個任務已經完成,會複用執行第一個任務的執行緒,而不用每次新建執行緒.

02. newFixedThreadPool

建立一個定長執行緒池,可控制執行緒最大併發數,超出的執行緒會在佇列中等待.示例程式碼如下:

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
    final int index = i;
    fixedThreadPool.execute(new Runnable() {
 
 
        @Override
        public void run() {
            try {
                System.out.println(index);
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    });
}

因為執行緒池大小為3,每個任務輸出index後sleep 2秒,所以每兩秒列印3個數字.
定長執行緒池的大小最好根據系統資源進行設定.如Runtime.getRuntime().availableProcessors().可參考PreloadDataCache.

03. newScheduledThreadPool

建立一個定長執行緒池,支援定時及週期性任務執行.延遲執行示例程式碼如下:

ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
scheduledThreadPool.schedule(new Runnable() {
    @Override
    public void run() {
        System.out.println("delay 3 seconds");
    }
}, 3, TimeUnit.SECONDS);

表示延遲3秒執行.
定期執行示例程式碼如下:

scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
    @Override
    public void run() {
        System.out.println("delay 1 seconds, and excute every 3 seconds");
    }
}, 1, 3, TimeUnit.SECONDS);

表示延遲1秒後每3秒執行一次.
ScheduledExecutorService比Timer更安全,功能更強大,後面會有一篇單獨進行對比.

04. newSingleThreadExecutor

建立一個單執行緒化的執行緒池,它只會用唯一的工作執行緒來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先順序)執行.示例程式碼如下:

ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
    final int index = i;
    singleThreadExecutor.execute(new Runnable() {
 
        @Override
        public void run() {
            try {
                System.out.println(index);
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }
    });
}

結果依次輸出,相當於順序執行各個任務.
現行大多數GUI程式都是單執行緒的.Android中單執行緒可用於資料庫操作,檔案操作,應用批量安裝,應用批量刪除等不適合併發但可能IO阻塞性及影響UI執行緒響應的操作.  
總結:

  1. 使用ExecutorService的submit函式由於execute函式
  2. 異常如何處理,異常後其他task停止

四. 執行緒安全問題

01. 日期型別轉換

SimpleDateFormat來做Date到String的型別轉換,建議使用Apache commons-lang中的FastDateFormat。
因為JDK裡自帶的SimpleDateFormat存線上程不安全問題。
maven依賴:

<dependency>
    <groupId>commons-lang</groupId>
    <artifactId>commons-lang</artifactId>
    <version>2.5</version>
</dependency>

程式碼:

private String initDate() {
    Date d = new Date();
    FastDateFormat fdf = FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss");
    return fdf.format(d);
}

多執行緒問題,主要是多執行緒執行時的順序是隨機的,無法保證同一程式碼的執行順序,任意兩步程式碼(非原子)操作都存在安全問題

02. 執行緒安全型別

多執行緒問題,主要是多執行緒執行時的順序是隨機的,無法保證同一程式碼的執行順序,任意兩步程式碼(非原子)操作都存在安全問題

(01). 鎖在String

String str="a";
synchronized(str){
    str = "b";  //str變為一個新物件,鎖失效,字元的賦值是新new一個String然後賦值的
}

(02). AtomicInteger

i++,使用java.util.concurrent.atomic下的原子類代替來做多執行緒的計數器
i++是兩步,讀取i的變數的值,然後更新+1,所以不安全, 使用AtomicInteger

(03). ConcurrentHashMap,CopyOnWriteArrayList

HashMap,ArrayList,使用ConcurrentHashMap,CopyOnWriteArrayList代替

(04). StringBuffer

StringBuilder,使用StringBuffer代替

參考引用: 
https://www.cnblogs.com/Steve…
https://blog.csdn.net/insistg…
https://blog.csdn.net/c511362…

相關文章