1. 程式人生 > >簡述Java執行緒池以及使用

簡述Java執行緒池以及使用

建立執行緒的幾種方式

  1. 繼承Thread,重寫run()方法
  2. 實現Runnable介面,實現run()方法
  3. 實現Callable介面,實現Call()方法
  4. 使用執行緒池,並向其提交任務task,其內部自動建立執行緒排程完成。

上述對比:

一般來說,使用第一種和第二種比較普遍,考慮到Java不支援多繼承,通常使用第二種,實現Runnable介面建立執行緒。
然而,假設建立執行緒進行一些複雜的處理,比如需要執行後做出反饋,這時候可以使用實現Callable方式建立執行緒。
About Callable的返回值,可以使用Future或者FutureTask來獲取。

Future

它是一個介面,包含方法:
boolean cancel();
嘗試取消任務,返回ture包括(任務已完成,任務成功取消)
boolean isCancelled();
區別於上一種方法這個更像是一個未完成狀態(完成任務之前的取消),不包括已完成
boolean isDone();
判斷任務是否完成
get();
獲取執行結果,如果任務在執行該方法會阻塞,直到有返回值
get(long timeout,TimeUnitunit);
在有限時間內阻塞得到返回值,否則返回null

FutureTask:

底部實現RunnableFuture介面,而RunnableFuture

繼承了FutureRunnable
所以FutureTask同樣具備上述5個方法。
參考部分原始碼:

/**
     * Creates a {@code FutureTask} that will, upon running, execute the
     * given {@code Callable}.
     *
     * @param  callable the callable task
     * @throws NullPointerException if the callable is null
     */
    public FutureTask
