多執行緒筆記 三

CaptainZ發表於2018-05-16

多執行緒筆記一

多執行緒筆記二

多執行緒筆記三

多執行緒相關問題

1. ThreadPoolExecutor

ThreadPoolExecutor是一種比較常見的處理多執行緒的執行器。你可以配置執行緒池的最小執行緒數,當執行器沒有太多的任務要處理的時候。亦可以配置最大執行緒size,如果有很多工需要處理。一旦當工作負載降下來,執行緒池就會慢慢的減少執行緒數量,知道執行緒數量達到最小值。

    ThreadPoolExecutor pool = new ThreadPoolExecutor(
            1, // keep at least one thread ready,
            // even if no Runnables are executed
            5, // at most five Runnables/Threads
            // executed in parallel
            1, TimeUnit.MINUTES, // idle Threads terminated after one
            // minute, when min Pool size exceeded
            new ArrayBlockingQueue<Runnable>(10)); // outstanding Runnables are kept here
         pool.execute(new Runnable() {
            @Override 
            public void run () {
                //code to run
            }
    });
    
複製程式碼
  • 備註:如果你配置了一個無界佇列的執行緒池。那麼執行緒數量將不會超過corePoolSize .
  • 執行緒池引數介紹
ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory,
RejectedExecutionHandler handler)

複製程式碼

如果執行緒池的數量大於corePoolSize並且小於maximumPoolSize,那麼只有線上程池佇列滿了的情況下才會建立新執行緒。

執行緒池的優點:

  1. BlockingQueue 能夠避免記憶體溢位的場景。應用的表現不會被佇列的尺寸限制。

  2. 可以採用不同的Rejection Handler 策略。

    1. 預設策略:丟擲RejectedExecutionException
    2. CallerRunsPolicy :如果執行緒池沒有被關閉,那麼就執行,否則丟棄
    3. DiscardPolicy: 無法執行,直接丟棄
    4. DiscardOldestPolicy :丟棄隊首的任務。
  3. 配置自定義的ThreadFactory的好處.

    1. 定義更有描述性的名稱
    2. 設定程式的狀態
    3. 設定執行緒優先權

2.獲取計算任務的值-callable

  • 如果你的運算會產生一些以後需要用到的資料,一個簡單的runnable是肯定無法滿足你的。在這種場景下,你可以使用 ExecutorService.submit(Callable) 。這個方法會在任務執行完畢之後返回一個值。
  • 這個方法返回一個Future物件,你可以從中獲取任務的值。
// Submit a callable for execution
    ExecutorService pool = anExecutorService;
    Future<Integer> future = pool.submit(new Callable<Integer>() {
    @Override public Integer call() {
    //do some computation
    return new Random().nextInt();
    }
    });
