Java線程實現
線程把處理器的調度和資源分配分開,是cpu的最小調度單位。多個線程可以共享進程的內存資源,又可以獨立調度。java線程關鍵方法都是通過高效的本地方法實現的。Java線程的主要實現方式有三種:內核實現、用戶實現、內核用戶混合實現。
1.內核實現
內核線程就是由內核調度、映射的線程。支持多線程的內核稱為多線程內核。這種線程,所有操作都需要系統調度,需要在內核態和用戶態切換,系統調用代價比較高。
2.用戶實現
這種線程建立在用戶空間,在用戶態中建立、同步、銷毀,不需要內核操作。這種操作非常快速且消耗低,可以支持更多的線程。但是線程阻塞和處理器的分配等功能在不借助內核態的情況下實現起來非常難,所以很少單獨使用。
3.內核用戶混合實現
最后是上面兩種方式的結合,通過用戶線程實現線程的創建、切換、析構等,而通過內核提供的輕量級進程實現線程的調度及處理器的映射。
JDK采用的是第二種方式即內核實現,每一個java線程都會對應內核提供的一條輕量級進程。
Java線程調度
線程調度就是分配處理器使用權的過程,主流的調度方式有兩種:協同式線程調度和搶占式線程調度。
協同式線程調度中線程的執行時間由線程自身控制,線程執行完后,要主動通知系統切換線程。這種方式實現起來比較簡單,且不存在線程同步問題。但是由于線程自身控制切換操作,若某個線程出現問題,可能會導致系統的崩潰。
搶占式線程調度中線程的調度由系統控制,這樣就可以避免某個線程掛掉而導致整個系統崩潰。我們的jdk線程就是采用的這種調度方式,系統運行起來會更加的穩定。
當然我們可以通過設置線程的優先級來提高某些線程的執行幾率,但是這種方式存在很大的不確定性。因為線程優先級的實現依賴于具體的操作系統平臺,不同的平臺優先級實現不同,可能會導致java中不同線程優先級在一些平臺上卻是按相同優先級進行調度的,另外操作系統還可能根據某些策略來忽略線程優先級,所以線程在cpu中的具體調度策略和執行順序是不可知的,我們不能想當然的臆測線程的執行邏輯。
java線程生命周期
Java線程主要存在5中狀態:新建(new)、運行(runnable)、無限期等待(waitting)、有限期等待(timed waiting)、阻塞(blocked)、結束(terminated)。
1.新建:創建后尚未啟動的線程。
2.運行:正在執行及等待cpu時間片的線程。
3.無限期等待:不會被分配時間片,等待喚醒的線程。主要包括:使用了Object.wait()、Thread.join()、LockSupport.park()等無timeout參數方法的線程。
4.有期限等待:這種狀態的線程也不會被分配時間片,但是在一定時間后系統會自動喚醒它們。主要包括:使用了Thread.sleep()及上面3中幾個帶timeout參數方法的線程。
5.結束:已經終止的線程。
線程池的優點
由于java線程是通過內核中的輕量級進程實現的,線程創建和銷毀都需要切換到內核態,線程生命周期開銷非常高。同時新建線程也會導致請求延遲一會才能被處理。另外由于每個線程都會分配一些獨立的內存空間,若創建過多的線程會增加內存的占用,同時大量空閑的線程持有對象強引用,會給垃圾回收帶來很大的壓力,大量的線程競爭cpu資源也會產生很大的性能開銷,降低程序的執行速度。在后端服務中經常會出現某些rpc接口的延遲抖動會導致整個服務所有接口性能下降,主要就是因為:依賴的外部接口抖動延遲響應時間變長,請求接口的線程阻塞同時大量請求重試,這時大量新線程被創建,cpu頻繁進行用戶態內核切換及大量線程爭用cpu,導致服務性能逐步下降。線程池的出現非常好的解決了上面的問題,現在代碼中已經很少能見到直接new Thread的操作了,有這種操作的程序猿要么是掃地圣僧,要么就是刪庫跑路的狠人了,哈哈哈哈。
線程及線程池使用注意點
1.盡量避免使用守護線程
Jvm在正常關閉時,會先并行執行關閉鉤子及所有已提交和執行中的普通線程,然后去處理定義了finalize方法的對象,做好這些后就會直接結束運行,不會管是否有是正在執行的守護線程,若我們在自定義的守護線程中進行了業務操作或IO操作之類的,就可能造成意外的業務錯誤。
2.避免改變線程優先級
jvm中的線程優先級只能作為線程調度的參考,線程并不一定按優先級高低順序執行,這是因為jvm中線程優先級是通過映射系統調度優先級實現的,依賴于特定的平臺,而不同平臺實現的調度優先級不同,因此兩個不同優先級的線程可能被映射成相同的調度優先級。除此之外使用優先級還可能會導致某些線程一直無法獲取cpu的調度,進而導致線程的饑餓問題。
3.依賴性任務可能導致線程的饑餓死鎖
在線程池中,如果任務依賴于其他任務,并且依賴的任務也在同一線程池中執行,那么便可能產生死鎖。當依賴的任務被拒絕或者一直停留在工作隊列中,那么任務就會一直阻塞并一直占用線程,隊列中任務也獲取不到這個線程,就會產生死鎖,這種死鎖被稱為線程饑餓死鎖。
4.線程池中的任務應該是同類型的獨立任務
計算密集型任務一定不能和IO密集型共用同一個線程池。道理其實很簡單,我們舉個例子:我們有兩個線程并行執行,其中一個需要9毫秒,而另一個需要1毫秒,當我們采用串行執行時任務執行所需時間為10毫秒,而當我并行執行時任務執行所需時間為9毫秒,線程的切換可能還需要一些時間(假設2毫秒),這樣算下來拋除線程切換造成的cpu資源浪費,結果并行時間反而還沒有串行快,吃力不討好啊。實際上計算密集型和IO密集型任務不但應該使用不同的線程池,連線程池大小的配置策略也是大不相同,小伙伴們要注意下。由此我們可以進一步推出:執行時間較長的任務不能和執行時較短的任務共用一個線程池,執行時間較長的任務不僅可能造成線程阻塞,也會增加執行時間較短任務的響應時間,甚至當長時間任務的qps大于線程池中的線程數量時,可能會出現所有線程都在執行長時間任務的現象,嚴重影響服務的性能。總而言之,線程池中的任務應該是同類型的獨立任務,并且我們需要根據任務類型去合理配置線程池的線程數量。