面試官不按套路,竟然問我Java線程池是怎麼統計線程空閑時間?

来源:https://www.cnblogs.com/yidengjiagou/archive/2022/11/18/16902327.html
-Advertisement-
Play Games

面試官: 小伙子,我看你簡歷上寫的項目中用到了線程池,你知道線程池是怎樣實現復用線程的? 這面試官是不是想坑我?是不是擺明瞭不讓我通過? 難道你不應該問線程池有哪些核心參數?每個參數具體作用是什麼? ...


背景介紹:

你剛從學校畢業後,到新公司實習,試用期又被畢業,然後你又不得不出來面試,好在面試的時候碰到個美女面試官!

image

面試官: 小伙子,我看你簡歷上寫的項目中用到了線程池,你知道線程池是怎樣實現復用線程的?

這面試官是不是想坑我?是不是擺明瞭不讓我通過?

難道你不應該問線程池有哪些核心參數?每個參數具體作用是什麼?

往線程池中不斷提交任務,線程池的處理流程是什麼?

這些才是你應該問的,這些八股文我已經背熟了,你不問,瞎問什麼復用線程?

幸虧我看了一燈的八股文,聽我給你背一遍!

我: 線程池復用線程的邏輯很簡單,就是線上程啟動後,通過while死迴圈,不斷從阻塞隊列中拉取任務,從而達到了復用線程的目的。

具體源碼如下:

// 線程執行入口
public void run() {
    runWorker(this);
}