(Callable<V> callable) { if (callable == null) throw new NullPointerException(); this.callable = callable; this.state = NEW; // ensure visibility of callable } /** * Creates a {@code FutureTask} that will, upon running, execute the * given {@code Runnable}, and arrange that {@code get} will return the * given result on successful completion. * * @param runnable the runnable task * @param result the result to return on successful completion. If * you don't need a particular result, consider using * constructions of the form: * {@code Future<?> f = new FutureTask<Void>(runnable, null)} * @throws NullPointerException if the runnable is null */ public FutureTask(Runnable runnable, V result) { this.callable = Executors.callable(runnable, result); this.state = NEW; // ensure visibility of callable }

可以看到FutureTask可以傳入引數Callable或者Runnable,Result作為構造方法,區別在於:
1. 前者呼叫get()方法獲取的是Callablecall()返回值
2. 後者傳入引數RunnableResult,如果執行成功,返回Result

Futuretask和Callable實現建立執行緒的Demo

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;

public class CallableThread {
    public static void main(String[] args) {
        ThreadCall tc = new ThreadCall();
        FutureTask<String> result = new FutureTask<>(tc);
        Thread t = new Thread(result);
        t.start();
        while (true) {
            if (result.isDone()) {
                try {
                    System.out.println("Call執行結束:"+result.get());
                    break;
                } catch (InterruptedException | ExecutionException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

class ThreadCall implements Callable<String>{

    @Override
    public String call() throws Exception {
        return "為Java打Call!";
    }

}

執行結果:
Call執行結束:為Java打Call!

執行緒池:

執行緒池可以避免頻繁建立銷燬執行緒,提高效能。常見如資料庫的連線池。
JUC(java.util.concurrent)中,Java提供了一個工具類Executors用於建立執行緒池[多種型別],返回一個執行器,再將Runnable或者Callable提交到執行器中[詳見demo]。

執行緒池的幾個型別:
  • newCachedThreadPool(int nThreads)
    接受任務才建立執行緒,如果某個執行緒空閒超過60秒,則會被撤銷。
  • newFixedThreadPool()
    固定執行緒數量的執行緒池,如果執行緒池數量超過nThreads,新任務會放在任務佇列中,如果某個執行緒終止了,另一個執行緒會來取代它。
  • newSingleThreadExecutor()
    只有一個執行緒的執行緒池,依次執行任務
  • newScheduledThreadPool(int corePoolSize)
    指定按照設定的時間週期執行任務,池中執行緒數量根據引數corePoolSize決定,即時有執行緒空閒狀態也不會撤銷。

Demo:

package com.dd.code.ThreadByCallable;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TestThreadPool {

    public static void main(String[] args) {
        //建立執行緒池(5個執行緒)
        ExecutorService pool = Executors.newFixedThreadPool(5);
        ThreadPoolDemo tpd = new ThreadPoolDemo();

        //為執行緒池分配10個任務
        for (int i = 0; i < 10; i++) {
            pool.submit(tpd);
        }
        //關閉執行緒池(等待任務執行完成才關閉),區別於shutdownnow立即關閉
        pool.shutdown();
    }

}

class ThreadPoolDemo implements Runnable{

    private int i = 0;

    @Override
    public void run() {
        System.out.println("當前執行緒號:"+Thread.currentThread().getName() + ",i = " + i++);
    }

}

執行結果:

當前執行緒號:pool-1-thread-3,i = 2
當前執行緒號:pool-1-thread-5,i = 3
當前執行緒號:pool-1-thread-1,i = 0
當前執行緒號:pool-1-thread-2,i = 1
當前執行緒號:pool-1-thread-3,i = 6
當前執行緒號:pool-1-thread-5,i = 5
當前執行緒號:pool-1-thread-4,i = 4
當前執行緒號:pool-1-thread-3,i = 8
當前執行緒號:pool-1-thread-2,i = 7
當前執行緒號:pool-1-thread-1,i = 9

可以看到,儘管分配10個任務給執行緒池,然而我們初始化5個固定執行緒的執行緒池,所以執行緒池執行的所在程序號不會超過5,而且可以看出在newFixedThreadPool中執行緒的排程是隨機的。

執行緒池多工計算

假如我們要使用執行緒池來執行相應計算,並且取得返回值要如何實現呢?
通常可以使用Future或者FutureTask以結合Callable來實現。

思路:

  1. 建立所需執行緒池,返回執行器[ExecutorService]executor
  2. 使用FutureTask構造方法傳入需要計算的Callable例項化建立FutureTask
  3. 呼叫執行器executor的方法executor.execute(futuretask);
  4. 建立List<FutureTask>將執行的futuretask新增到List中,方便後續取值。

示例:

下面模擬一個計算延遲操作,我們來看看多執行緒和單執行緒執行的效率對比

package com.dd.code.ThreadByCallable;

import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.FutureTask;
import java.util.concurrent.RejectedExecutionException;

public class MutilThreadTask {
    // 執行緒(內部類)實現Callable介面
    static class MyCallable implements Callable<Long> {
        private long uid;

        public MyCallable(long uid) {
            super();
            this.uid = uid;
        }

        @Override
        public Long call() throws Exception {
            return Work(uid);
        }

        public long Work(long uid) {
            try {
                //模擬查詢操作延遲
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return uid * 2;
        }

    }

    public static void main(String[] args) {
        // 多執行緒累加list
        List<Long> uidList = new ArrayList<Long>();
        for(int i = 0; i < 100; i++){
            uidList.add(10L);
        }

        long t1  = System.currentTimeMillis();
        //使用單執行緒進行計算
        System.out.println("累加結果:" + SumWithOneThread(uidList));
        System.out.println("常規計算時間:"+(System.currentTimeMillis()-t1));

        long t2  = System.currentTimeMillis();
        //建立10個執行緒的執行緒池參與運算
        System.out.println("累加結果:" + SumWithMutilThread(uidList,10));
        System.out.println("執行緒池計算時間:"+(System.currentTimeMillis()-t2));
    }

    static Long SumWithOneThread(List<Long> uidList){
        Long result = 0L;
        for (Long l : uidList) {            
            result += DoubleIt(l);
        }
        return result;
    }

    static Long DoubleIt(Long l){
        try {
            //模擬延遲計算
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return l*2;
    }

    static Long SumWithMutilThread(List<Long> uidList,int THREAD_NUM) {
        // 建立執行緒池
        ExecutorService executor = Executors.newFixedThreadPool(THREAD_NUM);
        // 關於Future和Futuretask可以參考http://blog.csdn.net/zmx729618/article/details/51596414
        // 建立任務列表
        List<FutureTask<Long>> ftlist = new ArrayList<FutureTask<Long>>();
        // MutilThreadTask mt = new MutilThreadTask();
        // 逐個執行任務並且新增到任務列表
        for (Long uid : uidList) {
            /*
             * 例項化內部類MyCallable方法
             * 1.把內部類設為靜態. 
             * 2.通過外部類.例項化內部類,MyCallable callable = mt.new MyCallable(uid);
             */
            MyCallable callable = new MyCallable(uid);
            FutureTask<Long> futuretask = null;
            try {
                futuretask = new FutureTask<Long>(callable);
                executor.execute(futuretask);
            } catch (RejectedExecutionException e) {
                //Re:處理進入執行緒池失敗的情況,也就是說,不僅獲取執行緒資源失敗,並且由於等待佇列已滿,甚至無法進入佇列直接 失敗
                e.printStackTrace();
            }
            ftlist.add(futuretask);
        }
        // 遍歷任務列表,把完成任務取出來獲取資料
        long totalResult = 0L; // 初始化計算資料量
        for (FutureTask<Long> ft : ftlist) {
            Long result = null;
            try {
                result = ft.get();
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
            if (result != null) {
                totalResult += result;
            }
        }
        // 關閉執行器
        executor.shutdown();
        return totalResult;
    }

}

執行結果:

累加結果:2000
常規計算時間:10035
累加結果:2000
執行緒池計算時間:1020

效率對比

果然,使用執行緒池建立多個執行緒計算效率快了許多,但是使用多執行緒就一定快嗎?
我們稍作修改一下方法的引數,只給FutureTask分配5個任務[區別於之前的100個任務],並且將模擬計算時間修改為1毫秒[區別於之前的100毫秒],再看看執行結果。

累加結果:100
常規計算時間:6
累加結果:100
執行緒池計算時間:24

發現使用執行緒池的多執行緒計算反而更慢了,可以初步斷定,如果任務量不是很大,計算量不是很複雜的情況下,多執行緒並不是首先,相反執行緒池還需要耗費資源去維護池中的執行緒,所以考慮多執行緒的使用場景,還要權衡一下任務量。