高併發面試:執行緒池的七大引數?手寫一個執行緒池?

y浴血發表於2021-07-11

執行緒池

1. Callable介面的使用

package com.yuxue.juc.threadPool;

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

/**
 * 多執行緒中,第三種獲得多執行緒的方式
 * */
public class CallableTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //FutureTask(Callable<V> callable)
        FutureTask<Integer> futureTask = new FutureTask<>(new myThread());

        new Thread(futureTask, "AAA").start();
        //new Thread(futureTask, "BBB").start();//複用,直接取值,不要重啟兩個執行緒
        int a = 100;
        int b = 0;
        //b = futureTask.get();//要求獲得Callable執行緒的計算結果,如果沒有計算完成就要去強求,會導致堵塞,直到計算完成
        while (!futureTask.isDone()) {
            ////當futureTask完成後取值
            b = futureTask.get();
        }
        System.out.println("===Result is:" + (a + b));
    }
}

class myThread implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        System.out.println(Thread.currentThread().getName() + "\tget in the callable");
        Thread.sleep(5000);
        return 1024;
    }
}

兩者區別:

  • Callable:有返回值,拋異常
  • Runnable:無返回值,不丟擲異常

2. 為什麼要使用執行緒池

  1. 執行緒池做的工作主要是控制執行的執行緒的數量,處理過程中將任務放入佇列,然後線上程建立後啟動給這些任務,如果執行緒數量超過了最大數量,超出數量的執行緒排隊等候,等其他執行緒執行完畢,再從佇列中取出任務來執行

  2. 主要特點

    執行緒複用、控制最大併發數、管理執行緒

    • 減少建立和銷燬執行緒上所花的時間以及系統資源的開銷 => 減少記憶體開銷,建立執行緒佔用記憶體,建立執行緒需要時間,會延遲處理的請求;降低資源消耗,通過重複利用已建立的執行緒降低執行緒建立和銷燬造成的消耗
    • 提高響應速度。當任務到達時,任務可以不需要等到執行緒建立就能立即執行
    • 提高執行緒的客觀理性。執行緒是稀缺資源,如果無限制的建立,不僅會消耗系統資源,還會降低系統的穩定性,使用執行緒池可以進行統一的分配,調優和監控(根據系統承受能力,達到執行的最佳效果) => 避免無限建立執行緒引起的OutOfMemoryError【簡稱OOM】

3. 執行緒池如何使用?

  1. 架構說明

    Java中的執行緒池是通過Executor框架實現的,該框架中用到了 :Executor,Executors,ExecutorService,ThreadPoolExecutor

    image-20210711181248546

  2. 編碼實現

    實現有五種,Executors.newScheduledThreadPool()是帶時間排程的,java8新推出

    Executors.newWorkStealingPool(int),使用目前機器上可用的處理器作為他的並行級別

    重點有三種

    • Executors.newFixedThreadPool(int)

      執行長期的任務,效能好很多

      建立一個定長執行緒池,可控制執行緒最大併發數,超出的執行緒回在佇列中等待newFixedThreadPool建立的執行緒池corePoolSize和maximumPoolSize值是相等的,它使用的是 LinkedBlockingQueue

      底層原始碼:

      image-20210711181410391

    • Executors.newSingleThreadExecutor()

      一個任務一個任務執行的場景

      建立一個單執行緒話的執行緒池,他只會用唯一的工作執行緒來執行任務,保證所有任務按照指定順序執行

      newSingleThreadExecutor將corePoolSize和maximumPoolSize都設定為1,使用LinkedBlockingQueue

      原始碼:

      image-20210711181321383

    • Executors.newCachedThreadPool()

      執行很多短期非同步的小程式或負載較輕的伺服器

      建立一個可快取執行緒池,如果執行緒池長度超過處理需要,可靈活回收空閒縣城,若無可回收,則新建執行緒

      newCachedThreadPool將corePoolSize設定為0,將maximumPoolSize設定為Integer.MAX_VALUE,使用 的SynchronousQueue,也就是說來了任務就建立執行緒執行,當縣城空閒超過60s,就銷燬執行緒

      原始碼:

      image-20210711181456304

    我們可以看到底層的程式碼都是由ThreadPoolExecutor這個類的構造方法建立的,只是傳入的引數不同,那麼研究一下這個類以及這些引數就很有必要,下節我們將介紹這些引數的使用

  3. ThreadPoolExecutor

4. 執行緒池的幾個重要引數