// 線程運行核心方法
final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock();
    boolean completedAbruptly = true;
    try {
        // 1. 使用while死迴圈,不斷從阻塞隊列中拉取任務
        while (task != null || (task = getTask()) != null) {
            // 加鎖,保證thread不被其他線程中斷(除非線程池被中斷)
            w.lock();
            // 2. 校驗線程池狀態,是否需要中斷當前線程
            if ((runStateAtLeast(ctl.get(), STOP) ||
                    (Thread.interrupted() &&
                            runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                wt.interrupt();
            try {
                beforeExecute(wt, task);
                Throwable thrown = null;
                try {
                    // 3. 執行run方法
                    task.run();
                } catch (RuntimeException x) {
                    thrown = x;
                    throw x;
                } catch (Error x) {
                    thrown = x;
                    throw x;
                } catch (Throwable x) {
                    thrown = x;
                    throw new Error(x);
                } finally {
                    afterExecute(task, thrown);
                }
            } finally {
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly);
    }
}

runWorker方法邏輯很簡單,就是不斷從阻塞隊列中拉取任務並執行。

面試官: 小伙子,有點東西。我們都知道線程池會回收超過空閑時間的線程,那麼線程池是怎麼統計線程的空閑時間的?

image

美女面試官的問題真刁鑽,讓人頭疼啊!這問的也太深了吧!

沒看過源碼的話,真不好回答。

我: 嗯...,可能是有個監控線程在後臺不停的統計每個線程的空閑時間,看到線程的空閑時間超過閾值的時候,就回收掉。

面試官: 小伙子,你的想法挺不錯,邏輯很嚴謹,你確定線程池內部是這麼實現的嗎?

問得我有點不自信了,沒看過源碼不能瞎蒙。

我還是去瞅一眼一燈寫的八股文吧。

我: 這個我知道,線程池統計線程的空閑時間的實現邏輯很簡單。

阻塞隊列(BlockingQueue)提供了一個poll(time, unit)方法用來拉取數據,
作用就是: 當隊列為空時,會阻塞指定時間,然後返回null。

線程池就是就是利用阻塞隊列的這個方法,如果在指定時間內拉取不到任務,就表示該線程的存活時間已經超過閾值了,就要被回收了。

具體源碼如下:

// 從阻塞隊列中拉取任務
private Runnable getTask() {
    boolean timedOut = false;
    for (; ; ) {
        int c = ctl.get();
        int rs = runStateOf(c);
        // 1. 如果線程池已經停了,或者阻塞隊列是空,就回收當前線程
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }
        int wc = workerCountOf(c);
        // 2. 再次判斷是否需要回收線程
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
        if ((wc > maximumPoolSize || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }
        try {
            // 3. 在指定時間內,從阻塞隊列中拉取任務
            Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
            if (r != null)
                return r;
          	// 4. 如果沒有拉取到任務,就標識該線程已超時,然後就被回收
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

面試官: 小伙子,可以啊,你是懂線程池源碼的。再問你個問題,如果線程池拋異常了,也沒有try/catch,會發生什麼?

image

美女面試官你這是準備打破砂鍋問到底,鐵了心不讓我過,是吧?

我的代碼風格是很嚴謹的,誰寫的業務代碼不try/catch,也沒遇到過這種情況。

讓我再看一下一燈總結的八股文吧。

我: 有了,線程池中的代碼如果拋異常了,也沒有try/catch,會從線程池中刪除這個異常線程,並創建一個新線程。

不信的話,我們可以測試驗證一下:

/**
 * @author 一燈架構
 * @apiNote 線程池示例
 **/
public class ThreadPoolDemo {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        // 1. 創建一個單個線程的線程池
        ExecutorService executorService = Executors.newSingleThreadExecutor();

        // 2. 往線程池中提交3個任務
        for (int i = 0; i < 3; i++) {
            executorService.execute(() -> {
                System.out.println(Thread.currentThread().getName() + " 關註公眾號:一燈架構");
                throw new RuntimeException("拋異常了!");
            });
        }

        // 3. 關閉線程池
        executorService.shutdown();
    }

}

輸出結果:

pool-1-thread-1 關註公眾號:一燈架構
pool-1-thread-2 關註公眾號:一燈架構
pool-1-thread-3 關註公眾號:一燈架構
Exception in thread "pool-1-thread-1" java.lang.RuntimeException: 拋異常了!
	at com.yideng.SynchronousQueueDemo.lambda$main$0(ThreadPoolDemo.java:21)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
Exception in thread "pool-1-thread-2" java.lang.RuntimeException: 拋異常了!
	at com.yideng.SynchronousQueueDemo.lambda$main$0(ThreadPoolDemo.java:21)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)
Exception in thread "pool-1-thread-3" java.lang.RuntimeException: 拋異常了!
	at com.yideng.SynchronousQueueDemo.lambda$main$0(ThreadPoolDemo.java:21)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)

從輸出結果中可以看出,線程名稱並不是同一個,而是累加的,說明原線程已經被回收,新建了個線程。

我們再看一下源碼,驗證一下:

// 線程拋異常後,退出邏輯
private void processWorkerExit(ThreadPoolExecutor.Worker w, boolean completedAbruptly) {
    if (completedAbruptly)
        decrementWorkerCount();

    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        completedTaskCount += w.completedTasks;
        // 1. 從工作線程中刪除當前線程
        workers.remove(w);
    } finally {
        mainLock.unlock();
    }

    // 2. 中斷當前線程
    tryTerminate();

    int c = ctl.get();
    if (runStateLessThan(c, STOP)) {
        if (!completedAbruptly) {
            int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
            if (min == 0 && !workQueue.isEmpty())
                min = 1;
            if (workerCountOf(c) >= min)
                return; // replacement not needed
        }
        // 3. 新建一個線程
        addWorker(null, false);
    }
}

如果想統一處理異常,可以自定義線程創建工廠,在工廠裡面設置異常處理邏輯。

/**
 * @author 一燈架構
 * @apiNote 線程池示例
 **/
public class ThreadPoolDemo {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        // 1. 創建一個單個線程的線程池
        ExecutorService executorService = Executors.newSingleThreadExecutor(runnable -> {
            // 2. 自定義線程創建工廠,並設置異常處理邏輯
            Thread thread = new Thread(runnable);
            thread.setUncaughtExceptionHandler((t, e) -> {
                System.out.println("捕獲到異常:" + e.getMessage());
            });
            return thread;
        });

        // 3. 往線程池中提交3個任務
        for (int i = 0; i < 3; i++) {
            executorService.execute(() -> {
                System.out.println(Thread.currentThread().getName() + " 關註公眾號:一燈架構");
                throw new RuntimeException("拋異常了!");
            });
        }

        // 4. 關閉線程池
        executorService.shutdown();
    }

}

輸出結果:

Thread-0 關註公眾號:一燈架構
捕獲到異常:拋異常了!
Thread-1 關註公眾號:一燈架構
捕獲到異常:拋異常了!
Thread-2 關註公眾號:一燈架構
捕獲到異常:拋異常了!

面試官: 小伙子,論源碼,還是得看你,還是你背的熟。現在我就給你發offer,薪資直接漲10%,明天9點就來上班吧,咱們公司實行996工作制。

image

我是「一燈架構」,如果本文對你有幫助,歡迎各位小伙伴點贊、評論和關註,感謝各位老鐵,我們下期見

image


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1./*(註釋內容,不做編譯)*/ 2.//(註釋內容) 3.“#”不能忘; 4.scanf中的取地址符不能忘帶; 5.如在scanf中輸入轉義符號外的文字或其他,則編譯出的控制台輸入時必須把輸入的其他原樣再輸入一遍;(否則如圖二) 6.printf中別慣性加了取地址符; 7.分數表示時需特別註意( ...
  • 基於 SpringWeb(5.3.23)的介面請求分析 前情提要 假定當前 Web 項目中有如下實體類和介面: package com.example.entity; public class WebUser { private String name; private Integer age; p ...
  • 創建第一個springmvc程式 1、創建父項目文件,導入依賴,刪除src文件夾 pom.xml文件 <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</ ...
  • 算術運算符 +(加) -(減) *(乘) /(除) %(取餘) ++(自增) --(自減) 註意:/(除):兩個整數相除,其結果一定是整數,小數位電腦自動略去 例: int num1 = 15; int num2 = 4; 1. int result = num1/num2; system.out ...
  • 目錄 一.簡介 1.freeglut 2.glew 3.glut 4.glfw 5.glad 二.分類 1.視窗管理 2.函數載入 三.組合使用 1.freeglut + glew 2.glfw + glew 3.glfw + glad 四.猜你喜歡 零基礎 OpenGL ES 學習路線推薦 : O ...
  • 繼承: 強調類與類之間的關係 組合: 強調對象和對象之間的關係 清楚python支持多繼承,從而涉及到一些MRO的點,這裡不做贅述,在實際工作過程中,我們經常會使用繼承來實現代碼復用,如果僅僅是為了復用,還是比較推薦使用組合方式,因為繼承方式,使得類與類之間的耦合性變得異常緊密,這多少違背了迪米特法 ...
  • 故事背景 最近同事遇到一個比較奇怪的問題,直接開門見山吧。在動態庫中調用靜態庫直接報錯了recompile with -fPIC,查看cmake的寫法也沒有問題,而且也是第一次遇見這個問題,所以就開啟了我的好奇之路。 探索之路 說實話我不喜歡百度,因為千篇一律,你抄我的我抄你的,沒有任何參考價值,直 ...
  • 一、序言 在日常一線開發過程中,總有列表轉樹的需求,幾乎是項目的標配,比方說做多級菜單、多級目錄、多級分類等,有沒有一種通用且跨項目的解決方式呢?幫助廣大技術朋友給業務瘦身,提高開發效率。 本文將基於Java8的Lambda 表達式和Stream等知識,使用TreeUtils工具類實現一行代碼完成列 ...
一周排行
    -Advertisement-
    Play Games
  • Timer是什麼 Timer 是一種用於創建定期粒度行為的機制。 與標準的 .NET System.Threading.Timer 類相似,Orleans 的 Timer 允許在一段時間後執行特定的操作,或者在特定的時間間隔內重覆執行操作。 它在分散式系統中具有重要作用,特別是在處理需要周期性執行的 ...
  • 前言 相信很多做WPF開發的小伙伴都遇到過表格類的需求,雖然現有的Grid控制項也能實現,但是使用起來的體驗感並不好,比如要實現一個Excel中的表格效果,估計你能想到的第一個方法就是套Border控制項,用這種方法你需要控制每個Border的邊框,並且在一堆Bordr中找到Grid.Row,Grid. ...
  • .NET C#程式啟動閃退,目錄導致的問題 這是第2次踩這個坑了,很小的編程細節,容易忽略,所以寫個博客,分享給大家。 1.第一次坑:是windows 系統把程式運行成服務,找不到配置文件,原因是以服務運行它的工作目錄是在C:\Windows\System32 2.本次坑:WPF桌面程式通過註冊表設 ...
  • 在分散式系統中,數據的持久化是至關重要的一環。 Orleans 7 引入了強大的持久化功能,使得在分散式環境下管理數據變得更加輕鬆和可靠。 本文將介紹什麼是 Orleans 7 的持久化,如何設置它以及相應的代碼示例。 什麼是 Orleans 7 的持久化? Orleans 7 的持久化是指將 Or ...
  • 前言 .NET Feature Management 是一個用於管理應用程式功能的庫,它可以幫助開發人員在應用程式中輕鬆地添加、移除和管理功能。使用 Feature Management,開發人員可以根據不同用戶、環境或其他條件來動態地控制應用程式中的功能。這使得開發人員可以更靈活地管理應用程式的功 ...
  • 在 WPF 應用程式中,拖放操作是實現用戶交互的重要組成部分。通過拖放操作,用戶可以輕鬆地將數據從一個位置移動到另一個位置,或者將控制項從一個容器移動到另一個容器。然而,WPF 中預設的拖放操作可能並不是那麼好用。為瞭解決這個問題,我們可以自定義一個 Panel 來實現更簡單的拖拽操作。 自定義 Pa ...
  • 在實際使用中,由於涉及到不同編程語言之間互相調用,導致C++ 中的OpenCV與C#中的OpenCvSharp 圖像數據在不同編程語言之間難以有效傳遞。在本文中我們將結合OpenCvSharp源碼實現原理,探究兩種數據之間的通信方式。 ...
  • 一、前言 這是一篇搭建許可權管理系統的系列文章。 隨著網路的發展,信息安全對應任何企業來說都越發的重要,而本系列文章將和大家一起一步一步搭建一個全新的許可權管理系統。 說明:由於搭建一個全新的項目過於繁瑣,所有作者將挑選核心代碼和核心思路進行分享。 二、技術選擇 三、開始設計 1、自主搭建vue前端和. ...
  • Csharper中的表達式樹 這節課來瞭解一下表示式樹是什麼? 在C#中,表達式樹是一種數據結構,它可以表示一些代碼塊,如Lambda表達式或查詢表達式。表達式樹使你能夠查看和操作數據,就像你可以查看和操作代碼一樣。它們通常用於創建動態查詢和解析表達式。 一、認識表達式樹 為什麼要這樣說?它和委托有 ...
  • 在使用Django等框架來操作MySQL時,實際上底層還是通過Python來操作的,首先需要安裝一個驅動程式,在Python3中,驅動程式有多種選擇,比如有pymysql以及mysqlclient等。使用pip命令安裝mysqlclient失敗應如何解決? 安裝的python版本說明 機器同時安裝了 ...