// ...  perform other tasks while future is executed in a different thread
複製程式碼
  • 當你要獲得執行結果時候,呼叫future.get()

  • 不確定等到時間的方法 get

    try {
    // Blocks current thread until future is completed
    Integer result = future.get();
    catch (InterruptedException || ExecutionException e) {
    // handle appropriately
    }
複製程式碼
  • 在指定時間等待結果
    try {
    // Blocks current thread for a maximum of 500 milliseconds.
    // If the future finishes before that, result is returned,
    // otherwise TimeoutException is thrown.
    Integer result = future.get(500, TimeUnit.MILLISECONDS);
    catch (InterruptedException || ExecutionException || TimeoutException e) {}
複製程式碼
如果計算結果不在需要了,你可以呼叫Future.cancel(boolean)
  • cancel(false) 將只會將任務從佇列裡面一處
  • cancel(true) 還會打斷當前執行的任務。

3. submit()與execute() 異常處理的區別

  • 普通的 execute() 命令一般是用來執行不要結果的任務,submit() 一般是用來分析Future 物件。
  • 我們應該關心著兩種異常處理機制不同之處。
  • submit() 方法如果不處理就會被框架處理。

示例程式碼如下:

案例1:用excute 命令來執行 runnable 任務,然後上報異常
import java.util.concurrent .*;
import java.util .*;

    public class ExecuteSubmitDemo {
        public ExecuteSubmitDemo() {
            System.out.println("creating service");
            ExecutorService service = Executors.newFixedThreadPool(2);
//ExtendedExecutor service = new ExtendedExecutor();
            for (int i = 0; i < 2; i++) {
                service.execute(new Runnable() {
                    public void run() {
                        int a = 4, b = 0;
                        System.out.println("a and b=" + a + ":" + b);
                        System.out.println("a/b:" + (a / b));
                        System.out.println("Thread Name in Runnable after divide by
                                zero:"+Thread.currentThread().getName());
                    }
                });
            }
            service.shutdown();
        }

        public static void main(String args[]) {
            ExecuteSubmitDemo demo = new ExecuteSubmitDemo();
        }
    }

    class ExtendedExecutor extends ThreadPoolExecutor {
        public ExtendedExecutor() {
            super(1, 1, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(100));
        }

        // ...
        protected void afterExecute(Runnable r, Throwable t) {
            super.afterExecute(r, t);
            if (t == null && r instanceof Future<?>) {
                try {
                    Object result = ((Future<?>) r).get();
                } catch (CancellationException ce) {
                    t = ce;
                } catch (ExecutionException ee) {
                    t = ee.getCause();
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt(); // ignore/reset
                }
            }
            if (t != null)
                System.out.println(t);
        }
    }
複製程式碼

輸出:

creating service
a and b=4:0
a and b=4:0
Exception in thread "pool-1-thread-1" Exception in thread "pool-1-thread-2"
java.lang.ArithmeticException: / by zero
at ExecuteSubmitDemo$1.run(ExecuteSubmitDemo.java:15)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at java.lang.Thread.run(Thread.java:744)
java.lang.ArithmeticException: / by zero
at ExecuteSubmitDemo$1.run(ExecuteSubmitDemo.java:15)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
at java.lang.Thread.run(Thread.java:744)
複製程式碼
案例2:用submit 替換excute ,service.submit(new Runnable() 在這個案例中,異常被框架吃了。

輸出

creating service
a and b=4:0
a and b=4:0
複製程式碼

案例3 將newFixedThreadPool換成ExtendedExecutor

ExtendedExecutor service = new ExtendedExecutor();
複製程式碼

輸出:

creating service
a and b=4:0
java.lang.ArithmeticException: / by zero
a and b=4:0
java.lang.ArithmeticException: / by zero
複製程式碼
  • 我們評估上面兩個案例,得出答案,使用自定義的執行緒池來處理異常。
  • 其他解決上述問題的方法,如果你使用普通的 ExecutorService & submit ,使用get來獲取結果。那麼請捕獲上述程式碼裡面捕獲的三個異常。自定義ThreadPoolExecutor 有一個好處,即是隻需要在一個地方捕捉異常。

4. 處理拒絕執行

如果:

  1. 你試圖向一個一個關閉的Executor 提交任務
  2. 佇列已經滿了,執行緒數已經達到最大值
  • 那麼RejectedExecutionHandler.rejectedExecution(Runnable, ThreadPoolExecutor)將會被呼叫。
  • 上述方法的預設行為是丟擲一個RejectedExecutionException異常。但是我們有更多的策略可以選擇:
    1. ThreadPoolExecutor.AbortPolicy (default, will throw REE)
    2. ThreadPoolExecutor.CallerRunsPolicy (executes task on caller's thread - blocking it)
    3. ThreadPoolExecutor.DiscardPolicy (silently discard task)
    4. ThreadPoolExecutor.DiscardOldestPolicy (silently discard oldest task in queue and retry execution of the new task)

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              RejectedExecutionHandler handler) // <--
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) // <
複製程式碼
  • 你也可以自己實現RejectedExecutionHandler 介面來自定義執行的行為方式。
void rejectedExecution(Runnable r, ThreadPoolExecutor executor)
複製程式碼

5 :發射後不管 - Runnable Tasks

Executors接受一個 java.lang.Runnable 引數物件,用來處理耗時或者計算量較大的任務。

  • 使用方法如下:
 Executor exec = Executors.newCachedThreadPool();
    exec.ex(new Runnable() {
            @Override public void run () {
    //offloaded work, no need to get result backJava® Notes for Professionals 696
            }
        });
        
複製程式碼

注意使用這個excutor ,你獲取不到任何資料返回值。在Java8 中你可以應用lamda 來簡化程式碼

    Executor exec = anExecutor;
    exec.execute(() -> {
    //offloaded work, no need to get result back
    });
複製程式碼

6:不同型別的併發的構造的使用案例:

  1. ExecutorService
  • ExecutorService executor = Executors.newFixedThreadPool(50);
  • 這種用法事發簡單。它隱藏了很多執行緒池的底層實現。
  • 在任務數量較小我傾向使用這種方式,它不會讓記憶體增長過快也不會降低系統的效能。如果你有cpu/記憶體 方面的約束,我建議使用執行緒池的時候對執行緒的容量以及 處理拒絕執行的任務。
  1. CountDownLatch
  • CountDownLatch 使用固定的數初始化。這個數會隨著countdown 方法被呼叫而減少。我們可以通過呼叫await方法讓當前 執行緒等待執行緒執行直至數量降至0。

使用條件:

  •     1. 實現最大數量的非同步任務:在某些情況,我們可以在同時啟動一組執行緒
    複製程式碼
  •     2. 在開始執行之前等待其他執行緒執行
    複製程式碼
  •     3. 死鎖檢測
    複製程式碼
  1. ThreadPoolExecutor
  • 提供了更多的控制。如果程式被限制了要執行一組延遲Runnable/Callable任務,你可以通過設定最大容積來使用有界陣列。一旦佇列達到最大容量,你可以定義一個 RejectionHandler 來處理拒絕的任務。Java提供了四種型別的RejectionHandler 不同的實現策略。上述已經提及了,這裡就不進行贅述了。
  1. 你可能不知道 ForkJoinPool
  • ForkJoinPool是Java 7 引入的。ForkJoinPool與 ExecutorService類似,但是還是有一點區別。ForkJoinPool與非常容易實現任務分割成一些小任務。當某些佇列任務執行完畢之後,他便會從其他佇列的尾部偷取一個任務進行執行。
    複製程式碼
  • Java 8 引入了一個新的api ,你不需要新建RecursiveTask 任務就只可以直接使用ForkJoinPool;
    複製程式碼
 public static ExecutorService newWorkStealingPool()
複製程式碼

建立work-stealing執行緒池會根據處理器的並行程度最大程度的利用處理器

  • 預設來說,他會採用cpu 的核心來作為引數。
上述提到的四種原理互不影響。你可以根據你自己的需求,選用合適的框架進行使用

7. 使用ExecutorService等待所有任務執行完畢

  • 我們先來看一眼方法:
  • ExecutorService invokeAll()
    複製程式碼

    當所有任務都執行完畢會返回一個Futures list。

示例程式碼:

    import java.util.concurrent .*;
    import java.util .*;

    public class InvokeAllDemo {
        public InvokeAllDemo() {
            System.out.println("creating service");
            ExecutorService service =
                    Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
            List<MyCallable> futureList = new ArrayList<MyCallable>();
   
            for (int i = 0; i < 10; i++) {
                MyCallable myCallable = new MyCallable((long) i);
                futureList.add(myCallable);
            }
            System.out.println("Start");
            try {
                List<Future<Long>> futures = service.invokeAll(futureList);
            } catch (Exception err) {
                err.printStackTrace();
            }
            System.out.println("Completed");
            service.shutdown();
        }

        public static void main(String args[]) {
            InvokeAllDemo demo = new InvokeAllDemo();
        }

        class MyCallable implements Callable<Long> {
            Long id = 0L;

            public MyCallable(Long val) {
                this.id = val;
            }

            public Long call() {
// Add your business logic
                return id;
            }
        }
    }
複製程式碼

8. 使用不同型別的ExecutorService

Executors 返回不同型別的執行緒池來滿足不同需求

1. 1. public static ExecutorService newSingleThreadExecutor()

建立一個單工作執行緒來操作一個無界佇列

它與 newFixedThreadPool(1) 和 newSingleThreadExecutor()的區別Java doc 是這樣說的:

與類似的 newFixedThreadPool(1)相比其不保證重新配置異常執行緒來使用替代執行緒。

  • 這就意味著newFixedThreadPool可以被程式重新配置類似如下:((ThreadPoolExecutor) fixedThreadPool).setMaximumPoolSize(10),這在newSingleThreadExecutor 是不可能的。
  • 使用場景: 1. 你打算按照提交順序來執行任務 2. 你需要一個執行緒來執行你的所有請求 缺點:無界佇列有風險
2. public static ExecutorService newFixedThreadPool(int nThreads)

建立一個固定數量的執行緒池,複用執行緒來操作一個共享的無界佇列。在任何時刻,大多數執行緒都會在執行任務的時候被啟用。如果提交了額外的任務,在所有執行緒都是啟用狀態的時候,那麼他們將會阻塞知道有空閒執行緒。

使用場景:

  1.         可以通過獲取cpu的數量來提高執行緒執行情況。
    複製程式碼
  2.          你可以選擇執行緒池的最大執行緒數
    複製程式碼

缺點:無界佇列有風險

3. public static ExecutorService newCachedThreadPool()

建立一個按需分配的執行緒池,會重複利用之前已經建立的執行緒。

使用條件:

  1.   一些執行時間段的非同步任務
    複製程式碼
  • 缺點:
    1. 無界佇列有風險
    2. 如果所有的存在的執行緒都在busy 那麼每個任務都會新建一個執行緒。如果任務長期執行,將會建立大量的執行緒,這將會降低系統的效能。在這種情況下推薦newFixedThreadPool。
4. public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)

建立一個執行緒池,是的能夠在指定時間段之後執行任務,或者每隔一段時間執行

使用場景:

  1.     處理週期性的事件
    複製程式碼

缺點:無界佇列有風險。

5. public static ExecutorService newWorkStealingPool()

建立任務偷取型的執行緒池,取決於處理器的併發水平

使用場景
  1.     將任務分割成很多子任務
    複製程式碼
  2.     對於空閒執行緒處理能力較高
    複製程式碼

缺點:無界佇列有風險

  • 你可能發現了一個通用的缺陷:無界佇列。它會與ThreadPoolExecutor一起繫結使用。
    ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
    TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory,
    RejectedExecutionHandler handler)
複製程式碼

使用執行緒池你可以:

  1. 動態控制執行緒池尺寸
  2. 設定BlockingQueue 容積
  3. 定了拒絕策略
  4. 自定義 CustomThreadFactory 可以有一些附帶功能

9. 排程執行緒在固定的時間執行,或者在延遲一段時間之後,或者重複執行

  • ScheduledExecutorService 提供了一個方法用來排程執行一次性或者重複的任務。
ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);
複製程式碼
  • 在普通的執行緒池方法之外,ScheduledExecutorService新增了4個方法來排程任務,然後返回ScheduledFuture 物件。