核心執行程式碼為:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
  this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
       Executors.defaultThreadFactory(), defaultHandler);
}

那麼我們再點進this方法可以看到其全引數的構造方法為:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler) 

簡單介紹一下:

  1. corePoolSize: 執行緒池中常駐核心執行緒數

    • 在建立了執行緒池後,當有請求任務來之後,就會安排池中的執行緒去執行請求任務
    • 當執行緒池的執行緒數達到corePoolSize後,就會把到達的任務放到快取佇列當中
  2. maximumPoolSize: 執行緒池能夠容納同時執行的最大執行緒數,必須大於等於1

  3. keepAliveTime: 多餘的空閒執行緒的存活時間

    • 當前執行緒池數量超過corePoolSize時,檔口空閒時間達到keepAliveTime值時,多餘空閒執行緒會被銷燬到只剩下corePoolSize個執行緒為止
  4. unit: keepAliveTime的單位

  5. workQueue: 任務佇列,被提交但尚未被執行的任務

    任務佇列底層是BlockingQueue阻塞佇列!不清楚阻塞佇列的參考這篇文章:用阻塞佇列實現一個生產者消費者模型?

  6. threadFactory:表示生成執行緒池中工作執行緒的執行緒工廠,用於建立執行緒一般用預設的即可

  7. handler: 拒絕策略,表示當佇列滿了並且工作執行緒大於等於執行緒池的最大執行緒數(maximumPoolSize)時如 何來拒絕請求執行的runable的策略

5. 執行緒池的底層工作原理以及過程

image-20210711182159169

如上圖所屬,其流程為:

  1. 在建立了執行緒池之後,等待提交過來的任務請求

  2. 當呼叫execute()方法新增一個請求任務時,執行緒池會做出如下判斷:

    2.1 如果正在執行的執行緒數量小於corePoolSize,那麼馬上建立執行緒執行這個任務;

    2.2 如果正在執行的執行緒數量大於或等於corePoolSize,那麼將這個任務放入佇列;

    2.3 如果此時佇列滿了且執行的執行緒數小於maximumPoolSize,那麼還是要建立非核心執行緒立刻執行此任務;

    2.4 如果佇列滿了且正在執行的執行緒數量大於或等於maxmumPoolSize,那麼啟動飽和拒絕策略來執行

  3. 當一個執行緒完成任務時,他會從佇列中取出下一個任務來執行

  4. 當一個執行緒無事可做超過一定的時間(keepAliveTime)時,執行緒池會判斷:

    如果當前執行的執行緒數大於corePoolSize,那麼這個執行緒會被停掉;所以執行緒池的所有任務完成後他最大會收 縮到corePoolSize的大小

5. 實際工作中如何設定合理引數

5.1 執行緒池的拒絕策略

  1. 什麼是執行緒的拒絕策略
    1. 等待佇列也已經排滿了,再也塞不下新任務了,同時執行緒池中的max執行緒也達到了,無法繼續為新任務服務。這時我們就需要拒絕策略機制合理的處理這個問題
  2. JDK內建的拒絕策略
    • AbortPolicy(預設):如果滿了會直接丟擲RejectedExecutionException異常阻止系統正常執行
    • CallerRunsPolicy:”呼叫者執行“一種調節機制,該策略既不會拋棄任務,也不會丟擲異常,而是將某些任務回退到呼叫者, 從而降低新任務的流量
    • DiscardOldestPolicy:拋棄佇列中等待最久的任務,然後把當前任務加入佇列中嘗試再次提交當前任務
    • DiscardPolicy:直接丟棄任務,不予任何處理也不拋異常。如果允許任務丟失,這是最好的一種方案
  3. 均實現了RejectedExecutionHandler介面

5.2 你在工作中單一的/固定數的/可變的三種建立執行緒池的方法,用哪個多?

回答:一個都不用,我們生產上只能使用自定義的!!!!

為什麼?

執行緒池不允許使用Executors建立,試試通過ThreadPoolExecutor的方式,規避資源耗盡風險

阿里巴巴規範手冊當中提到:

  • FixedThreadPool和SingleThreadPool允許請求佇列長度為Integer.MAX_VALUE,可能會堆積大量請求,導致OOM;
  • CachedThreadPool和ScheduledThreadPool允許的建立執行緒數量為Integer.MAX_VALUE,可能會建立大量執行緒,導致OOM

5.3 你在工作中時如何使用執行緒池的,是否自定義過執行緒池?

