前言
各行各業都有基本功,例如醫生,需要知道人體各個器官、各個系統的作用,知道細胞的作用、細菌和真菌的區別、病毒是怎么形成的,還得知道各種藥的作用,如何對癥下藥等。
在程序員世界里,也有著體現程序員基本功的東西,例如數據結構、算法、操作系統、網絡等,特別是當今互聯網,飛速發展下,迅速搞產品快速上線的時代已經不是主流,而且當下行業不景氣;大廠們招聘標準上對基本功也是越來越看重,基本功的修煉已經刻不容緩。
所以,我想通過一種訪談的形式來慢慢揭開程序員各種基本功的細節,分享給一起同行的你們!!
嘉賓介紹
今天我們很榮幸的邀請到了一位JAVA世界的老前輩,ThreadPoolExecutor,Java線程團隊Leader,被Doug Lea創造,2004年9月30日18:00PM,隨著J2SE1.5的發布后,被大眾熟知;在Java世界的15年里,他兢兢業業堅守著自己的崗位,在提高系統多核資源利用方面有著豐富的經驗。
采訪
作為線程團隊的Leader,您是否了解您的團隊成員?(ps:什么是線程?)
其實我的團隊成員都不是我招收進來的,他們都是由我的大BOSS(早期程序員開荒者)招進來;說到了解的話,這就得從我的大BOOS招來的操作系統老大說起;
操作系統指揮我們的程序干活主要靠以下幾種算法:
- 先來先服務和短作業(進程)優先調度算法
- 高優先權優先調度算法
- 優先權調度算法:非搶占式優先權算法和搶占式優先權算法
- 高響應比優先調度算法
- 基于時間片的輪轉調度算法:時間片輪轉法和多級反饋隊列調度算法
大部分操作系統(如windows、linux)的任務調度是采用時間片輪轉的搶占式調度方式,系統把所有就緒進程按先入先出的原則排成一個隊列。新來的進程加到就緒隊列末尾。每當執行進程調度時,進程調度程序總是選出就緒隊列的隊首進程,讓它在CPU上運行一個時間片的時間。在一個時間片結束時,發生時鐘中斷,調度程序據此暫停當前進程的執行,將其送到就緒隊列的末尾,并通過上下文切換執行當前的隊首進程,進程可以未使用完一個時間片,就讓出CPU
進程是什么?
進程是一個具有一定獨立功能的程序關于某個數據集合的一次運行活動。它是操作系統動態執行的基本單元,在傳統的操作系統中,進程既是基本的分配單元,也是基本的執行單元。進程由內存空間(代碼、數據、進程空間、打開的文件)和一個或多個線程組成。
介紹完上述的操作系統任務調度方式和進程的概念,我們再來聊聊我的團隊成員到底是什么樣的存在?
其實有進程就能滿足一個程序的正常執行了,那么為什么還需要我和我的團隊呢?其實是因為隨著計算機的快速發展,對CPU的要求越來越高,進程之間切換的開銷較大,已經無法滿足越來越復雜的程序的要求了。我的大BOSS為了能支撐公司的發展,就叫操作系統BOSS去解決這個問題,操作系統BOSS就不得不尋找一種新人才,于是線程就誕生了。線程是程序執行中一個單一的順序控制流程,是程序執行流的最小單元,是處理器調度和分派的基本單位。 有了線程也不代表代替了進程的地位,操作系統BOSS也比較聰明,讓進程來指揮線程,一個進程可以有一個或多個線程,各個線程之間共享程序的內存空間(也就是所在進程的內存空間)。
一個標準的線程由線程ID、當前指令指針(PC)、寄存器和堆棧組成
您日常的工作有哪些呢?(ps:線程池到底做了什么?)
簡單的說,我的工作就是來管理線程的,為啥需要管理呢?一般來說,我的團隊成員也就那么10幾個(ps:CPU密集型一般是N+1,I/O密集型一般是2N+1,其中N是CPU總核數),公司招一個人的成本也是很高的(ps:線程創建時間),而線程們也不會自己干活,他們沒事的時候就休息著,等到有任務過來時,我就找正在休息的人來干活。
- 線程的創建、銷毀是非常耗時的,可能比實際執行任務的運行時間還要長。
- 線程池就是對一定數量的線程保持其存活狀態,不讓其銷毀,有新的任務來了就直接調用空閑線程來執行,這樣就可以提升線程的運行效率。
能談談您具體是怎么管理您的團隊成員么?(如何管理調度線程執行)
我的管理方式主要是依靠以下幾個寶物:
- 狀態控制器ctl:用于管理線程池狀態和工作者線程個數
- 線程池大小配置corePoolSize和maximumPoolSize:決定了最大能運行多少線程
- 阻塞隊列workQueue:當無空閑線程時,暫存在阻塞隊列,等待poll出執行
狀態控制器ctl
怎么去判斷大家什么時候是休息狀態,什么時候是工作狀態呢;這就靠我的狀態控制器ctl,它包含兩部分:
- 線程池狀態runState:通過這個狀態我就能知道什么時候能分配任務,什么時候該下班休息
- 工作者線程個數:workCount
這里先介紹兩個常量和獲取這兩部分信息的方式:
// 將32位int分割為3和29,前三位用于存儲線程池狀態,后面的位數表示工作者線程個數 int COUNT_BITS = Integer.SIZE - 3; // 工作者線程個數,最大為2^29-1 int CAPACITY = (1 << COUNT_BITS) - 1; /** 1. runState:高三位來代表線程池狀態,runState 2. workCount:表示工作者個數 */ int runStateOf(int c) { return c & ~CAPACITY; } int workerCountOf(int c) { return c & CAPACITY; } int ctlOf(int rs, int wc) { return rs | wc; }
線程池狀態主要有以下幾種:
- RUNNING = -1 << COUNT_BITS:允許接入新的task或處理workQueue中的task
- SHUTDOWN = 0 << COUNT_BITS:不接受新的task,但是能處理workQueue中的task
- STOP = 1 << COUNT_BITS:不接受新task,也不處理workQueue中的task,同時還會interrupt當前運行的task
- TIDYING = 2 << COUNT_BITS:當狀態變為TIDYING將會調起terminated()方法:所有task都被中斷,workerCount=0;
- TERMINATED = 3 << COUNT_BITS:terminated()方法執行完成
其中TERMINATED > TIDYING > STOP > SHUTDOWN > RUNNING
狀態轉換過程為:
- RUNNING -> SHUTDOWN:shutdown()方法被調用時
- (RUNNING or SHUTDOWN) -> STOP:shutdownNow()方法被調用時
- SHUTDOWN -> TIDYING:當workQueue為空且線程池的運行線程為0時
- STOP -> TIDYING:當線程池的運行線程為0時
- TIDYING -> TERMINATED:當terminated()方法執行完成
如下圖:
線程調度
整個線程調度以來上述幾個法寶:線程池大小、阻塞隊列和狀態控制器,具體思路如下:
- 如果正在運行的線程數量小于 corePoolSize,那么馬上創建線程運行這個任務;
- 如果正在運行的線程數量大于或等于 corePoolSize,那么將這個任務放入隊列;
- 如果這時候隊列滿了,而且正在運行的線程數量小于 maximumPoolSize,那么還是要創建非核心線程立刻運行這個任務;
- 如果隊列滿了,而且正在運行的線程數量大于或等于 maximumPoolSize,那么線程池會交由RejectedExecutionHandler來處理
如下圖:
下面讓我們看看具體實現:
addWorker:創建一個新的Worker用于執行Runnable
Worker的run方法實際調用的是ThreadPoolExecutor#runWorker方法:如果工作者本身帶有task則執行,否則會從阻塞隊列workQueue中poll中一個task進行執行