在固定時間之後開始一個任務
  • 如下程式碼展示了在十分鐘之後執行一個任務:
ScheduledFuture<Integer> future = pool.schedule(new Callable<>() {
    @Override 
    public Integer call() {
    // do something
    return 42;
    }
},10, TimeUnit.MINUTES);
複製程式碼
以固定的頻率來執行任務
  • 如下程式碼展示了在十分鐘之後開始執行一個任務,然後每隔一分鐘開始重複任務:
ScheduledFuture<?> future = pool.scheduleAtFixedRate(new Runnable() {
    @Override 
    public void run() {
    // do something
    }
},10, 1, TimeUnit.MINUTES);
複製程式碼

任務會一直執行到執行緒池被關閉,future 被去小,或者某個任務發生了異常。


10. 執行緒池的使用

  • 執行緒池大多數情況下都是通過呼叫 ExcutorService 的方法建立
  • 如下方法可以用來提交任務
  1. submit: 執行提交的任務返回一個future 物件
  2. execute: 執行任務並不期望返回任何值
  3. invokeAll:執行一組任務,並且得到一個返回值列表
  4. invokeAny:執行所有任務,獲得其中一個正確執行的(沒有異常的),其餘沒有執行的任務或被取消。
  • 一旦你使用了 shutdown() 來終止執行緒池。這個操作會阻塞任務的提交。如果向等到所有任務都被執行,你可以用 awaitTermination 或isShutdown() 來包裹程式碼。

相關文章