上程式碼:

package com.yuxue.juc.threadPool;

import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class MyThreadPoolDemo {
    public static void main(String[] args) {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                //corePoolSize:常駐核心執行緒數
                2,
                //maximumPoolSize:最大的可容納執行緒數
                5,
                //存活時間設定為1s
                1L,
                TimeUnit.SECONDS,
                //這裡用LinkedBlockingQueue,且容量為3,意味著等候區最大容量三個任務
                new LinkedBlockingQueue<>(3),
                //預設的defaultThreadFactory即可
                Executors.defaultThreadFactory(),
                //丟棄方法使用AbortPolicy()
                new ThreadPoolExecutor.AbortPolicy());

        //這裡用來做任務的處理執行
        for (int i = 0; i < 5; i++) {
            threadPoolExecutor.execute(()->{
                System.out.println(Thread.currentThread().getName()+"\t 辦理業務;");
            });
        }
        threadPoolExecutor.shutdown();
    }
}

我們執行結果為:

pool-1-thread-2	 辦理業務;
pool-1-thread-1	 辦理業務;
pool-1-thread-2	 辦理業務;
pool-1-thread-1	 辦理業務;
pool-1-thread-2	 辦理業務;
//可以看到當我們任務書為5且處理速度非常快時,我們就用核心的corePoolSize就可以滿足任務需求

當任務數量變多或者任務變重時:如將我們的任務數量調整為20時,此時執行結果為:

pool-1-thread-1	 辦理業務;
pool-1-thread-3	 辦理業務;
pool-1-thread-2	 辦理業務;
pool-1-thread-3	 辦理業務;
pool-1-thread-1	 辦理業務;
pool-1-thread-4	 辦理業務;
pool-1-thread-1	 辦理業務;
pool-1-thread-3	 辦理業務;
pool-1-thread-2	 辦理業務;
pool-1-thread-5	 辦理業務;
pool-1-thread-4	 辦理業務;
pool-1-thread-1	 辦理業務;
pool-1-thread-3	 辦理業務;
pool-1-thread-1	 辦理業務;
pool-1-thread-2	 辦理業務;
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task com.yuxue.juc.threadPool.MyThreadPoolDemo$$Lambda$1/558638686@6d03e736 rejected from java.util.concurrent.ThreadPoolExecutor@568db2f2[Running, pool size = 5, active threads = 0, queued tasks = 0, completed tasks = 15]
	at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
	at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
	at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
	at com.yuxue.juc.threadPool.MyThreadPoolDemo.main(MyThreadPoolDemo.java:27)

發生了異常,且任務只執行完了15個,我們可以看到其中active threads = 0, queued tasks = 0也就是說我的阻塞佇列已經滿了,且沒有空閒的執行緒了,此時再申請任務我就會丟擲異常,這是執行緒池handler引數的拒絕策略,當我們更改策略為ThreadPoolExecutor.CallerRunsPolicy()時,執行結果當中存在main 辦理業務;語句,也就意味著執行緒池將某些任務回退到了呼叫者,另外的兩個拒絕策略在此就不演示

5.4 合理配置執行緒池你是如何考慮的?

  1. CPU密集型:

    • CPU密集的意思是該任務需要大量的運算,而沒有阻塞,CPU一直全速執行
    • CPU密集任務只有在真正多核CPU上才可能得到加速(通過多執行緒) 而在單核CPU上,無論你開幾個模擬的多執行緒該任務都不可能得到加速,因為CPU總的運算能力就那些
    • CPU密集型任務配置儘可能少的執行緒數量

    一般公式:CPU核數+1個執行緒的執行緒池

  2. IO密集型

    • 由於IO密集型任務執行緒並不是一直在執行任務,則應配置經可能多的執行緒,如CPU核數 * 2
    • IO密集型,即該任務需要大量的IO,即大量的阻塞。在單執行緒上執行IO密集型的任務會導致浪費大量的 CPU運算能力浪費在等待。
    • 所以在IO密集型任務中使用多執行緒可以大大的加速程式執行,即使在單核CPU上,這種加速主要就是利用 了被浪費掉的阻塞時間。
    • IO密集型時,大部分執行緒都阻塞,故需要多配置執行緒數:

    參考公式:CPU核數/(1-阻塞係數) 阻塞係數在0.8~0.9之間

例如八核CPU:,利用公式,約為:8/(1-0,9)=80個執行緒

相關文章