Java提供的幾種執行緒池

天天不是小可愛發表於2019-07-25

執行緒池,顧名思義,放執行緒的池子嘛,這個池子可以存放多少執行緒取決於你自己採用什麼樣的執行緒池,你的硬體資源,以及併發執行緒的數量。JDK提供了下面的四種執行緒池:

固定執行緒數的執行緒池

  1.  最簡單的

在Java中建立一個執行緒池,這很簡單,只需要兩行程式碼。

ExecutorService executor = Executor.newFixedTreadPool(6);//固定執行緒是6
//執行緒一般設定成processor核心數的倍數,因為我這臺機器是6核的,所以設成6。這也是充分利用硬體嘛

//執行執行緒任務
executor.execute(new Runnable(){
@Override
public void run(){
     //do nothing 
}
})
    
executor.shutdown();

Executor是Java併發包中提供的,用來創造不同型別的執行緒池。

Attention

但是在多人合作或者是一些部署上線的專案裡,是不允許去使用這種方法的,因為它是有效能隱患的。

Executors在建立執行緒池的時候,用的是new LinkedBlockingQueue(),它這個佇列本身是無邊界的,但是執行緒是固定數量的。這就意味著,在程式執行的過程中,最多會有N個執行緒在處於活動狀態。每次有新的任務來就會等待,直到有執行緒處於空閒狀態。所有的執行緒都會處於執行緒池裡裡面,直到shutdown()的執行。

它的問題就在於來者不拒,只要有任務來,你就進佇列等著。在入佇列和出佇列用的並不是同一個lock,在多processor的機器上,是可以做到真正意義上的並行的。拿經典的生產者和消費者來舉例子,在同一個時間點,有的在消費,有的在生產。

這種執行緒池不會銷燬執行緒,不會拒絕任務,固定執行緒數。所以如果不停的加入任務,會導致很糟糕的記憶體佔用,老年代可能會被佔滿。

  1.  稍複雜的(可以延時執行,也可以執行帶返回值的任務)
public static void main(String[] args) throws InterruptedException, ExecutionException {
        TestThread testThread = new TestThread();
        System.out.println(testThread.processors);

        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(6);
        FutureTask<String> futureTask = new FutureTask<>(new Callable<String>() {
            @Override
            public String call() throws Exception {
                return Thread.currentThread().getName();
            }
        });
        scheduledExecutorService.submit(futureTask);

        //獲取返回值
        String result = futureTask.get();
        System.out.println("result :"+result);

        //執行延時任務
        scheduledExecutorService.schedule(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+": bomb!");
            }
        },3L,TimeUnit.SECONDS);
    }

Output:

result :pool-1-thread-1
pool-1-thread-1: bomb!

快取的執行緒池

核心池大小為0,執行緒池最大執行緒數目為最大整型,這意味著所有的任務一提交就會wait。當執行緒池中的執行緒有60s沒有執行任務就會被Kill,阻塞佇列為SynchronousQueue。SynchronousQueue的take操作需要put操作等待,put操作需要take操作等待,否則會阻塞(執行緒池的阻塞佇列不能儲存,所以當目前執行緒處理忙碌狀態時,會開闢新的執行緒來處理請求**),執行緒進入wait set。

總結一下這是一個可以無限擴大的執行緒池;適合處理執行時間比較小的任務;執行緒空閒時間超過60s就會被Kill,所以長時間處於空閒狀態的時候,這種執行緒池幾乎不佔用資源,因為它壓根沒有執行緒在裡面;阻塞佇列沒有儲存空間,只要請求到來,就必須找到一條空閒執行緒去處理這個請求,找不到則線上程池新開闢一條執行緒。

如果主執行緒提交任務的速度遠遠大於CachedThreadPool的處理速度,則CachedThreadPool會不斷地建立新執行緒來執行任務,這樣有可能會導致系統耗盡CPU和記憶體資源,所以在使用該執行緒池時,要注意控制併發的任務數。如果是一個不斷增長的任務需求,很容易就會到效能瓶頸,它會不停的建立新的執行緒。

 ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        for (int i = 0; i < 10; i++) {
            cachedThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                }
            });
        }
        cachedThreadPool.shutdown();

Output:

pool-1-thread-2
pool-1-thread-6
pool-1-thread-1
pool-1-thread-7
pool-1-thread-8
pool-1-thread-3
pool-1-thread-5
pool-1-thread-9
pool-1-thread-4
pool-1-thread-10

單個執行緒的執行緒池

SingleThreadExecutor 是使用單個worker執行緒的Executor。只有一種情況會有新的執行緒加入執行緒池,那就是原有的執行緒執行時有丟擲異常,這時就會有建立的新的執行緒來替代它的工作。

拿生產者消費者模型來說的話,這就是一個單一消費者的模型

(ps.一般可以用來做一些日誌記錄

 public static void main(String[] args) {
        // 永遠是一條執行緒
        ExecutorService singleThreadPool = Executors.newSingleThreadExecutor();
        for (int i = 0; i < 10; i++) {
            final int j = i;
            singleThreadPool.execute(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName() + ":" + j);
                }
            });
        }
        singleThreadPool.shutdown();
        
    }

Output:

pool-1-thread-1:0
pool-1-thread-1:1
pool-1-thread-1:2
pool-1-thread-1:3
pool-1-thread-1:4
pool-1-thread-1:5
pool-1-thread-1:6
pool-1-thread-1:7
pool-1-thread-1:8
pool-1-thread-1:9

相關文章