【JavaSE8 高级编程 多线程】多线程入门级解析 2019_7_27
多線程_入門級
- 前置概念
- 入門級概念
- 并發
- 流水線
- CPU流水線
- 超線程技術
- 并行
- ********
- 程序
- 進程
- 進程內容
- 進程狀態
- 線程
- 線程與進程比較
- 分類
- Unix定義的線程
- Java線程分類
- Java 多線程如何實現在多 CPU 上分布?
- 線程狀態
- 生命周期
- 線程池
- ********
- 進階級概念
- Java線程相關庫(含實現)
- Thread(簡)
- 函數一覽
- 常用實例方法
- 構造方法
- 常用實例方法
- 常用靜態(類)方法
- 構造Thread的三種方法
- ①Runnable實現類
- ②繼承Thread子類
- ③Callable&Future實現類
- 繼承Thread類
- 使用方法
- 實例演示
- Thread類Q&A
- Runnable
- 源碼
- 實現Runnable接口:
- 實例演示
- Callable&Future
- 實現C&F
- 實例演示
- 簡單實例
- 稍微復雜實例
- FutureTask
- 定制化Future任務類
- Executor
- ForkJoinPool
- ThreadPoolExecutor
- 與Executor的關系
- 概述
- 創建線程池
- Factory method
- 3種常用線程池
- newFixedThreadPool
- newCachedThreadPool
- SingleThreadExecutor
- 自定義線程池(構造函數)
- 參數提要
- 實現Executor.execute()
- 線程池關閉
- shutdown源碼
- shutdownNow源碼
- 詳述看下面的大佬
- 關于鎖
前言:本篇,只是簡簡簡簡簡簡簡簡單的入門而已……
因為確實短時間不可能掌握多線程原理
前置概念
入門級概念
并發
并發:是定義在操作系統上的概念
指在某一個時間片段中,有多個進程都處于啟動狀態—(就緒,運行,阻塞)—退出狀態之間,且這幾個進程都是在同一個CPU(Control Processer Unit)上運行,但任一個時刻只有一個進程在CPU上運行。
舉例一段時間:||||||||——這是一個時間片段,由8個時刻組成。
一般來講,時刻的單位是1Hz,代表每秒中的周期性變動重復次數的計量
CPU的處理能力是 1GHz ≈ 1000MHz ≈ 106KHz ≈ 109 Hz
也就是……emmm這個東西很強很猛,1s處理109個的時刻【這個說法不嚴謹】
大概就是這個意思,也就是為什么說,在某時刻只有一個,但我們感覺不出來,還是覺得多個進程是同時完成的,是因為CPU處理的太快了,你不覺得它們是間斷交叉執行的。
和視覺殘留的感覺是差不多的。不是因為你眼前還有實物(不是程序從未中斷運行),而是你的記憶中還有實物的影子(而是多個程序 “左右橫跳” 晃瞎你了你,令你覺得它們都還在運行隊列)。
流水線
當然,看到這里會覺得有些模糊,但又覺得知道了什么。
那么在深究一點:CPU為什么能夠在一個時間段執行多個進程?
因為CPU這個物體實體內部,做了工業流水線的設計(實際的物理結構決定了并發功能)
什么是流水線?
在經濟學歷史上有一個人很厲害——科學管理之父·弗雷德里克·溫斯洛·泰勒
泰勒最牛逼的成就是什么?——提高工業生產效率,他是如何提高效率的?
很簡單,把生產過程作為一個整體,進行切割,然后進行分析,然后對每個部分提升效率
最終達到了整體生產效率大幅度提高。
這個成果可以用批評他的人一句話來展示:“辛克萊的年輕的社會主義者寫信給《美國雜志》主編,指責泰勒“把工人的工資提高了61%,但工作量卻提高了362%”。”
沒錯,這就是萬惡的資本主義,哈哈哈哈哈哈。這也引出了流水線的核心目標
不讓任何一個人空閑1秒鐘,要讓他工作的8個小時都在干活
不讓CPU的任何一個計算單元空閑1毫秒,要讓它無休無止的干活
如果還是覺得流水線很模糊,那就請了解一下歷史上的福特汽車,以超低售價霸占美國汽車銷售市場,采用流水線作業。
這里穿插一個分析,流水線的核心價值來源——生產效率與事件的復雜程度成反比。
就人類而言,越簡單的事情執行起來越高效,越復雜的事情執行起來效率會越低。
例如,一個人只劈柴,一天可以劈1200根木頭,只收集木頭,一天可收集400根,如果又劈柴又收集木頭只能處理200根,那么現在有4個人,怎么效率更高?
效率最低的是4個人自己干收集木頭+劈柴整個流程,1天內4人整體可處理800根
效率最高的是分工合作,流水線作業。3個人只收集木頭共1200根,1個人只劈柴共1200根,1天內4人整體效率提升了1200/800=150%
所以,我們盡可能的將一個復雜事情拆分成幾個簡單的事情。以通過和他人分工合作的方式來提高生產效率(時間利用率)。【是的,我們所謂效率是以產出與時間對比而來的】
CPU流水線
那么接下來,我們討論CPU流水線設計的具體結構
為什么我們的CPU作為一個整體可以拆分呢?
其實這個問題在某個程度上是不正確的,但CPU確實也是可以拆分的,由多個部分組成
但真正拆分的是線程命令轉換成的匯編指令在CPU中的運行過程(類比生產過程)。
我們來解析一下,匯編指令是如何被CPU運行的
JAVA代碼編譯為二進制碼,然后被JVM轉換為機器匯編指令,然后存儲在存儲器中。
這個時候——CPU開始工作
CPU從存儲器中取出指令,然后將取出的指令進行翻譯,得到執行指令的各種前置信息。然后對指令進行執行,執行過程中可能發現需要存儲器(內存)中的數據,那么就需要訪存,當計算完畢后,將結果寫回存儲器,好了CPU負責的整個流程就結束了。
我們可以從這個過程中抽出5個關鍵步驟:1.取指,2.譯碼,3.執行,4.訪存,5.寫回
嗯,但這又怎么做到像造汽車一樣的流水線作業呢?
原本的CPU是一整塊結構,一個挨著一個執行,A|B|C|D|EA部分執行完了,電流跑到B部分,最后一直跑到E部分,跑完整個流程。但這個時候你會發現,電流跑到C的時候,A、B部分都閑下來了!!不行,我們不能讓它們閑著
現在我們可以將整體分成為5個部分,讓取值的部分只取值,再給它配個存儲器,讓譯碼的只譯碼,也給它配個存儲器,等等,如此操作就會有這樣的效果,雖然仍然是一個時刻只有一個線程在運行EX,但好像我在一段時間內跑了好多個線程也不覺得有線程被中斷執行!!!【在一個時間段內,多個線程處于啟動狀態—(就緒,運行,阻塞)—退出狀態之間】
這里也就是所謂1核CPU=1個線程
超線程技術
順便說一下為什么現在的Inter是4核8線程,8核16線程,12核24線程。
實話實話,8核還是實質是8線程,只是因為Inter的一核有10-25級流水線,但通常情況下,功能過剩,可能只運行到13級就完成了。那么為了充分利用這么多級流水線,英特爾在一個實體CPU中,提供兩個邏輯線程【只有邏輯線程,但CPU不止由邏輯線程組成】,在CPU內部僅復制必要的資源、讓兩個線程可同時運行;在一單位時間內處理兩個線程的工作,模擬實體雙核心、雙線程運作。然后利用上未被使用的其他級流水線。這個技術叫做超線程技術。通常超線程技術能夠提升只有1個線程運行的情況下的15-30%的工作效率。
這個東西我…不是很清楚,有興趣的自行尋找。【感覺就是,把CPU組成線程的核心部件增加1個,然后復用原本一個線程的資源部件,一個身體兩個頭?雙頭蛇?】
好了,到此,關于并發應該就沒有任何疑問了。都有了宏觀概念及實物對應。多思考思考理解核心。
參考文獻:
http://m.elecfans.com/article/657563.html
https://baike.baidu.com/item/%E8%B6%85%E7%BA%BF%E7%A8%8B
并行
當你完完全全的將并發看懂了之后,對于并行理解起來,只需要1分鐘的時間
并行就是多個CPU……哈哈哈哈,對的,4核CPU4線程,同時執行4個線程
注意是:同時同時同時!!!!
當然,并不是核心越多越好,因為,人多嘴雜,管理及交流成本太高,以及你CPU就屁大點地方,如果離的太遠,會導致核心間通信的耗時比計算耗時還要久(面積≈距離與速度≈性能之比),還要考慮散熱(CPU耗能比很高,使用的電流90%發散為焦耳熱)等等
所以,并行很厲害,但并發也很重要,而且二者是可以疊加奏效的。
你強,我也強,我們聯合更強。大概是這樣的意思
********
程序
一般是指的,未被啟動的靜態EXE文件。
程序本質:是指令、數據及其組織形式的描述
進程
指計算機中已啟動并運行的程序。
在面向進程設計的系統(如早期的UNIX,Linux 2.4及更早的版本)中,進程是程序的基本執行實體;
在面向線程設計的系統(如當代多數操作系統、Linux 2.6及更新的版本)中,進程本身不是基本運行單位,而是線程的容器。
進程是一個動態的實體(包含程序執行環境/上下文的實體),它擁有自己的生命周期。是通過程序被OS啟動,實例化創建在內存的中的,程序的一個實體/線程依存的一個容器。
一個進程中可以并發多個線程,至少有一個線程,每條線程可并行執行不同的任務(順序代碼)。
也可簡單的認為:進程 = CPU加載上下文(就緒)+CPU執行+CPU保存上下文(掛起)
若干進程有可能與同一個程序相關系,例如雙開DNF游戲客戶端,2個進程同1個程序。
進程內容
- 程序的可執行機器代碼在存儲器的映像。
- OS分配的存儲器。存儲器的內容包括可執行代碼、特定于進程的數據(輸入、輸出)、永久堆棧、臨時堆棧。
- OS分配的資源描述符,諸如文件描述符(Unix術語)或文件句柄(Windows)、數據源和數據終端、進程PID。
- 安全特性,諸如進程擁有者和進程的權限集(可以容許的操作)。
- 處理器狀態(內文),諸如寄存器內容、物理存儲器定址等。當進程正在運行時,狀態通常存儲在寄存器,其他情況在存儲器。
- 一個進程一直運行,直到所有的非守護線程都結束運行后才能結束。
進程狀態
新生(new):進程新產生中。
運行(running):正在CPU中執行指令。
等待(waiting):等待某事發生,例如等待用戶輸入完成。亦稱“阻塞”(blocked)
就緒(ready):排隊中,等待CPU執行。
結束(terminated):完成運行。
線程
線程是操作系統能夠進行運算調度的最小單位。真正在CPU上運行的是線程。
它被包含在進程之中,是進程中的實際運作單位。
同一進程中的多條線程將共享該進程中的被分配到的資源。同一進程中的多個線程共享代碼段(代碼和常量),數據段(全局變量和靜態變量),擴展段(堆存儲)。**但同一進程中的多個線程又有獨立的系統資源(當然是從進程中劃撥過來的)**如調用棧(call stack),自己的寄存器環境(register context),自己的線程本地存儲(thread-local storage)。
線程與進程比較
1.線程節省了切換進程上下文,以及進程間交流的開銷。
因為它們共享一個進程的全部系統資源,可直接訪問共享變量。
2.進程管理OS分配給程序實例的資源,及管理它下面的多線程,線程管理如何進行計算/執行任務(讓專業的人做專業的事情來提高整體效率/Unix哲學:做好一件事)
3.進程是資源分配的最小單位,線程是CPU調度的最小單位(如果想要增大系統資源,創建子進程,如果想要增加工作效率開辟子線程,當然是子線程是使用父進程的系統資源)
4.進程的執行過程是線狀的,盡管中間發生中斷暫停,但該進程所擁有的資源只為該線狀執行過程服務。一旦發生進程上下文切換,這些資源都是被保護起來的,不被別人使用。
5.線程的改變只代表了 CPU 執行過程的改變,而沒有發生進程所擁有的資源變化。
6.計算機內的軟硬件資源的分配與線程無關,線程只能共享它所屬進程的資源。
7.進程擁有一個完整的虛擬地址空間,不依賴于線程而獨立存在;反之,線程是進程的一部分,沒有自己的地址空間,與進程內的其他線程一起共享分配給該進程的所有資源。
8.線程中執行時一般都要使用同步(synchronized)和互斥,因為他們共享同一進程的所有資源。
5.6.7.8.9來源
知乎作者:力扣(LeetCode)
分類
Unix定義的線程
用戶級線程
用戶級線程不需要經過用戶態/核心態切換,速度快,但操作系統內核不知道多線程的存在,因此一個線程阻塞將使得整個進程(包括它的所有線程)阻塞。由于這里的處理器時間片分配是以進程為基本單位,所以每個線程執行的時間相對減少。雖然少了進出內核態的消耗,但不能很好的利用多核Cpu。
應用進程利用線程庫提供創建、同步、調度和管理線程的函數來控制用戶線程。
內核級線程
切換由內核控制,當線程進行切換的時候,由用戶態轉化為內核態。切換完畢要從內核態返回用戶態;可以很好的利用smp,即利用多核cpu。windows線程就是這樣的。
一個內核線程由于I/O操作而阻塞,不會影響其它線程的運行。內核級線程又稱為內核支持的線程或輕量級進程
二者區別
1.用戶級線程的創建、撤消和調度不需要OS內核的支持,是在語言(如Java)這一級處理的;而內核支持線程的創建、撤消和調度都需OS內核提供支持,而且與進程的創建、撤消和調度大體是相同的。
2.內核支持線程是OS內核可感知的,而用戶級線程是OS內核不可感知的。
用戶進程優點
(1) 線程的調度不需要內核直接參與,控制簡單。
(2) 可以在不支持線程的操作系統中實現。
(3) 創建和銷毀線程、線程切換代價等線程管理的代價比內核線程少得多。
(4) 允許每個進程定制自己的調度算法,線程管理比較靈活。
(5) 線程能夠利用的表空間和堆棧空間比內核級線程多。
(6) 同一進程中只能同時有一個線程在運行,如果有一個線程使用了系統調用而阻塞,那么整個進程都會被掛起。另外,頁面失效也會產生同樣的問題。
用戶進程缺點:
(1)資源調度按照進程進行,多個CPU,同一個進程中的線程也只能在同一個處理機下分時復用
參考 知乎作者:那場遇見
Java線程分類
守護線程
守護線程又稱為服務線程,主要功用是——服務其他非守護線程即用戶線程。
只要當前JVM實例中尚存任何一個非守護線程沒有結束,守護線程就全部工作;
只有當最后一個非守護線程結束時,守護線程隨著JVM一同結束工作。
守護線程最典型的應用就是 GC (垃圾回收器),它就是一個很稱職的守護者。
守護線程并非只有虛擬機內部提供,用戶在編寫程序時也可以自己設置守護線程。
參考博文 ziq711
用戶線程(非守護線程)
用戶自己使用Thread類創建的線程
Java 多線程如何實現在多 CPU 上分布?
根據JVM虛擬機來確定實現方法。常用SE JVM是交給OS來進行選擇。
常用的JVM實現,Oracle/Sun的HotSpot VM,它是用1:1模型來實現Java線程的,也就是說一個Java線程是直接通過一個OS線程來實現的,中間并沒有額外的間接結構。而且HotSpot VM自己也不干涉線程的調度,全權交給底下的OS去處理。所以如果OS想把某個線程調度到某個CPU/核上,它就自己弄了。
知乎作者:RednaxelaFX
線程狀態
與進程相同
1.新建狀態:
使用 new 關鍵字和 Thread 類或其子類建立一個線程對象后,該線程對象就處于新建狀態。它保持這個狀態直到程序 start() 這個線程。
2.就緒狀態:
當線程對象調用了start()方法之后,該線程就進入就緒狀態。就緒狀態的線程處于就緒隊列中,要等待JVM里線程調度器的調度。
3.運行狀態:
如果就緒狀態的線程獲取 CPU 資源,就可以執行 run(),此時線程便處于運行狀態。處于運行狀態的線程最為復雜,它可以變為阻塞狀態、就緒狀態和死亡狀態。
4.阻塞狀態:
如果一個線程執行了sleep(睡眠)、suspend(掛起)等方法,失去所占用資源之后,該線程就從運行狀態進入阻塞狀態。在睡眠時間已到或獲得設備資源后可以重新進入就緒狀態。可以分為三種:
-
等待阻塞:
運行狀態中的線程執行 wait() 方法,使線程進入到等待阻塞狀態。 -
同步阻塞:
線程在獲取 synchronized 同步鎖失敗(因為同步鎖被其他線程占用)。 -
其他阻塞:
通過調用線程的 sleep() 或 join() 發出了 I/O 請求時,線程就會進入到阻塞狀態。當sleep() 狀態超時,join() 等待線程終止或超時,或者 I/O 處理完畢,線程重新轉入就緒狀態。
5.死亡狀態:
一個運行狀態的線程完成任務或者其他終止條件發生時,該線程就切換到終止狀態。
生命周期
線程池
類比數據庫連接池
線程池可以看做是線程的集合。在沒有任務時線程處于空閑狀態,當請求到來:線程池給這個請求分配一個空閑的線程,任務完成后回到線程池中等待下次任務**(而不是銷毀)。這樣就實現了線程的重用。
主要功能:減少創建銷毀Thread對象的時間開銷,以及創建Thread對象的延遲。
線程生命周期的開銷非常高。創建和銷毀線程所花費的時間和資源可能比處理客戶端的任務花費的時間和資源更多,并且還可能會出現空閑線程占用系統資源。
以下為JDK的線程池相關庫
相關參考 知乎@Java3y
********
進階級概念
JVM
Synchronized【本博客有2篇相關文章(入門)】
Volatile
Java線程相關庫(含實現)
Thread,Runnable,Callable,Future,FutureTask,Executer
Thread(簡)
Java SE8 官方文檔:Thread
Thread類是Java實現多線程的基本類,Thread類 實現 Runnable接口 void run()
值得一提,Java應用程序調用main()方法也是由一個非守護線程來執行的。
新的thread的優先級初始設置為創建新線程的線程的優先級,并且當且僅當創建新線程的線程是守護進程時才是守護進程線程。
函數一覽
常用實例方法
1 public void start()
使該線程開始執行;Java 虛擬機調用該線程的 run 方法。【準備進程上下文】
2 public void run()
如果該線程是使用獨立的 Runnable 運行對象構造的,則調用該 Runnable 對象的 run 方法;否則,該方法不執行任何操作并返回。【也就是必須實現Runnable接口】
3 public final void setName(String name)
改變線程名稱。
4 public final void setPriority(int priority)
更改線程的優先級。
5 public final void setDaemon(boolean on)
將該線程標記為守護線程(true)或用戶線程(false)。
6 public final void join(long millisec)
等待該線程終止的時間最長為 millis 毫秒。
7 public void interrupt()
中斷線程。
8 public final boolean isAlive()
測試線程是否處于活動狀態。
構造方法
Thread 定義了多個構造方法,下面是常用的:
Thread(Runnable threadOb,String threadName);
threadOb:實現Runnable接口的實現類對象,threadName定義線程名
常用實例方法
| public void start() | 使該線程開始執行;Java 虛擬機調用該線程的 run 方法。 |
| public void run() | 如果線程使用 Runnable 實現類構造,則調用該 Runnable 對象 run 方法;否則,該方法不執行任何操作并返回。 |
| public final void setName(String name) | 改變線程名稱,使之與參數 name 相同。 |
| public final void setPriority(int priority) | 更改線程的優先級。 |
| public final void setDaemon(boolean on) | 將該線程標記為守護線程或用戶線程。 |
| public final void join(long millisec) | 等待該線程終止的時間最長為 millis 毫秒。 |
| public void interrupt() | 中斷線程。 |
| public final boolean isAlive() | 測試線程是否處于活動狀態。 |
常用靜態(類)方法
| public static void yield() | 暫停當前正在執行的線程對象,并執行其他線程。 |
| public static void sleep(long millisec) | 在指定的毫秒數內讓當前正在執行的線程休眠(暫停執行),此操作受到系統計時器和調度程序精度和準確性的影響。 |
| public static boolean holdsLock(Object x) | 當且僅當當前線程在指定的對象上保持監視器鎖時,才返回 true。 |
| public static Thread currentThread() | 返回對當前正在執行的線程對象的引用。 |
| public static void dumpStack() | 將當前線程的堆棧跟蹤打印至標準錯誤流。 |
構造Thread的三種方法
傳入以下三種實例進入Thread類構造器中
①Runnable實現類
- 實現 Runnable 接口的實現類;【相較繼承無限制,使用方便】【但沒有返回值】
②繼承Thread子類
- 繼承 Thread 類的子類;【與Runnable沒本質區別,Thread也實現了Runnable】
③Callable&Future實現類
- 實現 Callable 和 Future接口的實現類(二次封裝的FutureTask類)【有返回值】*3
繼承Thread類
使用方法
繼承類必須重寫 run() 方法,run()是新線程的入口點。繼承類通過調用Thread.start() 方法執行。
實例演示
1.class 新進程1 extends Thread
2.Override run方法
run() 內可以調用其他方法,使用其他類,并聲明變量,就像主線程一樣。
注:創建一個實現 Runnable 接口的類之后,你可以在類中實例化一個線程對象。這樣就可以封裝Thread方法。如下
3.啟動線程
public class TestThread {public static void main(String args[]) {//未封裝Thread類SampleThread2 sampleThread = new SampleThread2();Thread A_thread1 = new Thread(sampleThread, "A_thread1");A_thread1.start();} }Thread類Q&A
Q1:為什么要繼承/實現接口,而不是直接實例化Thread?
A:因為Thread中的run方法是空的,需要程序員復寫。
Q2:run方法和調用start方法區別?
A:run方法是執行體,start方法是使目標線程進入就緒狀態。
Q3:為什么要新建線程?
A:多線程允許執行多個順序代碼單獨執行,而不是都在main方法中進行同步阻塞執行。
Q4:為什么線程執行具有隨機性?
A:啟動一個線程并不會立即執行,而是等待CPU的資源調度,CPU能調度哪個線程,是通過多種復雜的算法計算而來,故在不精通OS的程序員看來線程是隨機執行的。當然我們也可以在一定程度上控制線程執行順序。【線程優先級屬性控制,或者線程池控制,或者join函數,暫停當前主函數,執行子函數。哈哈哈,太蠢了】
Q5:就緒的子線程與父線程有區別嗎
A:每一個執行線程與父線程一樣,都有自己獨有的棧內存空間,進行方法的壓棧和彈棧。
Runnable
無返回值的線程執行
源碼
public interface Runnable {public abstract void run(); }當啟動一個線程后會自動調用復寫的run方法,run方法在一般契約下被允許做任何事情。
實現Runnable接口:
實現Runnable接口的類重寫 run() ,該方法是新線程的入口點。
新線程類必須通過調用Thread.start() 方法才能被執行。
實例演示
Runnable與繼承Thread類一模一樣……
1.類 新線程1 implements Runnable
2.@Override run方法
注:run() 內可以調用其他方法,使用其他類,聲明變量,同主線程一樣【do everything】
注:創建一個實現 Runnable 接口的類之后,你可以在類中實例化一個線程對象。這樣就可以封裝Thread方法。如下
class SampleThread implements Runnable {private Thread t;//SampleThread類的start方法封裝Thread.start()方法public void start () {System.out.println("Starting " + threadName );if (t == null) {t = new Thread (this, threadName);t.start ();}} }3.啟動線程
public class TestThread {public static void main(String args[]) {//未封裝Thread類SampleThread sampleThread = new SampleThread();//傳入Runnable實例構建Thread實例Thread A_thread1 = new Thread(sampleThread, "A_thread1");//啟動Thread實例A_thread1.start();} }Callable&Future
有返回值的線程執行
Callable接口源碼
Future接口源碼
public interface Future<V> {//取消任務(可能不會被取消)boolean cancel(boolean mayInterruptIfRunning);//查詢任務是否被取消(如果此任務在正常完成之前被取消,則返回true)boolean isCancelled();//查詢任務是否已完成(如果此任務已完成,則返回true)boolean isDone();//等待計算過程,然后獲取計算結果V get() throws InterruptedException, ExecutionException;//等待規定時間,若超時拋出TimeoutException,否則返回計算結果V get(long timeout, TimeUnit unit)throws InterruptedException, ExecutionException, TimeoutException; }實現C&F
與Runnable的使用方法沒太大區別,只是Callable多了返回值類型,而Future接口是增強了對于線程返回結果的操作(取消任務、查詢任務狀態、獲取結果可延時)。
實例演示
1.創建 Callable 接口的實現類,復寫 call方法, call()方法作為線程執行體,有返回值
【類比Runnable接口的實現類,復寫 run方法,run()方法作為線程執行體,無返回值】
2.new Callable 實現類的實例,使用 FutureTask 類(Future接口實現類) 包裝 Callable 對象
【該 FutureTask 對象封裝 Callable實現類 對象的 call() 方法的返回值】
注意:FutureTask也能封裝Runnable接口,當Runnable實例成功執行后,返回result。
3.使用 FutureTask 對象作為 Thread 對象的 target 創建并啟動新線程。
【Runnable是直接以Runnable實現類實例作為Thread對象的target,沒有二次封裝】
注意:Future接口與Runnable接口沒半毛錢關系,但Thread的構造函數接受的參數要么是Runnable,要么是ThreadGroup線程池,或者String。故需要FutureTask,它實現了Runnable及Future接口。
4.調用 FutureTask 對象的 get() 方法來獲得子線程執行結束后的返回值。
【Runnable沒有返回值】
簡單實例
//1.Callable<Integer>接口實現類,不過這個<Integer>不寫也沒問題 public class SampleCallableThread implements Callable<Integer> {@Overridepublic Object call() throws Exception {return 1;} } //測試類 public class ThreadTest {public static void main(String[] args) throws ExecutionException, InterruptedException {//2.實例化Callable實現類SampleCallableThread sampleCallableThread = new SampleCallableThread();//3.使用FutureTask類實例封裝Callable實現類對象(顯式指定返回值類型與Callable實現類的返回值相同)FutureTask<Integer> integerFutureTask = new FutureTask<Integer>(sampleCallableThread);//4.傳入FutureTask二次封裝對象新建Thread實例,并啟動線程Threadnew Thread(integerFutureTask, "SampleCallableThread").start();System.out.println(integerFutureTask.get());} }稍微復雜實例
//1.Callable<Integer>接口實現類 public class SampleCallableThread implements Callable<Integer> {@Overridepublic Integer call() throws Exception {int i = 0;for (; i < 10; i++)System.out.println(Thread.currentThread().getName() + " 的循環變量i的值" + i);//返回i=10return i;} } //測試類 public class ThreadMain {public static void main(String[] args) throws ExecutionException, InterruptedException {//2.實例化Callable<Integer>接口實現類SampleCallableThread sampleCallableThread = new SampleCallableThread();//3.實例化FutureTask<Integer>類(Future接口的實現類)//以Callable<Integer>接口實現類作為FT類構造函數的參數完成FT類實例初始化FutureTask<Integer> integerFutureTask = new FutureTask<Integer>(sampleCallableThread);for (int i = 0; i < 100; i++) {System.out.println(Thread.currentThread().getName() + " 的循環變量i的值" + i);//當i==20時,新建進程Thread(以FT實例作為Target啟動)if (i == 20)//4.傳入FT實例構建Thread實例,并啟動Thread線程。new Thread(integerFutureTask, "SampleCallableThread").start();}System.out.println("子線程的返回值:"+integerFutureTask.get());} }FutureTask
可取消的異步計算。該類提供了Future的基本實現,提供了啟動和取消計算、查詢是否完成計算以及檢索計算結果的方法。
計算完成后才可檢索結果;如果計算尚未完成, get方法將阻塞。一旦計算完成,就不能重新啟動或取消計算(除非使用runAndReset調用計算)。
注1:FutureTask也可二次封裝Runnable實例,不僅可封裝Callable實例。返回值是由構造時傳入參數(可指定的),然后通過適配器Adaptor類進行處理。
注2:因為FutureTask實現了Runnable,所以可將FutureTask提交給Executor線程池執行。
定制化Future任務類
FutureTask還擁有4個Protected函數,可幫助程序員對任務進行定制化
Executor
Executor接口提供了一種方法,可以將任務提交與每個任務的運行機制(包括線程使用、調度等細節)分離開來。
public interface Executor {//一般實現是在未來某個時刻,執行Runnable接口的run方法//Executes the given command at some time in the future.//因為不是立刻執行,故稱為 任務提交 ——》將執行體run()建立線程//進入線程的就緒狀態,等待CPU調用進入線程的運行狀態。void execute(Runnable command); }Executor向下繼承關系
ForkJoinPool
ForkJoinPool是Fork/Join框架的兩大核心類之一。與其它類型的ExecutorService相比,其主要的不同在于采用了工作竊取算法(work-stealing):所有池中線程會嘗試找到并執行已被提交到池中的或由其他線程創建的任務。這樣很少有線程會處于空閑狀態,非常高效。這使得能夠有效地處理以下情景:大多數由任務產生大量子任務的情況;從外部客戶端大量提交小任務到池中的情況。
jdk1.7新增實現類
CSDN作者:潘威威
ThreadPoolExecutor
線程池可以類比數據庫連接池,功能與存在意義都是相似的,只不過是服務的對象從數據庫連接對象,變成了線程對象。
意義:重用,減少new對象與destroy對象的時間開銷。
額外作用:提供管理、限定線程資源的方法
實現:當需要新建線程時,線程池給這個請求分配一個空閑的線程,Runnable實例的run()執行體內容完成后,Thread線程對象回到線程池中等待下次任務,而不是直接銷毀線程。
與Executor的關系
ThreadPoolExecutor實現了Executor的execute()方法
概述
創建線程池
ThreadPoolExecutor類 擁有眾多屬性,也就是——可以線性組合出多種適合不同使用需求的線程池。也就是——這個線程池類是擁有多種策略的集合體。
你可以根據需求自定義線程池不同方面的屬性(通過構造函數參數設置/繼承這個類),以來滿足你獨特♂的需求。
繼承ThreadPoolExecutor類可以使用其protected方法,如Hock method進行日志等擴展
詳情參考 jdk-8u221-docs-all/docs/api/index.html
Factory method
但是呢,大多數現實需求的線程池場景,jdk已經幫你設定好了,你只需要從Executors類選擇工廠方法即可創建,挑選你需要的♂
3種常用線程池
在Executors類種實現
newFixedThreadPool
固定線程數的線程池
public static ExecutorService newFixedThreadPool(int nThreads) {//corePoolSize == maximumPoolSize == nThreadsreturn new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());}newCachedThreadPool
彈性線程池,對于新任務,如果此時線程池沒有空閑線程,線程池會立即新建線程。
當然是在不超過最大線程數Integer.MAX_VALUE的前提下。
SingleThreadExecutor
單個線程的線程池Executor
注:這個池子,就是上面我提及到的線程順序執行的【線程池控制方法】
注1:如果此單個線程由于在關閉之前執行期間的故障而終止,則在需要執行后續任務時將使用新的線程。
注2:與其他等效的newFixedThreadPool(1);不同,SingleThreadExecutor保證返回的executor不可被重新配置以使用additional(其他的)線程。
自定義線程池(構造函數)
構造方法可以讓我們自定義(擴展)線程池
public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler) {if (corePoolSize < 0 ||maximumPoolSize <= 0 ||maximumPoolSize < corePoolSize ||keepAliveTime < 0)throw new IllegalArgumentException();if (workQueue == null || threadFactory == null || handler == null)throw new NullPointerException();this.acc = System.getSecurityManager() == null ?null :AccessController.getContext();this.corePoolSize = corePoolSize;this.maximumPoolSize = maximumPoolSize;this.workQueue = workQueue;this.keepAliveTime = unit.toNanos(keepAliveTime);this.threadFactory = threadFactory;this.handler = handler;}corePoolSize:指定核心線程數量
maximumPoolSize:指定最大線程數量
keepAliveTime:當線程數大于核心時,多余空閑線程在終止之前等待新任務的最長時間
unit:為keepAliveTime計時創建的時間對象
workQueue:用于在任務執行前保存任務的隊列。只包含execute方法提交的Runnable.run
threadFactory:創建新線程時使用的線程工廠
handler:任務拒絕策略
參數提要
corePoolSize&maximumPoolSize 線程數量:
- 如果運行線程的數量<核心線程數量,則創建新的線程處理請求
- 如果運行線程的數量>核心線程數量,<最大線程數量,則當workqueue滿時創建新線程
- 如果核心線程數量==最大線程數量,那么將創建固定大小的連接池
- 如果設置了最大線程數量為無窮,那么允許線程池適合任意數量的并發請求
線程空閑時間:
- 當前線程數大于核心線程數情況下,如果超過空閑時間了,多余空閑的線程會被銷毀
workqueue隊列策略:
- 同步移交:不會放到隊列中,而是等待線程執行它。如果當前線程沒有執行,很可能會新開一個線程執行。當然也有可能被拒絕任務。
- 無界限隊列策略:如果運行線程==核心線程數量都在CPU并發執行,則新進任務會放到隊列中等待。運行線程數永遠不會超過核心線程數。但存儲隊列會消耗內存
- 有界限隊列策略:有限隊列ArrayBlockingQueue與有限maximumPoolSizes一起使用時有助于防止系統資源(內存)耗盡。一般是兩個模式,①小線程池大隊列,有助于最小化CPU使用率、OS資源、上下文切換開銷,但可能導致人為的低吞吐量。②大線程池小隊列 ,有助于疏解多線程阻塞問題(一般遭遇到的是I/O阻塞)這會使CPU更加繁忙,但可能會遇到不可接受的調度開銷,這也會降低吞吐量。
線程關閉或者線程數量達到MAX和阻塞隊列飽和,就有拒絕任務的情況出現。
拒絕任務策略:
- 直接拋出異常
- 使用調用者的線程來處理
- 直接丟掉這個任務
- 丟掉最老的任務
實現Executor.execute()
public void execute(Runnable command) {//如果傳入的Runnable.run()任務是空的,則拋出空指針異常if (command == null)throw new NullPointerException();//前面我省略說了ctl屬性,ctl記錄了線程池中任務數量和線程池狀態(二進制的方法)int c = ctl.get();//Step1:如果處于運行狀態的線程數小于核心線程數,則直接新建一個進程,不論有無空閑線程if (workerCountOf(c) < corePoolSize) {//這里通過返回false可防止在失敗添加線程時拋出錯誤警報。if (addWorker(command, true))return;c = ctl.get();}//Step2:如果任務成功排入隊列WorkQueue,我們仍需要檢查線程池是否應該添加一個線程//(因為自上次檢查后現有的線程可能已經死亡),或者自這個任務進入隊列后線程池關閉if (isRunning(c) && workQueue.offer(command)) {int recheck = ctl.get();//如果線程池不是RUNNING狀態,且成功從阻塞隊列中刪除任務if (! isRunning(recheck) && remove(command))//則該任務由當前 RejectedExecutionHandler 處理。reject(command);//Step3:如果線程池正在運行,但無法令任務進入阻塞隊列(隊列已滿),則新建線程else if (workerCountOf(recheck) == 0)//注意:新建線程對應的任務為nulladdWorker(null, false);}//如果以上步驟都不行【可能的原因:線程池關閉、隊列&最大線程數達到上限】else if (!addWorker(command, false))//則將任務由當前 RejectedExecutionHandler 處理。reject(command); }線程池關閉
ThreadPoolExecutor提供了shutdown()和shutdownNow()兩個方法來關閉線程池
淺顯比喻:
一個是臺式機正常關機(退出所有程序)拔電,一個是臺式機直接拔電。
實際區別:
- 調用shutdown(),線程池狀態立刻變為SHUTDOWN,調用shutdownNow(),線程池狀態立刻變為STOP。
- shutdown()等待當前屬于運行狀態的線程(包含任務)執行完才中斷線程,而shutdownNow()不等任務執行完就中斷線程。
shutdown源碼
啟動有序關閉,先前提交的任務與正在執行的任務都將完整執行,但不接受任何新任務
注:如果不想執行workqueue隊列中等待的任務,只執行運行狀態的線程(包含任務),可執行 public boolean awaitTermination(long timeout, TimeUnit unit)
public void shutdown() {//獲取鎖final ReentrantLock mainLock = this.mainLock;//鎖定等待線程狀態,禁止線程池其他線程進入執行狀態//Lock held on access to workers set and related bookkeeping(?).mainLock.lock();try {//檢測是否對工作線程擁有中斷權限checkShutdownAccess();//設置線程池為shutdown狀態advanceRunState(SHUTDOWN);//中斷空閑線程interruptIdleWorkers();//在本類中此函數no-op,但在STPExecutor中用于取消延遲的任務onShutdown(); // hook for ScheduledThreadPoolExecutor} finally {mainLock.unlock();}//如果(SHUTDOWN,池和隊列為空)或(STOP和池為空),則轉換為TERMINATED狀態。tryTerminate();}shutdownNow源碼
嘗試停止所有正在執行的任務(線程),停止處理等待的任務(workqueue),并返回等待執行的任務列表。從該方法返回時,將從任務隊列中刪除這些任務。
public List<Runnable> shutdownNow() {List<Runnable> tasks;final ReentrantLock mainLock = this.mainLock;//鎖定等待線程狀態,禁止線程池其他線程進入執行狀態mainLock.lock();try {//檢測是否對工作線程擁有中斷權限checkShutdownAccess();//設置線程池為stop狀態advanceRunState(STOP);//中斷全部線程!!!重點,shutdown是interruptIdleWorkers,now沒IdinterruptWorkers();//返回drainqueue隊列<List>內全部成員,同時清空drainqueue隊列tasks = drainQueue();} finally {mainLock.unlock();}//如果(SHUTDOWN,池和隊列為空)或(STOP和池為空),則轉換為TERMINATED狀態。tryTerminate();return tasks;}詳述看下面的大佬
高清圖_bky作者Java3y:線程池詳解
關于鎖
emmm不寫鎖,確實多線程不完整。多線程1/3是玩弄線程,1/3是玩弄各種鎖。
多線程到此為止就算是入門了。之后單獨開一篇鎖入門。
總結
以上是生活随笔為你收集整理的【JavaSE8 高级编程 多线程】多线程入门级解析 2019_7_27的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: CSDN公式插入——关于对数
- 下一篇: 转:Java IO