高並發麵試:線程池的七大參數?手寫一個線程池?

線程池

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個線程