前幾天小強去阿里巴巴面試Java崗,止步於二面。java
他和我訴苦本身被虐的多慘多慘,特別是深挖線程和線程池的時候,竟然被問到不知道如何做答。程序員
對於他的遭遇,結合他過了一面的那個嘚瑟樣,我深表同情(加大力度)!面試
好了,不開玩笑了,在和小強的面試題中,我選取了幾個比較典型的線程和線程池的問題。數組
Java中的線程和操做系統的線程有什麼關係?多線程
調用start方法是如何執行run方法的?併發
線程池提交任務有哪幾種方式?分別有什麼區別?操作系統
談談你對阻塞隊列的理解。線程
常見的線程池有哪些?爲何阿里不容許使用 Executors 去建立線程池?3d
線程池任務調度的流程大體講一下。code
線程池裏面的線程執行異常了會怎麼樣?
核心線程和非核心線程是如何區分的?
想要答對這些問題,並非很難,可是想要答好,我以爲是很是考驗我的功底的。
爲了弄清這些問題,我連夜加急,採訪了「線程」,下面是線程的自述。
我是一個線程,一個底層的打工人。
總有人把我和進程搞混,但其實我和進程的區別很大。
進程是程序的一次執行,CPU的資源都是分發給進程而不是分發給咱們線程,進程是資源分配的最小單位,一個進程能夠包含不少向我這樣的線程。
咱們線程是CPU調度執行的最小單位,真正的打工人。
在Java裏面,個人名字叫作java.lang.Thread。
須要注意的是,調用run方法和執行一個普通方法沒有區別。想要真正的建立一個線程並啓動,須要調用個人start方法。
有一點我必須告訴你,就是我也是有小弟的。
在JVM裏面,我有一個JavaThread的小弟,他幫我聯繫操做系統的osthread線程。
調用個人start方法以後,具體的執行流程是這樣的:
固然了,這個過程省略了不少細節,不過很明確的是,我和內核線程是一一對應的。
調度我就至關於調度內核線程,而調度內核線程須要在用戶態和內核態之間切換,這個過程開銷是很是大的。
因此,建立我成本是很高的,必定要慎重。
和大家人類同樣,我也有着精彩的一輩子,也會經歷出生(建立)、奮鬥(Running)、死亡(銷燬)等過程,今天我主要和你講述的是我打工奮鬥的生活。
原來我是打零工的,有人須要個人時候就建立一個我,等我完成工做就把我銷燬。
上面也提到過,我和內核線程是一對一的,建立和銷燬的過程是很是消耗資源的,因此這樣的成本很是高。
因而,有人就想了一個辦法,開了一個公司,也就是大家說的線程池。
線程池公司統一管理調度咱們線程。咱們在線程池裏面重復着等待工做——完成工做
的步驟。
這樣我就能夠日復一日年復一年的重複打工了,這種提供了減小對象數量從而改善應用所需的對象結構的方式的模式,被大家人類叫作「享元模式」。
線程池公司有不少種,但都離不開這幾個主要指標:
線程池中的workQueue是一個阻塞隊列,用於存放線程池未能及時處理執行的任務。
它的存在既解耦了任務的提交與執行,又能起到一個緩衝的做用。
阻塞隊列有不少,下面我帶你瞭解一下常見的阻塞隊列。
基於數組實現的有界阻塞隊列,建立的時候須要指定容量。此類型的隊列按照FIFO(先進先出)的規則對元素進行排序。
基於鏈表實現阻塞隊列,默認大小爲Integer.MAX_VALUE。按照FIFO(先進先出)的規則對元素進行排序
一個不存儲元素的阻塞隊列。每個put操做必須阻塞等待其餘線程的take操做,take操做也必須等待其餘線程的put操做。
一個基於數組利用堆結構實現優先級效果的無界隊列,默認天然序排序,也能夠本身實現compareTo方法自定義排序規則。
一個實現了優先級隊列功能且實現了延遲獲取的無界隊列,在建立元素時,能夠指定多久多久才能在隊列中獲取當前元素。只有延時期滿了後才能從隊列中獲取元素。
當任務隊列滿了以後,若是還有任務提交過來,會觸發拒絕策略,常見的拒絕策略有:
AbortPolicy:丟棄任務並拋出異常,默認該方式。
CallerRunsPolicy:由調用線程本身處理該任務。誰調用,誰處理。
DiscardPolicy:丟棄任務,可是不拋出異常。
DiscardOldestPolicy:拋棄任務隊列中最舊的任務也就是最早加入隊列的,再把這個新任務添加進去。先從任務隊列中彈出最早加入的任務,空出一個位置,而後再次執行execute方法把任務加入隊列。
固然,除了以上這幾種拒絕策略,你也能夠根據實際的業務場景和業務需求去自定義拒絕策略,只須要實現RejectedExecutionHander接口,自定義裏面的rejectedExecution方法。
咱們每一個線程會被包裝成Worker,線程池裏面有一個HashSet存放Worker。
當有任務提交過來以後:
總結而言,就是核心線程能幹的事情儘可能不去建立非核心線程,這是線程池很關鍵的一點。
new ThreadPoolExecutor(4, 8, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<Runnable>(4));
以這個線程池爲例,下面是他的任務提交和執行流程:
我有過四段工做經歷,每段經歷都有着精彩的故事。
SingleThreadExecutor是我加入的第一家線程池,這是一家創業公司,整個線程池就只有我一個線程。
全部的任務都由我幹,並且任務隊列是一個無界隊列。就是說,打工的線程只有我一個,可是需求任務能夠是無限多。
在需求任務不少的時候,常常出現任務處理不過來的狀況,致使任務堆積,出現OOM。
但由於全部的活都是我幹,沒有繁瑣的溝通成本,不須要處理線程同步的問題,這算是這種線程池的一個優勢吧。
這種線程池適用於併發量不大且須要任務順序執行的場景。
後來公司倒閉了,我又加入了一個叫FixedThreadPool的線程池。
FixedThreadPool和SingleThreadExecutor惟一不一樣的地方就是核心線程的數量,FixedThreadPool能夠招收不少的打工線程。
在這裏,我再也不是孤軍奮鬥了,我有了一羣共同打拼的小夥伴,你們一塊兒完成任務,一塊兒承擔壓力。
可這種線程池仍是存在一個問題——任務隊列是無界的,需求任務過多的話,仍是會形成OOM。
這種線程池線程數固定,且不被回收,線程與線程池的生命週期同步的線程池,適用於任務量比較固定但耗時長的任務。
後來,爲了離家更近,我離職了。加入了一家叫CachedThreadPool的線程池,進去以後,卻發現這是一家外包公司。
這種線程池裏面沒有一個核心線程(正式工),一有需求就去招聘一個非核心線程(臨時工)。
若是一個線程任務幹完了以後,60秒以後沒有新的任務就會被辭退。
這種線程池的任務隊列採用的是SynchronousQueue,這個隊列是沒法插入任務的,一有任務就建立一個線程執行,若是併發高且任務耗時長,建立太多線程也是可能致使OOM的。因此CachedThreadPool比較適合任務量大但耗時少的任務。
經歷了外面的風風雨雨,我以爲仍是找份固定的工做比較可靠,因而我加入了一家叫作ScheduleThreadPool的國企。
在這裏,工做比較的輕鬆,多數狀況下,我只須要在固定的時間幹固定的活。
任務忙不過來的時候,公司也會招聘一些臨時工幫忙處理,臨時工幹完活就會被辭退。
綜合來講,這類線程池適用於執行定時任務和具體固定週期的重複任務。因爲採用的任務隊列是DelayedWorkQueue無界隊列,因此也是有OOM的風險的。
好了,關於線程的故事就告一段落了。關於線程池的應用實踐,咱們下次再聊。
文章開頭的面試題在大部分在文中都能找到答案,對於沒有提到的,這裏作一個補充:
有execute和submit兩種方式
execute只能提交Runnable類型的任務,無返回值。submit既能夠提交Runnable類型的任務,也能夠提交Callable類型的任務,會有一個類型爲Future的返回值,但當任務類型爲Runnable時,返回值爲null。
execute在執行任務時,若是遇到異常會直接拋出,而submit不會直接拋出,只有在使用Future的get方法獲取返回值時,纔會拋出異常。
若是一個線程執行任務的過程當中出現異常,那麼這個線程對應的Worker會被移出線程池,該線程也會被銷燬回收。
同時會經過指定的線程工廠建立一個線程,並封裝成Worker放入線程池代替移除的Worker。
核心線程默認不會被回收。可是能夠調用allowCoreThreadTimeOut讓核心線程能夠被回收。
須要注意的是,調用這個方法的線程池必須將keepAliveTime設置爲大於0,不然會拋出異常。
核心線程和非核心線程是一個抽象概念,只是用於更好的表述線程池的運行邏輯,實際上都對應操做系統的osThread,都是重量級線程。
在新增Worker的時候,經過一個boolean表達是核心線程仍是非核心線程,本質上二者沒有什麼不一樣。
FixedThreadPool 和 SingleThreadPool:容許的請求隊列長度爲 Integer.MAX_VALUE,可能會堆積大量的請求,從而致使 OOM。
CachedThreadPool:容許的建立線程數量爲 Integer.MAX_VALUE,可能會建立大量的線程,從而致使 OOM。
總結來講就是,使用Executors建立線程池會容易忽視線程池的一些屬性,使用不當容易引發資源耗盡。
這個世界上或許沒有線程,又或許人人都是線程。
無畏年少青春,迎風瀟灑前行,作一個努力奮鬥的線程,但願他日回首望去,不是一片黑暗,而是漫天星光。
好了,今天的文章就到這裏了。
最後,感謝你的閱讀!
我是CoderW,一個普通的程序員。
點個關注,咱們下期再見!