一、基本概念梳理
1.1 線程生命週期:
Java中線程的狀態分爲6種。
1. 初始(NEW):新創建了一個線程對象,但還沒有調用start()方法。
2. 運行(RUNNABLE):Java線程中將就緒(ready)和運行中(running)兩種狀態籠統的稱爲“運行”。
線程對象創建後,其他線程(比如main線程)調用了該對象的start()方法。該狀態的線程位於可運行線程池中,等待被線程調度選中,獲取CPU的使用權,此時處於就緒狀態(ready)。就緒狀態的線程在獲得CPU時間片後變爲運行中狀態(running)。
3.阻塞(BLOCKED):表示線程阻塞於鎖。
4.等待(WAITING):進入該狀態的線程需要等待其他線程做出一些特定動作(通知或中斷)。
5.超時等待(TIMED_WAITING):該狀態不同於WAITING,它可以在指定的時間後自行返回。
6. 終止(TERMINATED):表示該線程已經執行完畢。
直接使用thread執行run方法會咋樣呢?因爲run方法是thread裏面的一個普通的方法,所以我們直接調用run方法,這個時候它是會運行在我們的主線程中的。
調start方法,正常的話,是將該線程加入線程組,最後嘗試調用start0方法,而start0方法是私有的native方法(Native Method是一個java調用非java代碼的接口)。
3.阻塞(BLOCKED):表示線程阻塞於鎖。
這個狀態,一般是線程等待獲取一個鎖,來繼續執行下一步的操作,比較經典的就是synchronized關鍵字,這個關鍵字修飾的代碼塊或者方法,均需要獲取到對應的鎖,在未獲取之前,其線程的狀態就一直未BLOCKED,如果線程長時間處於這種狀態下,我們就是當心看是否出現死鎖的問題了。
4.等待(WAITING):進入該狀態的線程需要等待其他線程做出一些特定動作(通知或中斷)。
一個線程會進入這個狀態,一定是執行了如下的一些代碼,例如
Object.wait()
Thread.join()
LockSupport.park()
當一個線程執行了Object.wait()的時候,它一定在等待另一個線程執行Object.notify()或者Object.notifyAll()。
或者一個線程thread,其在主線程中被執行了thread.join()的時候,主線程即會等待該線程執行完成。當一個線程執行了LockSupport.park()的時候,其在等待執行LockSupport.unpark(thread)。當該線程處於這種等待的時候,其狀態即爲WAITING。需要關注的是,這邊的等待是沒有時間限制的,當發現有這種狀態的線程的時候,若其長時間處於這種狀態,也需要關注下程序內部有無邏輯異常。
5.超時等待(TIMED_WAITING):該狀態不同於WAITING,它可以在指定的時間後自行返回。
這個狀態和WAITING狀態的區別就是,這個狀態的等待是有一定時效的,即可以理解爲WAITING狀態等待的時間是永久的,即必須等到某個條件符合才能繼續往下走,否則線程不會被喚醒。但是TIMED_WAITING,等待一段時間之後,會喚醒線程去重新獲取鎖。
Thread.sleep(long)
Object.wait(long)
Thread.join(long)
LockSupport.parkNanos()
LockSupport.parkUntil()
1.2 Java虛擬機(JVM)簡介
https://www.cnblogs.com/hexinwei1/p/9406239.html
(1)java代碼編譯執行過程:
1. 源碼編譯:通過Java源碼編譯器將Java代碼編譯成JVM字節碼(.class文件)
2. 類加載:通過ClassLoader及其子類來完成JVM的類加載
3. 類執行:字節碼被裝入內存,進入JVM虛擬機,被解釋器解釋執行
(2)JVM在它的生存週期中有一個明確的任務,那就是運行Java程序,因此當Java程序啓動的時候,就產生JVM的一個實例;當程序運行結束的時候,該實例也跟着消失了。
1.3 JVM運行時內存劃分
https://blog.csdn.net/weixin_42762133/article/details/95735737
https://blog.csdn.net/qq_31615049/article/details/81611918
(1)堆 Java Heap
所有的對象實例以及數組都要在堆上分配,此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這裏分配內存。這一點在Java 虛擬機規範中的描述是:所有的對象實例以及數組都要在堆上分配,但是隨着JIT 編譯器的發展與逃逸分析技術的逐漸成熟,棧上分配、標量替換優化技術將會導致一些微妙的變化發生,所有的對象都分配在堆上也漸漸變得不是那麼“絕對”了。
堆是Java 虛擬機所管理的內存中最大的一塊。Java 堆是被所有線程共享的一塊內存區域,在虛擬機啓動時創建。堆中不存放基本類型和對象引用,只存放對象本身。
(2)方法區
方法區(Method Area) 與Java堆一樣,是各個線程共享的內存區域,用於存儲已被虛擬機加載的類信息,常量,靜態變量,即時編譯器編譯後的代碼等數據。
1. 靜態變量:以static修飾的類變量,類的所有實例都共享,我們只需知道,在方法區有個靜態區,靜態區專門存放靜態變量和靜態塊。
2. 類信息:類的版本、字段、方法、接口、構造函數等描述信息;
3. 常量池:類常量池、運行時常量池、字符串常量池;
在JDK1.7以前HotSpot虛擬機使用永久代(永久代是一片連續的堆空間)來實現方法區,永久代的大小在啓動JVM時可以設置一個固定值(-XX:MaxPermSize),不可變;
在JDK1.7中 存儲在永久代的部分數據就已經轉移到Java Heap或者Native memory,永久代仍存在於JDK 1.7中,並沒有完全移除。
在JDK1.8中進行了較大改動:
1. 移除了永久代(PermGen),替換爲元空間(Metaspace)。除JVM佔用的內存,剩餘的內存空間都可以被元空間(metaspace)使用;
2. 類常量池、字符串常量池和類靜態變量存在 Java 堆中;
3. 運行時常量池存在於本地內存的元空間(Metaspace)中。
(3)程序計數器
每個線程都有一個程序計算器,就是一個指針,指向方法區中的方法字節碼(下一個將要執行的指令代碼),由執行引擎讀取下一條指令,是一個非常小的內存空間,幾乎可以忽略不記。
(4)本地方法棧
Native Method Stack中登記native方法,在Execution Engine執行時加載native libraies(一個Native Method就是一個java調用非java代碼的接口)。
本地方法棧與虛擬機棧基本類似,區別在於虛擬機棧爲虛擬機執行的java方法服務,而本地方法棧則是爲Native方法服務。
(5)棧 JVM Stack
Java虛擬機棧(Java Virtual Machine Stacks) 也是線程私有的,它的生命週期與線程相同。虛擬機棧描述的是Java方法執行的內存模型:每個方法被執行的時候都會同時創建一個棧幀用於存儲局部變量表,操作棧,動態鏈接,方法出口等信息。每一個方法被調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中從入棧到出棧的過程。
局部變量表存放了編譯期可知的各種基本數據類型(Boolean, byte , char, short, int, float , long , double),對象引用(reference類型,它不等同於對象本身, 根據不同的虛擬機實現,他可能是一個指向對象起始地址的引用指針,也可能指向一個代表對象的句柄或者其他與此對象相關的位置)和returnAddress類型(指向了一條字節碼指令的地址)。
線程共享所屬進程的內存是堆內存和方法區內存,棧內存不共享,每個線程有自己的棧
1.4 變量的存儲區域
成員變量:
1、成員變量定義在類中,在整個類中都可以被訪問。
2、成員變量隨着類對象的建立而建立,隨着對象的消失而消失,存在於對象所在的堆內存中。
3、成員變量有默認初始化值。
局部變量:存在於棧內存中,作用的範圍結束,變量空間會自動釋放。局部變量沒有默認初始化值
1.5 主內存和工作內存
https://blog.csdn.net/y874961524/article/details/61617778
Java內存模型(JMM)規定了所有的共享的變量都存儲在主內存(Main Memory)中。每個線程還有自己的工作內存(Working Memory)。從抽象的角度來看,JMM定義了線程和主內存之間的抽象關係:線程之間的共享變量存儲在主內存(Main Memory)中,每個線程都有一個私有的本地內存(Local Memory),線程私有內存中存儲了該線程以讀/寫共享變量的副本。線程私有內存是JMM的一個抽象概念,並不真實存在。它涵蓋了緩存、寫緩衝區、寄存器以及其他的硬件和編譯器優化。
線程的工作內存中保存了該線程使用到的變量的主內存的副本拷貝,線程對變量的所有操作(讀取、賦值等)都必須在工作內存中進行,而不能直接讀寫主內存中的變量(volatile變量仍然有工作內存的拷貝,但是由於它特殊的操作順序性規定,所以看起來如同直接在主內存中讀寫訪問一般)。不同的線程之間也無法直接訪問對方工作內存中的變量,線程之間值的傳遞都需要通過主內存來完成。
1.6 線程安全與非線程安全:
當多個線程訪問某個類時,不管運行時環境採用何種調度方式或者這些線程將如何交替執行,並且在主調代碼中不需要任何額外的同步或者協同,這個類都能表現出正確的行爲(這個類的結果行爲都是我們設想的正確行爲),那麼就稱這個類是線程安全的。
非線程安全主要是指多個線程對同一個對象中的同一個實例變量進行操作時會出現值被更改、值不同步的情況,進而影響程序的執行流程。
線程安全就是以獲得的實例變量的值是經過同步處理的,不會出現髒讀(取到的數據其實是被更改過的)的現象。通俗點說,就是線程訪問時不產生資源衝突。
線程不安全示例AtomicIntegerTest.java
二、多線程的使用
2.1 多線程實現的三種方式:
(1)繼承Thread類,重寫run方法
Thread本質上也是實現了Runnable接口的一個實例,它代表一個線程的實例,並且啓動線程的唯一方法就是通過Thread類的start()實例方法。start()方法是一個native方法,它將啓動一個新線程,並執行run()方法。
這種方式實現多線程很簡單,通過自己的類直接extend Thread,並重寫run()方法,就可以啓動新線程並執行自己定義的run()方法。
(2)實現Runnable接口,重寫run方法
如果自己的類已經extends另一個類,就無法直接extends Thread,此時,必須實現一個Runnable接口。
爲了啓動自己的RunnableThread ,需要首先實例化一個Thread,並傳入自己的RunnableThread實例;
RunnableThread runnableThread = new RunnableThread();
new Thread(runnableThread).start();
(3)實現有返回結果的多線程
實現Callable接口,重寫call()方法,有返回值:
1.創建Callable接口的實現類,並實現call()方法,該call()方法將作爲線程執行體,並且有返回值。
2.創建Callable實現類的實例,使用FutureTask類來包裝Callable對象,該FutureTask對象封裝了該Callable對象的call()方法的返回值。(FutureTask是一個包裝器,它通過接受Callable來創建,它同時實現了Future和Runnable接口。)
3.使用FutureTask對象作爲Thread對象的target創建並啓動新線程。
4.調用FutureTask對象的get()方法來獲得子線程執行結束後的返回值
ExecutorService、Callable、Future實際上都是屬於Executor框架中的功能類。Executor框架實現的就是線程池的功能。
線程創建示例ThreadCreate.java
什麼是Native方法
https://www.jianshu.com/p/22517a150fe5
簡單地講,一個Native Method就是一個java調用非java代碼的接口。一個Native Method是這樣一個java的方法:該方法的實現由非java語言實現,比如C。這個特徵並非java所特有,很多其它的編程語言都有這一機制,比如在C++中,你可以用extern "C"告知C++編譯器去調用一個C的函數。
創建線程的三種方式的對比
1、採用實現Runnable、Callable接口的方式創建多線程時,
優勢是:
線程類只是實現了Runnable接口或Callable接口,還可以繼承其他類。
在這種方式下,多個線程可以共享同一個target對象,所以非常適合多個相同線程來處理同一份資源的情況,從而可以將CPU、代碼和數據分開,形成清晰的模型,較好地體現了面向對象的思想。
劣勢是:
編程稍微複雜,如果要訪問當前線程,則必須使用Thread.currentThread()方法。
2、使用繼承Thread類的方式創建多線程時,
優勢是:編寫簡單,如果需要訪問當前線程,則無需使用Thread.currentThread()方法,直接使用this即可獲得當前線程。
劣勢是: 線程類已經繼承了Thread類,所以不能再繼承其他父類。
3、Runnable和Callable的區別:
(1) Callable規定重寫的方法是call(),Runnable規定重寫的方法是run()。
(2) Callable的任務執行後可返回值,而Runnable的任務是不能返回值的。
(3) call方法可以拋出異常,run方法不可以。
(4) 運行Callable任務可以拿到一個Future對象,表示異步計算的結果。它提供了檢查計算是否完成的方法,以等待計算的完成,並檢索計算的結果。通過Future對象可以瞭解任務執行情況,可取消任務的執行,還可獲取執行結果。
2.2 Thread類的幾種常用方法
start(); //啓動線程
getId(); //獲得線程ID
getName(); //獲得線程名字
getPriority(); //獲得優先級(1~10共10個等級,最小爲1,最大爲10,正常爲5)
isAlive(); //判斷線程是否活動
isDaemon(); //判斷是否守護線程
getState(); //獲得線程狀態
sleep(long mill); //讓當前正在執行的線程休眠
join(); //等待線程結束
yield(); //放棄cpu使用權利
interrupt(); //中斷線程
currentThread(); //獲得正在執行的線程對象
yield():放棄當前的CPU資源,將它讓給其他任務去佔用CPU執行時間。但放棄的時間不確定,有可能剛剛放棄,馬上又獲得了CPU時間片。
interrupt():中斷僅僅是在線程對象做一個標記而已,稱爲中斷標誌。中斷標誌默認爲false,在線程 t 調用自己的 t.interrupt() 方法後,此線程中斷標誌就變成true。但是,中斷標誌爲true實際上不會對正常運行的線程產生影響,因爲正常運行的線程不會自己去檢查自己的中斷標誌。
只有那些被阻塞的線程纔會不停的檢查自己的中斷標誌,這個阻塞包括因 wait、join、yield、而進入阻塞的線程,這些被阻塞的線程如果檢查到自己的中斷標誌爲true,就會拋出InterruptException異常。
活動狀態:指線程已經啓動且尚未終止,線程處於正在運行或準備開始運行的狀態,就認爲線程是存活的。
RUNNABLE BLOCKED WAITING TIMED_WAITING
守護線程:在Java中有兩類線程:User Thread(用戶線程)、Daemon Thread(守護線程)
當前JVM實例中最後一個用戶線程結束時,守護線程就會隨着JVM一同結束工作。Daemon的作用是爲其他線程的運行提供便利服務。
demo示例ThreadMethod.java
在Java中有兩類線程:User Thread(用戶線程)、Daemon Thread(守護線程)
• 當前JVM實例中最後一個用戶線程結束時,守護線程就會隨着JVM一同結束工作。Daemon的作用是爲其他線程的運行提供便利服務,守護線程最典型的應用就是 GC (垃圾回收器)
• JVM實例的誕生:當啓動一個Java程序時,一個JVM實例就產生了,任何一個擁有public static void main(String[] args)函數的class都可以作爲JVM實例運行的起點。
任何線程都可以是“守護線程Daemon”或“用戶線程User”。他們在幾乎每個方面都是相同的,唯一的區別是判斷虛擬機何時離開:
用戶線程:Java虛擬機在它所有用戶線程離開後自動離開。
守護線程:守護線程則是用來服務用戶線程的,如果沒有其他用戶線程在運行,那麼就沒有可服務對象,也就沒有理由繼續下去。會隨着JVM一同結束。
注意:
(1)thread.setDaemon(true)必須在thread.start()之前設置,否則會拋出一個IllegalThreadStateException異常。你不能把正在運行的常規線程設置爲守護線程。
(2)在Daemon線程中產生的新線程也是Daemon的。
(3)不要認爲所有的任務都可以分配給Daemon來進行服務,比如讀寫操作或者計算邏輯。
因爲你不可能知道在所有的User完成之前,Daemon是否已經完成了預期的服務任務。一旦User退出了,可能大量數據還沒有來得及讀入或寫出,計算任務也可能多次運行結果不一樣。這對程序是毀滅性的。造成這個結果理由已經說過了:一旦所有User Thread離開了,虛擬機也就退出運行了。
(4)使用循環控制守護線程裏的run方法(run方法結束,線程也就結束了),不要讓守護線程提前於服務的用戶線程消亡。
代碼的邏輯裏如果讓守護線程提前於用戶線程消亡的情況下,守護線程並不會主動延長生命和用戶線程一起消亡。但是,代碼的邏輯讓守護線程延遲於用戶線程消亡的情況下,守護線程會提前和用戶線程一起消亡。
2.3 線程同步方法一:關鍵字sychronized
sychronized 一般用來修飾一個方法或者一個代碼塊。利用sychronized實現同步的基礎:java中每一個對象都可以作爲鎖。具體表現爲以下3中形式:
1.對於普通方法的同步,鎖是當前實例對象。
sychronized修飾普通方法的時候,鎖是當前實例對象,每個實例對象一把鎖,同一時刻只能一個線程獲得鎖。不同實例對象獲取的鎖不一樣,不需等待其他實例對象鎖的釋放。
當兩個併發線程(thread1和thread2)訪問同一個對象(test1)中的synchronized代碼時在同一時刻只能有一個線程得到執行,另一個線程受阻塞,必須等待當前線程執行完這個代碼塊以後才能執行該代碼塊。
Thread1和thread2是互斥的,因爲在執行synchronized代碼塊時會鎖定當前的對象,只有執行完該代碼塊才能釋放該對象鎖,下一個線程才能執行並鎖定該對象。
2.對於靜態方法的同步,鎖是當前類的Class對象。
sychronized修飾靜態方法的時候,鎖是當前類的Class對象,每個類對應一把鎖(此類的所有對象用的是同一把鎖),多線程不同實例對象同一時刻也只能是一個獲得鎖,其他必須等待鎖的釋放。
3.對於同步方法塊,鎖是sychronized括號裏配置的對象。
sychronized(object){ // 代碼塊 }
object爲普通變量的時候,每個實例對象一把鎖,同一時刻只能一個線程獲得鎖。不同實例對象獲取的鎖不一樣,不需等待其他實例對象鎖的釋放。
object爲靜態變量的時候,鎖是當前類的Class對象,每個類對應一把鎖,多線程不同實例對象同一時刻也只能是一個獲得鎖,其他必須等待鎖的釋放。
object爲類Class的時候,鎖是當前類的Class對象,每個類對應一把鎖,多線程不同實例對象同一時刻也只能是一個獲得鎖,其他必須等待鎖的釋放。
sychronized關鍵字總結:
A. 無論synchronized關鍵字是修飾在方法上還是代碼塊,如果它作用的對象是非靜態的,則它取得的鎖是對象,每個實例對象一把鎖;如果synchronized作用的對象是一個靜態方法或一個類,則它取得的鎖是對類,該類所有的對象同一把鎖。
B. 每個對象只有一個鎖(lock)與之相關聯,誰拿到這個鎖誰就可以運行它所控制的那段代碼。
sychronized修飾方法的注意事項:
1、接口方法時不能使用synchronized關鍵字;
2、構造方法不能使用synchronized關鍵字,但可以使用synchronized代碼塊進行同步;
(解釋:由於鎖即對象,構造函數用於創建對象,無對象何來鎖,鎖的安全性也不用顧及);
3、synchronized關鍵字無法繼承;
雖然可以使用synchronized來定義方法,但synchronized並不屬於方法定義的一部分,因此,synchronized關鍵字不能被繼承。
如果在父類中的某個方法使用了synchronized關鍵字,而在子類中覆蓋了這個方法,在子類中的這個方法默認情況下並不是同步的。
子類方法同步的解決方案:
1)子類方法也加上synchronized 關鍵字
2)子類方法中調用父類同步的方法,例如:使用 super.xxxMethod()調用父類方法
demo示例見ThreadSychronized.java
併發之原子性、可見性、有序性:
https://www.cnblogs.com/guanghe/p/9206635.html
原子性:即一個操作或者多個操作,要麼全部執行並且執行的過程不會被任何因素打斷,要麼就都不執行。
在整個操作過程中不會被線程調度器中斷的操作,都可認爲是原子性。原子性是拒絕多線程交叉操作的,不論是多核還是單核,具有原子性的量,同一時刻只能有一個線程來對它進行操作。例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作。
可見性:指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。
有序性:即程序執行的順序按照代碼的先後順序執行。
在Java內存模型中,允許編譯器和處理器對指令進行重排序,但是重排序過程不會影響到單線程程序的執行,卻會影響到多線程併發執行的正確性。
要想併發程序正確地執行,必須要保證原子性、可見性以及有序性。只要有一個沒有被保證,就有可能會導致程序運行不正確。
synchronized通過鎖機制的實現,滿足了原子性,可見性和有序性,所以synchronized能夠保證線程安全。
2.4 線程同步方法二:關鍵字volatile
線程寫volatile變量的過程:
1. 改變線程工作內存中volatile變量副本的值
2. 將改變後的副本的值從工作內存刷新到主內存
線程讀volatile變量的過程:
1. 從主內存中讀取volatile變量的最新值到線程的工作內存中
2. 從工作內存中讀取volatile變量的副本
volatile只能保證可見性和有序性(通過加入內存屏障和禁止重排序優化),不能保證原子性操作,也就不能保證線程安全。
爲何volatile不能保證原子性操作:
private volatile int i = 0; i++;
i++操作可以被拆分爲三步:
1、線程讀取i的值
2、i進行自增計算
3、刷新回i的值
某線程使用該變量時,重新從主存內讀取該變量的值,爲3,然後對其進行+1操作,此時該線程內i變量的副本值爲4。
但此時該線程的時間片時間到了,等該線程再次獲得時間片的時候,主存內a的值已經是另外的值,如5,但是該線程並不知道,該線程繼續完成其未完成的工作,將線程內的i副本的值4寫入主存。
這時,主存內i的值就是4了。這樣,之前修改i的值爲5的操作就相當於沒有發生了,i的值出現了意料之外的結果。
總結:
1.volatile本身並不處理數據的原子性,而是強制對數據的讀寫及時影響到主內存。
2.volatile主要使用的場合是在多個線程中可以感知實例變量被更改了,並且可以獲得最新的值使用,也就是用多線程讀取共享變量時可以獲得最新值使用。
3.volatile最適用一個線程寫,多個線程讀的場合(即對於一寫多讀,是可以解決變量同步問題)。如果有多個線程併發寫操作,仍然需要使用鎖或者線程安全的容器或者原子變量來代替。
demo示例見FlagThread.java
volatile如何實現內存可見性、有序性:
深入來說:通過加入內存屏障和禁止重排序優化來實現的。
對volatile變量執行寫操作時,會在寫操作後加入一條store屏障指令
對volatile變量執行讀操作時,會在讀操作前加入一條load屏障指令
2.5 線程同步方法三:使用ReentrantLock鎖
ReentrantLock類是可重入、互斥、實現了Lock接口的鎖,它與使用synchronized方法具有相同的基本行爲和語義,並且擴展了其能力。
synchronized鎖和ReentrantLock鎖的異同:
(1)synchronized是獨佔鎖,加鎖和解鎖的過程自動進行,易於操作,但不夠靈活。ReentrantLock也是獨佔鎖,加鎖和解鎖的過程需要手動進行,不易操作,但非常靈活。
(2)synchronized可重入,因爲加鎖和解鎖自動進行,不必擔心最後是否釋放鎖;ReentrantLock也可重入,但加鎖和解鎖需要手動進行,且次數需一樣,否則其他線程無法獲得鎖。
(3)synchronized不可響應中斷,一個線程獲取不到鎖就一直等着;ReentrantLock可以響應中斷。
(4)ReentrantLock還可以實現公平鎖機制。(公平鎖:在鎖上等待時間最長的線程將獲得鎖的使用權,通俗的理解就是誰排隊時間最長誰先執行獲取鎖。)
關於Lock對象和synchronized關鍵字的選擇:
(1)最好兩個都不用,使用一種java.util.concurrent包提供的機制,能夠幫助用戶處理所有與鎖相關的代碼。
(2)如果synchronized關鍵字能滿足用戶的需求,就用synchronized,因爲它能簡化代碼 。
(3)如果需要更高級的功能,就用ReentrantLock類,此時要注意及時釋放鎖,否則會出現死鎖,通常在finally代碼釋放鎖。
總而言之,ReentrantLock是一種比較“高級”的鎖,比較適合“高級”地去使用。
demo示例見ThreadReentrantLock.java
讀寫鎖ReentrantReadWriteLock
ReentrantReadWriteLock是Lock的另一種實現方式,ReentrantLock是一個排他鎖,同一時間只允許一個線程訪問,而ReentrantReadWriteLock允許多個讀線程同時訪問,但不允許寫線程和讀線程、寫線程和寫線程同時訪問。(讀讀共享、寫寫互斥、讀寫互斥、寫讀互斥)
線程進入讀鎖的前提條件:沒有其他線程的寫鎖;沒有寫請求或者有寫請求,但調用線程和持有鎖的線程是同一個。
線程進入寫鎖的前提條件:沒有其他線程的讀鎖;沒有其他線程的寫鎖。
2.6 使用線程局部變量ThreadLocal
ThreadLocal是除了加鎖這種同步方式之外的一種規避多線程訪問出現線程不安全的方法,創建一個ThreadLocal變量,那麼訪問這個變量的每個線程都會有這個變量的一個副本,在實際多線程操作的時候,操作的是自己本地內存中的變量,從而規避了線程安全問題。
ThreadLocal 實例通常是類中的 private static 字段,它們希望將狀態與某一個線程(例如,用戶 ID 或事務 ID)相關聯。
ThreadLocal與synchronized的比較:
synchronized通過對象的鎖機制保證同一時間只有一個線程訪問變量,這時該變量是多個線程共享的。 synchronized用於線程間的數據共享。
ThreadLocal會爲每一個線程提供一個獨立的變量副本,從而隔離了多個線程對數據的訪問衝突。因爲每一個線程都擁有自己的變量副本,從而也就沒有必要對該變量進行同步了。ThreadLocal則用於線程間的數據隔離。
概括起來說,對於多線程資源共享的問題,同步機制採用了“以時間換空間”的方式,而ThreadLocal採用了“以空間換時間”的方式。前者僅提供一份變量,讓不同的線程排隊訪問,而後者爲每一個線程都提供了一份變量,因此可以同時訪問而互不影響。
ThreadLocal類提供了四個方法:
get():返回此線程局部變量的當前線程副本中的值。
set(T value):將此線程局部變量的當前線程副本中的值設置爲指定值。
remove():移除此線程局部變量當前線程的值。
initialValue():返回此線程局部變量的當前線程的“初始值”,默認返回null,供子類重寫。
可以看get方法實現源碼,當map爲null時,會調用initialValue方法賦初值。比如創建ThreadLocal變量後就立即使用 get() 方法訪問變量的時候返回的就是初值。如果線程先於 get 方法調用 set(T) 方法,則不會在線程中再調用 initialValue 方法。
ThreadLocal應用場景:
1、服務器(例如tomcat)處理請求的時候,會從線程池中取一條出來進行處理請求,如果想把每個請求的用戶信息保存到一個靜態變量裏以便在處理請求過程中隨時獲取到用戶信息。這時候可以建一個攔截器,請求到來時,把用戶信息存到一個靜態ThreadLocal變量中,那麼在請求處理過程中可以隨時從靜態ThreadLocal變量獲取用戶信息。
2、Spring的事務實現也藉助了ThreadLocal類。Spring會從數據庫連接池中獲得一個connection,然會把connection放進ThreadLocal中,也就和線程綁定了,事務需要提交或者回滾,只要從ThreadLocal中拿到connection進行操作。
demo示例見ThreadLocalTest.java
2.7 線程間通信:等待/通知機制 wait/notify、notifyAll
方法wait()的作用:
使當前執行代碼的線程進行等待,該方法用來將當前線程置入“預執行隊列”中,並且在wait()所在的代碼行處停止執行,直到接到通知或被中斷爲止。在調用wait()之前,線程必須獲得該對象的對象級別鎖,即只能在同步方法或同步塊中調用wait()方法。在執行wait()方法後,當前線程釋放鎖。在從wait()返回前,線程與其他線程競爭重新獲得鎖。如果調用wait()方法時沒有持有適當的鎖,則拋出IllegalMonitorStateException異常。
方法notify()的作用:
也要在同步方法或同步塊中調用,即在調用前,線程也必須獲得該對象的對象級別鎖。如調用notify()時沒有持有適當的鎖,也會拋出IllegalMonitorStateException。該方法用來通知那些可能等待該對象的對象鎖的其他線程,如果有多個線程等待,則由線程規劃器隨機挑選出其中一個呈wait狀態的線程,對其發出通知notify,並使它等待獲取該對象的對象鎖。
需要說明的是,在執行notify()方法後,當前線程不會馬上釋放該對象鎖,呈wait狀態的線程也並不能馬上獲取該對象鎖,到等到執行notify()方法的線程將程序執行完,也就是退出synchronized代碼塊後,當前線程纔會釋放鎖,而呈wait狀態所在的線程纔可以獲取該對象鎖。
當第一個獲得了該對象鎖的wait線程運行完畢以後,它會釋放掉該對象鎖,此時如果該對象沒有再次使用notify語句,則即便該對象已經空閒,其他wait狀態等待的線程由於沒有得到該對象的通知,還會繼續阻塞在wait狀態,直到這個對象發出一個notify或notifyAll。
總結:
wait()方法可以使調用該方法的線程釋放共享資源的鎖,然後從運行狀態退出,進入等待隊列,直到被再次喚醒。
notify()方法可以隨機喚醒等待隊列中等待同一共享資源的“一個”線程,並使該線程退出等待隊列,進入可運行狀態,也就是notify()方法僅隨機通知“一個”線程。
notifyAll()方法可以使所有正在等待隊列中等待統一共享資源的“全部”線程從等待狀態退出,進入可運行狀態。此時,優先級最高的那個線程最先執行,但也有可能是隨機執行,因爲這要取決於JVM虛擬機的實現。
帶一個參數的wait(long)方法的功能是等待某一時間內是否有線程對鎖進行喚醒,如果超過這個時間則自動喚醒。
wait()方法可以被interrupt 打斷並拋出InterruptedException。遇到異常導致呈wait狀態的線程終止,鎖也會被釋放。
demo見TestWaitNotify.java
2.7 線程間通信:join方法
1、在很多情況下,主線程創建並啓動子線程,如果子線程中要進行大量的耗時計算,主線程往往將早於子線程結束之前結束。這時,如果主線程想等待子線程執行完成之後再結束,比如子線程處理一個數據,主線程要取得這個數據中的值,就要用到 join() 方法了。
2、join() 的作用是等待線程銷燬,而使當前線程進行無限期的阻塞,等待 join() 的線程銷燬後再繼續執行當前線程的代碼。join方法可以使得線程之間的並行執行變爲串行執行。在A線程中調用了B線程的join()方法時,表示只有當B線程執行完畢時,A線程才能繼續執行。
3、同樣的,join() 方法可以被 interrupt() 方法打斷並拋出 InterruptedException 異常。(是調用thread.join()的線程被中斷纔會進入異常,比如a線程調用b.join(),線程a在等待線程b結束,此時調用a. interrupt(); 線程a中就會拋出異常,不再等待了,直接進入異常處理代碼)
4、join() 方法是 wait/notify 範式的簡潔應用。當子線程調用 join() 時,主線程執行 wait() 方法進入等待,釋放鎖。子線程得到鎖,執行結束後調用 notifyAll() 方法,釋放鎖。主線程接着執行。
5、join方法中如果傳入參數,則表示這樣的意思:如果A線程中調用B線程的join(10),則表示A線程會等待B線程執行10毫秒,10毫秒過後,A、B線程並行執行。需要注意的是,jdk規定,join(0)的意思不是A線程等待B線程0毫秒,而是A線程等待B線程無限時間,直到B線程執行完畢,即join(0)等價於join()。(其實join()中調用的是join(0))
demo見TestJoin.java
2.8 死鎖以及死鎖的排查
1、什麼是死鎖?
死鎖是指兩個或兩個以上的進程(線程)在執行過程中,由於競爭資源或者由於彼此通信而造成的一種阻塞的現象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永遠在互相等待的進程(線程)稱爲死鎖進程(線程) 。
2、死鎖的根本原因:
1)是多個線程涉及到多個鎖,這些鎖存在着交叉,所以可能會導致了一個鎖依賴的閉環;
2)默認的鎖申請操作是阻塞的。
3、如何避免死鎖?
最好的是從源頭控制問題,而不是後期遇到問題再去填坑。
1)避免在對象的同步方法中調用其它對象的同步方法,那麼就可以避免死鎖產生的可能性。
兩個鎖的申請就沒有發生交叉,避免了死鎖的可能性,這是最理想的情況,因爲鎖沒有發生交叉。
2)按順序加鎖:必須存在鎖交叉的時候,在一個類中有多個方法需要同時獲得兩個對象上的鎖,那麼這些方法就必須以相同的順序獲得鎖。
3)獲取鎖時限:每個獲取鎖的時候加上時限,如果超時就放棄獲取鎖之類的。
4)在使用阻塞等待獲取鎖的方式中,必須在try代碼塊之外,並且在加鎖方法與try代碼塊之間沒有任何可能拋出異常的方法調用,避免加鎖成功後,在finally中無法解鎖。
阿里開發規約,裏面有對避免死鎖的說明,具體如下:
【強制】對多個資源、數據庫表、對象同時加鎖時,需要保持一致的加鎖順序,否則可能會 造成死鎖。 說明:線程一需要對錶 A、B、C 依次全部加鎖後纔可以進行更新操作,那麼線程二的加鎖順序也必須是 A、B、C,否則可能出現死鎖。
4、死鎖的排查(jstack、jcmd)
jdk自帶的工具包中提供的命令
1)查看當前機器上所有的 jvm 進程信息
jcmd 導出堆、查看Java進程、導出線程信息、執行GC、還可以進行採樣分析(jmc 工具的飛行記錄器)。
jcmd -l
jps 列出系統中所有的 Java 應用程序
2)使用jstack 或者 jcmd:打印指定Java進程的線程堆棧跟蹤信息。
使用jstack -l pid或者jcmd pid Thread.print可以查看當前應用的進程信息,如果有死鎖也會分析出來。
Linux系統中的定位:
通過top命令定位到cpu佔用率較高的線程之後,繼續使用jstack pid命令查看當前java進程的堆棧狀態:
1、通過top命令查看各個進程的cpu使用情況,默認按cpu使用率排序。
2、佔用了較多的cpu資源Java進程的pid。
3、通過top -Hp pid可以查看該進程下各個線程的cpu使用情況。
4、查看佔了較多的cpu資源線程的pid,利用jstack命令繼續查看該線程當前的堆棧狀態。
死鎖示例SynchronizedDeadLock.java
concurrent並發包
3.1 java.util.concurrent併發包介紹
concurrent 併發包裏有很多內容,可查看下面鏈接瞭解詳細內容。
https://blog.csdn.net/lfj19941225/article/details/88549965
這裏主要介紹以下幾個常用類:
atomic包 原子操作類
BlockingQueue 阻塞隊列
ConcurrentMap 併發 Map
locks Lock、ReentrantLock、ReentrantReadWriteLock
Executor框架 ThreadPoolExecutor是線程池的核心實現類,用來執行被提交的任務。
3.2 原子操作類
1. 原子操作類介紹
在併發編程中很容易出現併發安全的問題,有一個很簡單的例子就是多線程更新變量i=1,比如多個線程執行i++操作,就有可能獲取不到正確的值,而這個問題,最常用的方法是通過Synchronized進行控制來達到線程安全的目的。但是由於synchronized是採用的是悲觀鎖策略,並不是特別高效的一種解決方案。
實際上,在J.U.C下的atomic包提供了一系列的操作簡單,性能高效,並能保證線程安全的類去更新基本類型變量,數組元素,引用類型以及更新對象中的字段類型。atomic包下的這些類都是採用的是樂觀鎖策略去原子更新數據,在java中則是使用CAS操作具體實現。
悲觀鎖:顧名思義,就是很悲觀,每次去拿數據的時候都認爲別人會修改,所以每次在拿數據的時候都會上鎖,這樣別人想拿這個數據就會block直到它拿到鎖。
樂觀鎖:顧名思義,就是很樂觀,每次去拿數據的時候都認爲別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據。樂觀鎖適用於多讀的應用類型,這樣可以提高吞吐量。
2. CAS操作
https://blog.csdn.net/qq_32998153/article/details/79529704
使用鎖時,線程獲取鎖是一種悲觀鎖策略,即假設每一次執行臨界區代碼都會產生衝突,所以當前線程獲取到鎖的時候同時也會阻塞其他線程獲取該鎖。而CAS操作是一種樂觀鎖策略,它假設所有線程訪問共享資源的時候不會出現衝突,既然不會出現衝突自然而然就不會阻塞其他線程的操作。因此,線程就不會出現阻塞停頓的狀態。那麼,如果出現衝突了怎麼辦?無鎖操作是使用CAS(compare and swap)又叫做比較並替換來鑑別線程是否出現衝突,出現衝突就重試當前操作直到沒有衝突爲止。
CAS比較交換的過程可以通俗的理解爲CAS(V,O,N),包含三個值分別爲:V 內存地址存放的實際值;O 預期的值(舊值);N 更新的新值。當V和O相同時,也就是說舊值和內存中實際的值相同表明該值沒有被其他線程更改過,即該舊值O就是目前來說最新的值了,自然而然可以將新值N賦值給V。反之,V和O不相同,表明該值已經被其他線程改過了則該舊值O不是最新版本的值了,所以不能將新值N賦給V,返回V即可。當多個線程使用CAS操作一個變量是,只有一個線程會成功,併成功更新,其餘會失敗。失敗的線程會重新嘗試(重新獲取主內存變量最新值,重新執行對變量的操作,重新嘗試更新)。
CAS的缺點:
1、 CPU開銷過大:
在併發量比較高的情況下,如果許多線程反覆嘗試更新某一個變量,卻又一直更新不成功,循環往復,會給CPU帶來很到的壓力。
2、不能保證代碼塊的原子性:
CAS機制所保證的知識一個變量的原子性操作,而不能保證整個代碼塊的原子性。比如需要保證3個變量共同進行原子性的更新,就不得不使用synchronized了。
3、 ABA問題:
這是CAS機制最大的問題所在。
3. 原子類的使用
atomic包提供原子更新基本類型的工具類,主要有這些:
1. AtomicBoolean:以原子更新的方式更新boolean;
2. AtomicInteger:以原子更新的方式更新Integer;
3. AtomicLong:以原子更新的方式更新Long;
這幾個類的用法基本一致,這裏以AtomicInteger爲例總結常用的方法:
1. addAndGet(int delta) :以原子方式將輸入的數值與實例中原本的值相加,並返回最後的結果;
2. incrementAndGet() :以原子的方式將實例中的原值進行加1操作,並返回最終相加後的結果;
3. getAndSet(int newValue):將實例中的值更新爲新值,並返回舊值;
4. getAndIncrement():以原子的方式將實例中的原值加1,返回的是自增前的舊值;
使用示例AtomicIntegerTest.java
3.3 ConcurrentHashMap
ConcurrentHashMap從JDK1.5開始隨java.util.concurrent包一起引入JDK中,主要爲了解決HashMap線程不安全和Hashtable效率不高的問題。HashMap在多線程編程中是線程不安全的,而Hashtable由於使用了synchronized修飾方法而導致執行效率不高;synchronized關鍵字加鎖是對整個對象進行加鎖,也就是說在進行put等修改Hash表的操作時,鎖住了整個Hash表,從而使得其表現的效率低下;
ConcurrentHashMap 能夠提供比 HashTable 更好的併發性能。在你從中讀取對象的時候 ConcurrentHashMap 並不會把整個 Map 鎖住。此外,在你向其中寫入對象的時候,ConcurrentHashMap 也不會鎖住整個 Map。
在JDK1.7之前的ConcurrentHashMap使用分段鎖機制實現(即將整個Hash表劃分爲多個分段。使用ReetrantLock對每個分段進行加鎖),JDK1.8則使用數組+鏈表+紅黑樹數據結構和CAS原子操作實現ConcurrentHashMap;
使用示例:ConcurrentHashMapTest.java
Java 8系列之重新認識HashMap
https://cloud.tencent.com/developer/article/1343130
3.4 BlockingQueue 阻塞隊列
阻塞隊列,顧名思義,首先它是一個隊列,通過一個共享的隊列,可以使得數據由隊列的一端輸入,從另外一端輸出;當隊列滿時,插入阻塞;當隊列爲空時,刪除(取出)阻塞。多線程環境中,通過隊列可以很容易實現數據共享,比如經典的“生產者”和“消費者”模型中,
一個線程將會持續生產新對象並將其插入到隊列之中,直到隊列達到它所能容納的臨界點。也就是說,它是有限的。如果該阻塞隊列到達了其臨界點,負責生產的線程將會在往裏邊插入新對象時發生阻塞。它會一直處於阻塞之中,直到負責消費的線程從隊列中拿走一個對象。
負責消費的線程將會一直從該阻塞隊列中拿出對象。如果消費線程嘗試去從一個空的隊列中提取對象的話,這個消費線程將會處於阻塞之中,直到一個生產線程把一個對象丟進隊列。
在concurrent包發佈以前,在多線程環境下,我們都必須去自己控制這些細節,尤其還要兼顧效率和線程安全。這也是我們在多線程環境下,爲什麼需要BlockingQueue的原因。作爲BlockingQueue的使用者,我們再也不需要關心什麼時候需要阻塞線程,什麼時候需要喚醒線程,因爲這一切BlockingQueue都給你一手包辦了。
BlockingQueue 是個接口,常用的實現有:
ArrayBlockingQueue:基於數組實現的阻塞隊列,有界。
LinkedBlockingQueue:基於鏈表實現的阻塞隊列,可以當做無界隊列,也可以當做有界隊列來使用。
如果沒有指定其容量大小,會默認一個類似無限大小的容量(Integer.MAX_VALUE—int型的 0x7fffffff)
PriorityBlockingQueue:一個支持優先級排序的無界阻塞隊列。
默認情況下元素採用自然順序升序排序,也可以通過構造函數來指定比較器Comparator來對元素進行排序。
DelayQueue:延遲阻塞隊列、底層是基於優先級隊列PriorityQueue來實現的、無界。
只有在延遲期滿時才能從中提取元素;應用場景:店鋪取票後五分鐘內沒有出票,即收回所取的票。
SynchronousQueue:同步隊列。沒有容量,是無緩衝等待隊列,是一個不存儲元素的阻塞隊列。
直接將任務交給消費者,必須等隊列中的添加元素被消費後才能繼續添加新的元素。每個插入操作必須等待另一個操作執行相應的刪除操作線程,反之亦然。同步隊列沒有任何內部容量,甚至沒有一個容量。
常用的隊列主要有以下兩種:
先進先出(FIFO):先插入的隊列的元素也最先出隊列,類似於排隊的功能。從某種程度上來說這種隊列也體現了一種公平性。
後進先出(LIFO):後插入隊列的元素最先出隊列,這種隊列優先處理最近發生的事件。
DelayQueue是一個無界阻塞隊列,用於放置實現了Delayed接口的對象,只有在延遲期滿時才能從中提取元素。該隊列時有序的,即隊列的頭部是延遲期滿後保存時間最長的Delayed 元素。注意:不能將null元素放置到這種隊列中。
PriorityBlockingQueue是一個支持優先級的無界阻塞隊列。默認情況下元素採用自然順序升序排序,當然我們也可以通過構造函數來指定Comparator來對元素進行排序。需要注意的是PriorityBlockingQueue不能保證同優先級元素的順序。
BlockingQueue的方法
BlockingQueue 具有 4 組不同的方法用於插入、移除以及對隊列中的元素進行檢查。如果請求的操作不能得到立即執行的話,每個方法的表現也不同。這些方法如下:
四組不同的行爲方式解釋:
拋異常:如果嘗試的操作無法立即執行,拋一個異常。
特定值:如果嘗試的操作無法立即執行,返回一個特定的值(true / false)。
阻塞:如果嘗試的操作無法立即執行,該方法調用將會發生阻塞,直到能夠執行。
超時:如果嘗試的操作無法立即執行,該方法調用將會發生阻塞,直到能夠執行,但等待時間不會超過給定值。返回一個特定值以告知該操作是否成功(true / false)。
無法向一個 BlockingQueue 中插入 null。如果你試圖插入 null,BlockingQueue 將會拋出一個 NullPointerException。
add: 內部實際上獲取的offer方法,當Queue已經滿了時,拋出一個異常。不會阻塞。
offer:當Queue已經滿了時,返回false。不會阻塞。
put:當Queue已經滿了時,會進入等待,只要不被中斷,就會插入數據到隊列中。會阻塞,可以響應中斷。
取出方法中 remove和add相互對應。也就是說,調用remove方法時,假如對列爲空,則拋出一場。poll與offer相互對應。take和put相互對應。
element() 和 peek()都是用來返回隊列的頭元素,不刪除。
在隊列元素爲空的情況下,element() 方法會拋出NoSuchElementException異常,peek() 方法只會返回 null。
使用示例見BlockingQueueTest.java
ArrayBlockingQueue 一個由數組結構組成的有界阻塞隊列:
基於數組的阻塞隊列實現,在ArrayBlockingQueue內部,維護了一個定長數組,以便緩存隊列中的數據對象,其內部沒有實現讀寫分離(都是共用同一個鎖對象),長度是需要定義的,按照先進先出(FIFO)的原則對元素進行排序,是有界隊列(bounded)。
LinkedBlockingQueue 一個由鏈表結構組成的有界阻塞隊列:
是一個用鏈表實現的有界阻塞隊列。此隊列的默認和最大長度爲Integer.MAX_VALUE。此隊列按照先進先出的原則對元素進行排序。
需要注意的是:如果沒有指定其容量大小,LinkedBlockingQueue會默認一個類似無限大小的容量(Integer.MAX_VALUE—int型的0x7fffffff),這樣的話,如果生產者的速度一旦大於消費者的速度,也許還沒有等到隊列滿阻塞產生,系統內存就有可能已被消耗殆盡了。
LinkedBlockingQueue之所以能夠高效的處理併發數據,還因爲其對於生產者端和消費者端分別採用了獨立的鎖來控制數據同步,這也意味着在高併發的情況下生產者和消費者可以並行地操作隊列中的數據,以此來提高整個隊列的併發性能。但在線程數量很大時其性能的可預見性低於ArrayBlockingQueue.
二者不同點:
1、鎖機制不同
LinkedBlockingQueue中的鎖是分離的,生產者的鎖PutLock,消費者的鎖takeLock
而ArrayBlockingQueue生產者和消費者使用的是同一把鎖;
Doug Lea之所以沒這樣去做,也許是因爲ArrayBlockingQueue的數據寫入和獲取操作已經足夠輕巧,以至於引入獨立的鎖機制,除了給代碼帶來額外的複雜性外,其在性能上完全佔不到任何便宜。
2、底層實現機制也不同
LinkedBlockingQueue內部維護的是一個鏈表結構。在生產和消費的時候,需要創建Node對象進行插入或移除,大批量數據的系統中,其對於GC的壓力會比較大。
而ArrayBlockingQueue內部維護了一個數組。在生產和消費的時候,是直接將枚舉對象插入或移除的,不會產生或銷燬任何額外的對象實例。
3、構造時候的區別
LinkedBlockingQueue有默認的容量大小爲:Integer.MAX_VALUE,當然也可以傳入指定的容量大小。當添加速度大於移除速度時,在大小爲默認的MAX_VALUE的情況下,可能會造成內存溢出等問題。
ArrayBlockingQueue在初始化的時候,必須傳入一個容量大小的值。
個人感覺大多數場景適合使用LinkedBlockingQueue。優勢:讀寫分離;需要的時候纔會創建一個Node節點。
3.5 Executor框架
在Java中使用線程來異步執行任務。Java線程的創建與銷燬需要一定的開銷,如果我們爲每一個任務創建一個新線程來執行,這些線程的創建與銷燬將消耗大量的計算資源。
單個的線程既是工作單元也是執行機制,從JDK1.5開始,爲了把工作單元與執行機制分離開,Executor框架誕生了,他是一個用於統一創建與運行的接口。Executor框架實現的就是線程池的功能。線程池就是線程的集合,線程池集中管理線程,以實現線程的重用,降低資源消耗,提高響應速度等。
在Executor框架中,將工作單元與執行機制分離開來。Runnable和Callable是工作單元(也就是俗稱的任務),而執行機制由Executor來提供。這樣一來Executor可以理解爲基於生產者消費者模式的,提交任務的操作相當於生成者,執行任務的線程相當於消費者。
Executor框架包括三大部分:
(1)任務。也就是工作單元,包括被執行任務需要實現的接口:Runnable接口或者Callable接口;
(2)任務的執行。也就是把任務分派給多個線程的執行機制,包括Executor接口及繼承自Executor接口的ExecutorService接口。
(3)異步計算的結果。包括Future接口及實現了Future接口的FutureTask類。
Executor框架的成員及其關係可以用右邊的關係圖表示:
使用步驟示例:
(1)創建Runnable並重寫run()方法或者Callable對象並重寫call()方法;
(2)創建Executor接口的實現類ThreadPoolExecutor類或者ScheduledThreadPoolExecutor類的對象,然後調用其execute()方法或者submit()方法把工作任務添加到線程中,如果有返回值則返回Future對象。其中Callable對象有返回值,因此使用submit()方法;
而Runnable可以使用execute()方法,此外還可以使用submit()方法。只要使用callable(Runnable task)或者callable(Runnable task, Object result)方法把Runnable對象包裝起來就可以,使用callable(Runnable task)方法返回的null,使用callable(Runnable task, Object result)方法返回result。
ThreadPoolExecutor tpe = new ThreadPoolExecutor(5, 10, 100, MILLISECONDS, new ArrayBlockingQueue<Runnable>(5));
Future<String> future = tpe.submit(new callableTest());
(3)調用Future對象的get()方法後的返回值,或者調用Future對象的cancel()方法取消當前線程的執行。最後關閉線程池。
try {
System.out.println(future.get());
} catch (Exception e) {
e.printStackTrace();
} finally {
tpe.shutdown();
}
demo示例見CallableDemo.java
Executor框架內容
1、Executor接口和ExecutorService接口
Executor:一個接口,其定義了一個接收Runnable對象的方法executor,其方法簽名爲executor(Runnable command),該方法接收一個Runable實例,它用來執行一個任務,任務即爲一個實現了Runnable接口的類。
ExecutorService:是一個比Executor使用更廣泛的子類接口,其提供了生命週期管理的方法,返回 Future 對象,以及可跟蹤一個或多個異步任務執行狀況返回Future的方法;可以調用ExecutorService的shutdown()方法來平滑地關閉 ExecutorService,調用該方法後,將導致ExecutorService停止接受任何新的任務且等待已經提交的任務執行完成(已經提交的任務會分兩類:一類是已經在執行的,另一類是還沒有開始執行的),當所有已經提交的任務執行完畢後將會關閉ExecutorService。因此我們一般用該接口來實現和管理多線程。
通過 ExecutorService.submit() 方法返回的 Future 對象,可以調用isDone()方法查詢Future是否已經完成。當任務完成時,它具有一個結果,你可以調用get()方法來獲取該結果。你也可以不用isDone()進行檢查就直接調用get()獲取結果,在這種情況下,get()將阻塞,直至結果準備就緒,還可以取消任務的執行。Future 提供了 cancel() 方法用來試圖取消任務的執行。
簡單工廠模式以及其他設計模式
https://www.jianshu.com/p/e55fbddc071c
2、Executors類:主要用於提供線程池相關的操作
Executors類,提供了一系列工廠方法用於創建線程池,返回的線程池都實現了ExecutorService接口。
• newSingleThreadExecutor():創建一個單線程的線程池。這個線程池只有一個線程在工作,也就是相當於單線程串行執行所有任務。如果這個唯一的線程因爲異常結束,那麼會有一個新的線程來替代它。此線程池保證所有任務的執行順序按照任務的提交順序執行。
• newFixedThreadPool(int Threads) :創建固定大小的線程池。每次提交一個任務就創建一個線程,直到線程達到線程池的最大大小。線程池的大小一旦達到最大值就會保持不變,如果某個線程因爲執行異常而結束,那麼線程池會補充一個新線程。
• newCachedThreadPool():創建一個可緩存的線程池。如果線程池的大小超過了處理任務所需要的線程,那麼就會回收部分空閒(60秒不執行任務)的線程,當任務數增加時,此線程池又可以智能的添加新線程來處理任務。此線程池不會對線程池大小做限制,線程池大小完全依賴於操作系統(或者說JVM)能夠創建的最大線程大小。
• newScheduledThreadPool (int corePoolSize) :創建一個支持定時及週期性的任務執行的線程池。
雖然Executors利用工廠模式向我們提供了4種線程池實現方式,但是並不推薦使用。
1)newFixedThreadPool 和 newSingleThreadExecutor
傳入的最後一個參數阻塞隊列 ”workQueue“,默認的長度是INTEGER.MAX_VALUE,而它們允許的最大線程數量又是有限的,所以當請求線程的任務過多線程不夠用時,它們會在隊列中等待,又因爲隊列的長度特別長,所以可能會堆積大量的請求,從而導致內存不足(Out Of Memory)。
2)newCachedThreadPool 和 newScheduledThreadPool
它們的阻塞隊列長度有限,但是傳入的第二個參數maximumPoolSize 爲Integer.MAX_VALUE,這就意味着當請求線程的任務過多線程不夠而且隊列也滿了的時候,線程池就會創建新的線程,因爲它允許的最大線程數量是相當大的,所以可能會創建大量線程,導致OOM。
阿裏的 Java開發手冊,上面有線程池的一個建議:
【強制】線程池不允許使用 Executors 去創建,而是通過 ThreadPoolExecutor 的方式,這樣的處理方式讓寫的同學更加明確線程池的運行規則,規避資源耗盡的風險。
3、Executor、ExecutorService、Executors
這三者均是 Executor 框架中的一部分,總結一下這三者間的區別:
1)Executor 和 ExecutorService 這兩個接口主要的區別是:ExecutorService 接口繼承了 Executor 接口,是 Executor 的子接口。
2)Executor 和 ExecutorService 第二個區別是:Executor 接口定義了 execute()方法用來接收一個Runnable接口的對象,而 ExecutorService 接口中的 submit()方法可以接受Runnable和Callable接口的對象。
3)Executor 和 ExecutorService 接口第三個區別是 Executor 中的 execute() 方法不返回任何結果,而 ExecutorService 中的 submit()方法可以通過一個 Future 對象返回運算結果。
4)Executor 和 ExecutorService 接口第四個區別是除了允許客戶端提交一個任務,ExecutorService 還提供用來控制線程池的方法。比如:調用 shutDown() 方法終止線程池。
5)Executors 類提供工廠方法用來創建不同類型的線程池。
4、使用Executor比直接new Thread()的優點:
Java中的線程池是運用場景最多的併發框架,幾乎所有需要異步或併發執行任務的程序都可以使用線程池。在開發過程中,合理地使用線程池能夠帶來3個好處。
第一:降低資源消耗。通過重複利用已創建的線程降低線程創建和銷燬造成的消耗。
第二:提高響應速度。當任務到達時,任務可以不需要等到線程創建就能立即執行。
第三:提高線程的可管理性。線程是稀缺資源,如果無限制地創建,不僅會消耗系統資源, 還會降低系統的穩定性,使用線程池可以進行統一分配、調優和監控。
線程池 -- ThreadPoolExecutor
線程池,其實就是一個容納多個線程的容器,其中的線程可以反覆使用,省去了頻繁創建線程對象的操作,無需反覆創建線程而消耗過多資源。需要的時候從池中獲取線程不用自行創建,使用完畢不需要銷燬線程而是放回池中,從而減少創建和銷燬線程對象的開銷。
ThreadPoolExecutor是Executor接口的一個重要的實現類,是線程池的具體實現,用來執行被提交的任務。
ThreadPoolExecutor定義了很多構造函數,以其中一個爲例:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
1、corePoolSize:必需參數,線程池中所保存的核心線程數,包括空閒線程。在創建了線程池之後,默認情況下線程池中沒有任何線程。當提交一個任務到線程池時,線程池會創建一個線程來執行任務,即使其他空閒的基本線程能夠執行新任務也會創建線程,等到需要執行的任務數大於線程池核心線程數時就不再創建。
2、maximumPoolSize:必需參數,線程池中允許的最大線程數,這個參數表示了線程池中最多能創建的線程數量,當任務數量比corePoolSize大時,任務添加到workQueue,當workQueue滿了,將繼續創建線程以處理任務,maximumPoolSize表示的就是wordQueue滿了,線程池中最多可以創建的線程數量。
3、keepAliveTime:必需參數,線程池中的空閒線程所能持續的最長時間。當線程池中的線程數大於corePoolSize時,keepAliveTime爲多餘的空閒線程等待新任務的最長時間,超過這個時間後多餘的線程將被終止。
4、unit:必需參數, keepAliveTime持續時間單位。天、小時、分鐘、秒、毫秒、微秒、納秒。
5、workQueue:必需參數,任務執行前保存等待執行的任務的隊列。
6、threadFactory:非必需參數,不設置此參數會採用內置默認參數(defaultThreadFactory()方法創建屬於同一個ThreadGroup對象的基本線程對象)。也可以自己定義線程工廠,重新實現ThreadFactory中newThread (Runnable)方法,設置屬性等.
7、handler:非必需參數,不設置此參數會採用內置默認參數,設置飽和策略,當隊列和線程池都滿了,說明線程池處於飽和狀態,那麼必須採取一種策略處理提交的新任務。
RejectedExecutionHandler 的實現類在ThreadPoolExecutor中有四個靜態內部類,這個策略默認情況下是AbortPolicy,表示無法處理新任務時拋出異常。共有4中策略:包括AbortPolicy(丟棄任務並拋出RejectedExecutionException異常)、 DiscardPolicy(丟棄任務,但是不拋出異常) 、DiscardOldestPolicy(丟棄執行隊列中最老的任務,嘗試爲當前提交的任務騰出位置)、 CallerRunsPolicy(由調用者所在線程來運行任務)。也可以根據應用場景需要來實現RejectedExecutionHandler接口自定義策略。
當提交一個任務給線程池之後,線程池的處理流程:
1、如果線程池中的線程數量少於corePoolSize,即使線程池中有空閒線程,也會創建一個新的線程來執行新添加的任務;
2、如果線程池中的線程數量大於等於corePoolSize(核心線程池裏的線程都在執行任務),但緩衝隊列workQueue未滿,則將新添加的任務放到workQueue中,按照FIFO的原則依次等待執行(線程池中有線程空閒出來後依次將緩衝隊列中的任務交付給空閒的線程執行);
3、如果線程池中的線程數量大於等於corePoolSize,且緩衝隊列workQueue已滿,但線程池中的線程數量小於maximumPoolSize,則會創建新的線程來處理被添加的任務;
4、如果線程池中的線程數量等於了maximumPoolSize,有4種處理方式。
總結起來,也即是說:
當有新的任務要處理時,先看線程池中的線程數量是否大於corePoolSize,再看緩衝隊列workQueue是否滿,最後看線程池中的線程數量是否大於maximumPoolSize。
另外,當線程池中的線程數量大於corePoolSize時,如果裏面有線程的空閒時間超過了keepAliveTime,就將其移除線程池,這樣,可以動態地調整線程池中線程的數量。
線程池 -- ThreadPoolExecutor
這些參數中,比較容易引起問題的有corePoolSize, maximumPoolSize, workQueue以及handler:
1、corePoolSize和maximumPoolSize設置不當會影響效率,甚至耗盡線程;
2、workQueue設置不當容易導致OutOfMemory(內存不足);
3、handler設置不當會導致提交任務時拋出異常。
workQueue如果使用無界隊列會帶來如下影響:
1、當線程池中的線程數達到corePoolSize後,新任務將在無界隊列中等待,因此線程池中的線程數不會超過corePoolSize。
2、由於1,使用無界隊列時maximumPoolSize將是一個無效參數。
3、由於1和2,使用無界隊列時keepAliveTime將是一個無效參數。
線程池使用注意點:
避免使用無界隊列,不要使用Executors.newXXXThreadPool()快捷方法創建線程池,因爲這種方式會使用無界的任務隊列,爲避免OOM,我們應該使用ThreadPoolExecutor的構造方法手動指定隊列的最大長度。
合理配置線程池:
CPU密集:
CPU密集的意思是該任務需要大量的運算(如對視頻進行高清解碼等,全靠CPU的運算能力),而沒有阻塞,CPU一直全速運行。CPU密集任務只有在真正的多核CPU上纔可能得到加速(通過多線程),而在單核CPU上,無論你開幾個模擬的多線程,該任務都不可能得到加速,因爲CPU總的運算能力就那些。
IO密集:
IO密集型,即該任務需要大量的IO,即大量的阻塞(CPU等待硬盤、內存、網絡等讀寫操作)。在單線程上運行IO密集型的任務會導致浪費大量的CPU運算能力浪費在等待。所以在IO密集型任務中使用多線程可以大大的加速程序運行,即時在單核CPU上,這種加速主要就是利用了被浪費掉的阻塞時間。
要想合理的配置線程池的大小,首先得分析任務的特性,可以從以下幾個角度分析:
1. 任務的性質:CPU密集型任務、IO密集型任務、混合型任務。
2. 任務的優先級:高、中、低。
3. 任務的執行時間:長、中、短。
4. 任務的依賴性:是否依賴其他系統資源,如數據庫連接等。
性質不同的任務可以交給不同規模的線程池執行。
對於不同性質的任務來說,CPU密集型任務應配置儘可能小的線程,如配置CPU個數+1的線程數。IO密集型任務應配置儘可能多的線程,因爲IO操作不佔用CPU,不要讓CPU閒下來,應加大線程數量,如配置兩倍CPU個數+1。而對於混合型的任務,如果可以拆分,拆分成IO密集型和CPU密集型分別處理,前提是兩者運行的時間是差不多的,如果處理時間相差很大,則沒必要拆分了。
若任務對其他系統資源有依賴,如某個任務依賴數據庫的連接返回的結果,這時候等待的時間越長,則CPU空閒的時間越長,那麼線程數量應設置得越大,才能更好的利用CPU。
可以總結爲:
線程等待時間所佔比例越高,需要越多線程。線程CPU時間所佔比例越高,需要越少線程。
CPU密集型時,任務可以少配置線程數,大概和機器的CPU核數相當,這樣可以使得每個線程都在執行任務。
IO密集型時,大部分線程都阻塞,故需要多配置線程數,2*CPU核數。
獲取CPU的核數
private static final int corePoolSize = Runtime.getRuntime().availableProcessors();
監控線程池運行狀態:
使用線程池,則有必要對線程池進行監控,方便在出現問題時,如線程池阻塞,無法提交新任務等。就可以根據線程池的使用狀況快速定位問題。
可以通過線程池執行類ThreadPoolExecutor提供的方法獲取到相關的屬性去監控線程池的當前狀態:
getTaskCount():線程池需要執行的任務數量(排隊任務數+活動線程數+已執行完成的任務數)。
getQueue().size():當前排隊的任務數。
getCompletedTaskCount():線程池在運行過程中已完成的任務數量,小於或等於taskCount。
getActiveCount():線程池中活動的線程數(正在執行任務)。
getLargestPoolSize():線程池裏曾經創建過的最大線程數量,通過該值可以判斷線程池是否曾經滿過。如該數值等於線程池的最大大小,則表示線程池曾經滿過;
getPoolSize():線程池當前的線程數量;
還可以通過擴展線程池進行監控。可以通過繼承線程池來自定義線程池,重寫線程池的 beforeExecute、afterExecute和terminated方法,也可以在任務執行前、執行後和線程池關閉前執行一些代碼來進行監控。例如監控任務的平均執行時間、最大執行時間和最小執行時間等。 這幾個方法在線程池裏是空方法。
demo示例見ThreadPoolTest.java
ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor類繼承了ThreadPoolExecutor並實現了ScheduledExecutorService接口。主要用於在給定的延遲後執行任務或者定期執行任務。作用類似於java.util包下的Timer定時器類,但是比Timer功能更強大、更靈活,因爲Timer只能控制單個線程延遲或定期執行,而ScheduledThreadPoolExecutor對應的是多個線程的後臺線程。
調度線程池主要用於定時器或者延遲一定時間在執行任務時候使用。內部使用優化的DelayQueue(DelayedWorkQueue)來實現,由於使用隊列來實現定時器,有出入隊調整堆等操作,所以定時並不是非常非常精確。
demo示例見ScheduledThreadPoolTest.java
Future接口
JDK5新增了Future接口,用於描述一個異步計算的結果。Future是一個接口,他提供給了我們方法來檢測當前的任務是否已經結束,還可以等待任務結束並且拿到一個結果,通過調用Future的get()方法可以當任務結束後返回一個結果值,如果工作沒有結束,則會阻塞當前線程,直到任務執行完畢。
可以通過調用cancel()方法來試圖停止一個任務,如果停止成功,則cancel ()方法會返回true;如果任務已經完成或者已經停止了或者這個任務無法停止,則cancel()會返回一個false。isDone()和isCancelled()方法可以判斷當前工作是否完成和是否取消。
Future通常和線程池搭配使用,用來獲取線程池返回執行後的返回值。我們假設通過Executors工廠方法構建一個線程池es ,es要執行某個任務有兩種方式,一種是執行 es.execute(runnable) ,這種情況是沒有返回值的; 另外一種情況是執行 es.submit(runnale)或者 es.submit(callable) ,這種情況會返回一個Future的對象,然後調用Future的get()來獲取返回值。
也就是說Future提供了三種功能:
1)判斷任務是否完成;
2)能夠中斷任務;
3)能夠獲取任務執行結果。
FutureTask類
因爲Future只是一個接口,所以是無法直接用來創建對象使用的,因此就有了FutureTask類。FutureTask類實現了RunnableFuture接口。而RunnableFuture繼承了Runnable接口和Future接口。
FutureTask實現了Runnable,因此它既可以傳遞給Thread對象執行,也可以提交給ExecuteService來執行。
FutureTask實現了Futrue可以直接通過get()函數獲取執行結果,該函數會阻塞,直到結果返回。
FutureTask是爲了彌補Thread的不足而設計的,它可以讓程序員準確地知道線程什麼時候執行完成並獲得到線程執行完成後返回的結果(如果有需要)。
FutureTask是一種可以取消的異步的計算任務。它的計算是通過Callable實現的,它等價於可以攜帶結果的Runnable,並且有三個狀態:等待、運行和完成。完成包括所有計算以任意的方式結束,包括正常結束、取消和異常。
demo示例見CallableDemo.java
方法解析:
V get() :獲取異步執行的結果,如果沒有結果可用,此方法會阻塞直到異步計算完成。
V get(Long timeout , TimeUnit unit) :獲取異步執行結果,如果沒有結果可用,此方法會阻塞,但是會有時間限制,如果阻塞時間超過設定的timeout時間,該方法將拋出異常。
boolean isDone() :如果任務執行結束,無論是正常結束或是中途取消還是發生異常,都返回true。
boolean isCancelled() :任務是否被取消成功,如果在任務正常完成前被取消成功,則返回 true。
boolean cancel(boolean mayInterruptRunning) :如果任務還沒開始,執行cancel(...)方法將返回false;如果任務已經啓動,執行cancel(true)方法將以中斷執行此任務線程的方式來試圖停止任務,如果停止成功,返回true;當任務已經啓動,執行cancel(false)方法將不會對正在執行的任務線程產生影響(讓線程正常執行到完成),此時返回false;當任務已經完成,執行cancel(...)方法將返回false。mayInterruptRunning參數表示是否中斷執行中的線程。
Future模式之CompletableFuture – JDK8
Future 接口的侷限性:
雖然 Future 以及相關使用方法提供了異步執行任務的能力,但是對於結果的獲取卻是很不方便,只能通過阻塞get()或者輪詢isDone()的方式得到任務的結果。阻塞的方式顯然和我們的異步編程的初衷相違背,輪詢的方式又會耗費無謂的 CPU 資源,而且也不能及時地得到計算結果。而且它很難直接表述多個Future 結果之間的依賴性。
實際開發中,經常需要達成以下目的:
1、將兩個異步計算合併爲一個——這兩個異步計算之間相互獨立,同時第二個又依賴於第一個的結果。
2、等待 Future 集合中的所有任務都完成。
3、僅等待 Future集合中最快結束的任務完成(有可能因爲它們試圖通過不同的方式計算同一個值),並返回它的結果。
4、通過編程方式完成一個Future任務的執行(即以手工設定異步操作結果的方式)。
5、應對 Future 的完成事件(即當 Future 的完成事件發生時會收到通知,並能使用 Future 計算的結果進行下一步的操作,不只是簡單地阻塞等待操作的結果)
CompletableFuture:
CompletableFuture 是Java 8 新增加的API,該類實現了Future和CompletionStage兩個接口,提供了非常強大的Future的擴展功能(包含約50個方法),讓Java擁有了完整的非阻塞編程模型。
非阻塞指線程處理異步任務時,當異步任務獲取到數據時使用回調函數處理數據,而不是CPU空閒等待數據返回後再處理。
CompletableFuture一種函數風格的異步和事件驅動編程模型,它不會造成堵塞。CompletableFuture背後依靠的是fork/join框架來啓動新的線程實現異步與併發。當然,我們也能通過指定線程池來做這些事情。
什麼是函數式編程:
函數式編程中的函數指的並不是編程語言中的函數(或方法),它指的是數學意義上的函數,即映射關係(如:y = f(x)),就是 y 和 x 的對應關係。
數學上對於函數的定義是這樣的:“給定一個數集 A,假設其中的元素爲 x。現對 A 中的元素 x 施加對應法則 f,記作 f(x),得到另一數集 B。假設 B 中的元素爲 y。”
所以當我們在討論“函數式”時,我們其實是在說“像數學函數那樣,接收一個或多個輸入,生成一個或多個結果,並且沒有副作用”
回調函數:
回調函數比較通用的解釋是,它是一個通過函數指針調用的函數。如果你把函數的指針(地址)作爲參數傳遞給另一個函數,當這個指針被用爲調用它所指向的函數時,我們就說這是回調函數。回調函數不是由該函數的實現方直接調用,而是在特定的事件或條件發生時由另外一方調用的,用於對該事件或條件進行響應。
回調函數的機制:
1、定義一個回調函數;
2、提供函數實現的一方在初始化時候,將回調函數的函數指針註冊給調用者;
3、當特定的事件或條件發生的時候,調用者使用函數指針調用回調函數對事件進行處理。
什麼是Fork/Join框架
https://www.jianshu.com/p/42e9cd16f705
Fork/Join框架是一組允許程序員利用多核處理器支持的並行執行的API。它使用了“分而治之”策略:把非常大的問題分成更小的部分,反過來,小部分又可以進一步分成更小的部分,遞歸地直到一個部分可以直接解決。這被叫做“fork”。
然後所有部件在多個處理核心上並行執行。每個部分的結果被“join”在一起以產生最終結果。因此,框架的名稱是“Fork/Join”。
Fork/Join框架在JDk7中被加入,並在JDK8中進行了改進。它用了Java語言中的幾個新特性,包括並行的Stream API和排序。
Fork/Join框架簡化了並行程序的原因有:
它簡化了線程的創建,在框架中線程是自動被創建和管理。
它自動使用多個處理器,因此程序可以擴展到使用可用處理器。
由於支持真正的並行執行,Fork/Join框架可以顯著減少計算時間,並提高解決圖像處理、視頻處理、大數據處理等非常大問題的性能。
關於Fork/Join框架的一個有趣的地方是:它使用工作竊取算法來平衡線程之間的負載:如果一個工作線程沒有事情要做,它可以從其他仍然忙碌的線程竊取任務。
CompletableFuture的使用
1、主動完成計算
CompletableFuture類實現了CompletionStage和Future接口,所以你還是可以像以前一樣通過阻塞或者輪詢的方式獲得結果,儘管這種方式不推薦使用。
public T get() //該方法爲阻塞方法,會等待計算結果完成
public T get(long timeout, TimeUnit unit) //有時間限制的阻塞方法
public T getNow(T valueIfAbsent) //立即獲取方法結果,如果沒有計算結束則返回傳的值
public T join() //get() 方法類似也是主動阻塞線程,等待計算結果。和get() 方法對拋出的異常的處理有細微的差別
2、執行異步任務
創建一個異步任務
•public static <U> CompletableFuture<U> completedFuture(U value)
創建一個有初始值的CompletableFuture
•public static CompletableFuture<Void> runAsync(Runnable runnable)
•public static CompletableFuture<Void> runAsync(Runnable runnable, Executor executor)
•public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier)
•public static <U> CompletableFuture<U> supplyAsync(Supplier<U> supplier, Executor executor)
1)以上四個方法中,以 Async 結尾並且沒有 Executor 參數的,會默認使用 ForkJoinPool.commonPool() 作爲它的線程池執行異步代碼。
2)以run開頭的,因爲以 Runable 類型爲參數所以沒有返回值,CompletableFuture的計算結果爲空。
3)supplyAsync方法以Supplier<U>函數式接口類型爲參數,CompletableFuture的計算結果類型爲U。因爲方法的參數類型都是函數式接口,所以可以使用lambda表達式實現異步任務。
demo示例見CompletableFutureDemo.java; Disposition040107.java
3、當計算結果完成時對結果進行處理
當CompletableFuture的計算結果完成,或者拋出異常的時候,我們可以執行特定的Action。
public CompletableFuture<T> whenComplete(BiConsumer<? super T,? super Throwable> action)
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action)
public CompletableFuture<T> whenCompleteAsync(BiConsumer<? super T,? super Throwable> action, Executor executor)
public CompletableFuture<T> exceptionally(Function<Throwable,? extends T> fn)
1)參數類型爲 BiConsumer<? super T, ? super Throwable> 會獲取上一步計算的計算結果和異常信息。
2)不以Async結尾的方法由原來的線程計算,以Async結尾的方法由默認的線程池ForkJoinPool.commonPool()或者指定的線程池executor運行。 whenComplete這幾個方法都會返回CompletableFuture,當Action執行完畢後它的結果返回原始的CompletableFuture的計算結果或者返回異常。
3)exceptionally方法用來處理異常的情況,當原始的CompletableFuture拋出異常的時候,就會觸發這個CompletableFuture的計算。
4、對計算結果進行轉換
public <U> CompletableFuture<U> thenApply(Function<? super T,? extends U> fn)
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn)
public <U> CompletableFuture<U> thenApplyAsync(Function<? super T,? extends U> fn, Executor executor)
1)這一組函數的功能是當原來的CompletableFuture計算完後,將結果傳遞給函數fn,將fn的結果作爲新的CompletableFuture計算結果。因此它的功能相當於將CompletableFuture<T>轉換成CompletableFuture<U>。
2)需要注意的是,這些轉換並不是馬上執行的,也不會阻塞,而是在前一個stage完成後繼續執行。
5、合併多個任務的結果 allOf 和 anyOf
public static CompletableFuture<Void> allOf(CompletableFuture<?>... cfs)
把所有方法都執行完才往下執行,無CompletableFuture的計算結果。
public static CompletableFuture<Object> anyOf(CompletableFuture<?>... cfs)
任何一個方法執行完都往下執行,返回的是其中任意一個CompletableFuture,所以這裏沒有明確的返回類型,統一使用Object接受。
Lambda 表達式
Lambda 表達式,也可稱爲閉包,它是推動 Java 8 發佈的最重要新特性。Lambda 允許把函數作爲一個方法的參數(函數作爲參數傳遞進方法中)。使用 Lambda 表達式可以使代碼變的更加簡潔緊湊。
語法
lambda 表達式的語法格式如下:
(parameters) -> expression
或
(parameters) ->{ statements; }
以下是lambda表達式的重要特徵:
•可選的返回關鍵字:如果主體只有一個表達式返回值則編譯器會自動返回值,大括號需要指定明表達式返回了一個數值。
Lambda 表達式簡單實例:
1. 不需要參數,返回值爲 5
() -> 5
2. 接收一個參數(數字類型),返回其2倍的值
x -> 2 * x
3. 接受2個參數(數字),並返回他們的差值
(x, y) -> x – y
4. 接收2個int型整數,返回他們的和
(int x, int y) -> x + y
5. 接受一個 string 對象,並在控制檯打印,不返回任何值(看起來像是返回void)
(String s) -> System.out.print(s)