Java高併發系列——檢視閱讀

Java高併發系列——檢視閱讀java

參考node

java高併發系列web

liaoxuefeng Java教程 CompletableFutureredis

AQS原理沒講,須要找資料補充。算法

JUC中常見的集合原來沒講,好比ConcurrentHashMap最經常使用的,後面的都很泛,沒有深刻,有始無終。spring

阻塞隊列講得不夠深刻。sql

併發概念詞數據庫

同步(Synchronous)和異步(Asynchronous)編程

同步和異步一般來形容一次方法調用,同步方法調用一旦開始,調用者必須等到方法調用返回後,才能繼續後續的行爲。異步方法調用更像一個消息傳遞,一旦開始,方法調用就會當即返回,調用者就能夠繼續後續的操做。api

舉例:

拿泡泡麪來講,咱們把整個泡泡麪的過程分3個步驟:

  1. 燒水
  2. 泡麪加調味加蛋
  3. 倒開水泡麪

若是咱們泡泡麪的時候是按照這三個步驟,等開水開了再加調味加蛋,最後倒開水泡麪,這時候就是同步步驟;而若是咱們燒開水的同時把泡麪加調味加蛋準備好,就能夠省去燒水的同步等待時間,這就是異步。

併發(Concurrency)和並行(Parallelism)

併發和並行是兩個很是容易被混淆的概念。他們均可以表示兩個或者多個任務一塊兒執行,可是側重點有所不一樣。併發偏重於多個任務交替執行,而多個任務之間有可能仍是串行的(等待阻塞等),而並行是真正意義上的「同時執行」 。

舉例:

你們排隊在一個咖啡機上接咖啡,交替執行,是併發;兩臺咖啡機上面接咖啡,是並行。

併發說的是在一個時間段內,多件事情在這個時間段內交替執行。

並行說的是多件事情在同一個時刻同時發生。

若是系統內只有一個CPU,而使用多進程或者多線程任務,那麼真實環境中這些任務不多是真實並行的,畢竟一個CPU一次只能執行一條指令,在這種狀況下多進程或者多線程就是併發的,而不是並行的(操做系統會不停地切換多任務) 。

臨界區

臨界區用來表示一種公共資源或者說共享數據,能夠被多個線程使用,可是每一次只能有一個線程使用它,一旦臨界區資源被佔用,其餘線程要想使用這個資源就必須等待。

阻塞(Blocking)和非阻塞(Non-Blocking)

阻塞和非阻塞一般用來形容不少線程間的相互影響。好比一個線程佔用了臨界區資源,那麼其餘全部須要這個資源的線程就必須在這個臨界區中等待。等待會致使線程掛起,這種狀況就是阻塞。 非阻塞的意思與之相反,它強調沒有一個線程能夠妨礙其餘線程執行,全部的線程都會嘗試不斷向前執行。

死鎖(Deadlock)、飢餓(Starvation)和活鎖(Livelock)

死鎖、飢餓和活鎖都屬於多線程的活躍性問題 。

死鎖:兩個線程都持有獨佔的資源(鎖),同時又互相嘗試獲取對方獨佔的資源(鎖),這時候雙方都沒有釋放本身的獨佔資源,致使永遠也獲取不到阻塞等待下去。

飢餓是指某一個或者多個線程由於種種緣由沒法得到所要的資源,致使一直沒法執行。一種好比它的優先級可能過低,而高優先級的線程不斷搶佔它須要的資源,致使低優先級線程沒法工做。另外一種如某一個線程一直佔着關鍵資源不放(例子:單線程池裏submit一個線程任務,而該線程又往該單線程池裏submit一個新的任務並等待結果返回,由於線程池是單線程池,因此便一種套娃着),致使其餘須要這個資源的線程沒法正常執行,這種狀況也是飢餓的一種。與死鎖相比,飢餓仍是有可能在將來一段時間內解決的(好比,高優先級的線程已經完成任務,再也不瘋狂執行)。

活鎖:當兩個線程都秉承着「謙讓」的原則(致使死循環),主動將資源釋放給他人使用,那麼就會致使資源不斷地在兩個線程間跳動,而沒有一個線程能夠同時拿到全部資源正常執行。這種狀況就是活鎖。

擴展

經過jstack查看到死鎖信息

一、使用jps找到執行代碼的進程ID,啓動類名爲DeadLockTest(main函數所在類)的進程ID爲11084 
jps
二、經過jstack命令找到java進程中死鎖的線程鎖信息,執行jstack -l 11084 
jstack -l 11084

最後輸出:
===================================================
"thread2":
        at com.self.current.DeadLockTest$SynAddRunalbe.run(DeadLockTest.java:331)
        - waiting to lock <0x000000076b77e048> (a com.self.current.DeadLockTest$Obj1)
        - locked <0x000000076b780358> (a com.self.current.DeadLockTest$Obj2)
        at java.lang.Thread.run(Thread.java:748)
"thread1":
        at com.self.current.DeadLockTest$SynAddRunalbe.run(DeadLockTest.java:282)
        - waiting to lock <0x000000076b780358> (a com.self.current.DeadLockTest$Obj2)
        - locked <0x000000076b77e048> (a com.self.current.DeadLockTest$Obj1)
        at java.lang.Thread.run(Thread.java:748)

併發級別

因爲臨界區的存在,多線程之間的併發必須受到控制。根據控制併發的策略,咱們能夠把併發的級別分爲阻塞、無飢餓、無障礙、無鎖、無等待5種。

阻塞——悲觀鎖

一個線程是阻塞的,那麼在其餘線程釋放資源以前,當前線程沒法繼續執行。當咱們使用synchronized關鍵字或者重入鎖時,咱們獲得的就是阻塞的線程。

synchronize關鍵字和重入鎖都試圖在執行後續代碼前,獲得臨界區的鎖,若是得不到,線程就會被掛起等待,直到佔有了所需資源爲止。

例子:synchronize或ReentrantLock。

無飢餓(Starvation-Free)——公平與非公平鎖

表示非公平鎖與公平鎖兩種狀況 。若是線程之間是有優先級的,那麼線程調度的時候老是會傾向於先知足高優先級的線程。

對於非公平鎖來講,系統容許高優先級的線程插隊。這樣有可能致使低優先級線程產生飢餓。但若是鎖是公平的,按照先來後到的規則,那麼飢餓就不會產生 。

例子:ReentrantLock 默認採用非公平鎖,除非在構造方法中傳入參數 true 。

//默認
public ReentrantLock() {
    sync = new NonfairSync();
}
//傳入true or false
public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

無障礙(Obstruction-Free)——樂觀鎖CAS

無障礙是一種最弱的非阻塞調度。兩個線程若是無障礙地執行,那麼不會由於臨界區的問題致使一方被掛起。

對於無障礙的線程來講,一旦檢測到這種狀況,它就會當即對本身所作的修改進行回滾,確保數據安全。但若是沒有數據競爭發生,那麼線程就能夠順利完成本身的工做,走出臨界區。

無障礙的多線程程序並不必定能順暢運行。由於當臨界區中存在嚴重的衝突時,全部的線程可能都會不斷地回滾本身的操做,而沒有一個線程能夠走出臨界區。這種狀況會影響系統的正常執行。因此,一種可行的無障礙實現能夠依賴一個"一致性標記"來實現。

數據庫中樂觀鎖(經過版本號或者時間戳實現)。表中須要一個字段version(版本號),每次更新數據version+1,更新的時候將版本號做爲條件進行更新,根據更新影響的行數判斷更新是否成功,僞代碼以下:

1.查詢數據,此時版本號爲w_v
2.打開事務
3.作一些業務操做
//此行會返回影響的行數c
4.update t set version = version+1 where id = 記錄id and version = w_v;
5.if(c>0){        //提交事務    }else{        //回滾事務    }

多個線程更新同一條數據的時候,數據庫會對當前數據加鎖,同一時刻只有一個線程能夠執行更新語句。

無鎖(Lock-Free)

無鎖的併發都是無障礙的。在無鎖的狀況下,全部的線程都能嘗試對臨界區進行訪問,但不一樣的是,無鎖的併發保證必然有一個線程可以在有限步內完成操做離開臨界區。 (注意有限步)

在無鎖的調用中,一個典型的特色是可能會包含一個無窮循環。在這個循環中,線程會不斷嘗試修改共享變量。若是沒有衝突,修改爲功,那麼程序退出,不然繼續嘗試修改。但不管如何,無鎖的並行總能保證有一個線程是能夠勝出的,不至於全軍覆沒。至於臨界區中競爭失敗的線程,他們必須不斷重試,直到本身獲勝。若是運氣很很差,老是嘗試不成功,則會出現相似飢餓的先寫,線程會中止。(併發量太大時會出現飢餓,這時候有必要改爲阻塞鎖)

下面就是一段無鎖的示意代碼,若是修改不成功,那麼循環永遠不會中止。

while(!atomicVar.compareAndSet(localVar, localVar+1)){        localVal = atomicVar.get();}

無等待——讀寫鎖

無鎖只要求有一個線程能夠在有限步內完成操做,而無等待則在無鎖的基礎上更進一步擴展。無等待要求全部線程都必須在有限步內完成,這樣不會引發飢餓問題。若是限制這個步驟的上限,對循環次數的限制不一樣。分爲爲

  1. 有界無等待
  2. 線程數無關的無等待。

一種典型的無等待結果就是RCU(Read Copy Update)。它的基本思想是,對數據的讀能夠不加控制。所以,全部的讀線程都是無等待的,它們既不會被鎖定等待也不會引發任何衝突。但在寫數據的時候,先獲取原始數據的副本,接着只修改副本數據(這就是爲何讀能夠不加控制),修改完成後,在合適的時機回寫數據。

並行的兩個重要定律

爲何要使用並行程序 ?

第一,爲了得到更好的性能;

第二,因爲業務模型的須要,確實須要多個執行實體。

關於並行程序對性能的提升定律有二,Amdahl(阿姆達爾)定律和Gustafson(古斯塔夫森 )定律。

加速比定義:加速比 = 優化前系統耗時 / 優化後系統耗時

根據Amdahl定律,使用多核CPU對系統進行優化,優化的效果取決於CPU的數量,以及系統中串行化程序的比例。CPU數量越多,串行化比例越低,則優化效果越好。僅提升CPU數量而不下降程序的串行化比例,也沒法提升系統的性能。

根據Gustafson定律,咱們能夠更容易地發現,若是串行化比例很小,並行化比例很大,那麼加速比就是處理器的個數。只要不斷地累加處理器,就能得到更快的速度。

總結

Gustafson定律和Amdahl定律的角度不一樣

Amdahl強調:當串行換比例必定時,加速比是有上限的,無論你堆疊多少個CPU參與計算,都不能突破這個上限。

Gustafson定律強調:若是可被並行化的代碼所佔比例足夠大,那麼加速比就能隨着CPU的數量線性增加。

總的來講,提高性能的方法:想辦法提高系統並行的比例(減小串行比例),同時增長CPU數量。

附圖:

Amdahl公式的推倒過程

其中n表示處理器個數,T表示時間,T1表示優化前耗時(也就是隻有1個處理器時的耗時),Tn表示使用n個處理器優化後的耗時。F是程序中只能串行執行的比例。

Gustafson公式的推倒過程

併發編程中JMM相關的一些概念

JMM(JAVA Memory Model:Java內存模型),因爲併發程序要比串行程序複雜不少,其中一個重要緣由是併發程序中數據訪問一致性和安全性將會受到嚴重挑戰。如何保證一個線程能夠看到正確的數據呢?

Q:如何保證一個線程能夠看到正確的數據呢?

A:經過Java內存模型管理,JMM關鍵技術點都是圍繞着多線程的原子性、可見性、有序性來創建的 。

原子性

原子性是指操做是不可分的,要麼所有一塊兒執行,要麼不執行。java中實現原子操做的方法大體有2種:鎖機制、無鎖CAS機制。

可見性

可見性是指一個線程對共享變量的修改,對於另外一個線程來講是不是可見的。

看一下java線程內存模型及規則:

  • 咱們定義的全部變量都儲存在 主內存中。
  • 每一個線程都有本身 獨立的工做內存,裏面保存該線程使用到的變量的副本(主內存中該變量的一份拷貝)
  • 線程對共享變量全部的操做都必須在本身的工做內存中進行,不能直接從主內存中讀寫(不能越級)
  • 不一樣線程之間也沒法直接訪問其餘線程的工做內存中的變量,線程間變量值的傳遞須要經過主內存來進行。(同級不能相互訪問)

例子:線程須要修改一個共享變量X,須要先把X從主內存複製一份到線程的工做內存,在本身的工做內存中修改完畢以後,再從工做內存中回寫到主內存。若是線程對變量的操做沒有刷寫回主內存的話,僅僅改變了本身的工做內存的變量的副本,那麼對於其餘線程來講是不可見的。而若是另外一個線程的變量沒有讀取主內存中的新的值,而是使用舊的值的話,一樣的也能夠列爲不可見。

共享變量可見性的實現原理:

線程A對共享變量的修改要被線程B及時看到的話,須要進過如下2個步驟:

1.線程A在本身的工做內存中修改變量以後,須要將變量的值刷新到主內存中 。

2.線程B要把主內存中變量的值更新到工做內存中。

關於線程可見性的控制,能夠使用volatile、synchronized、鎖來實現。

有序性

有序性指的是程序按照代碼的前後順序執行。這是由於爲了性能優化,編譯器和處理器會進行指令重排序,有時候會改變程序語句的前後順序。

例子:

在單例模式的實現上有一種雙重檢驗鎖定的方式,由於指令重排致使獲取併發時獲取到的單例多是未正確初始化的單例。代碼以下:

public class Singleton {
  static Singleton instance;
  static Singleton getInstance(){
    if (instance == null) {
      synchronized(Singleton.class) {
        if (instance == null)
          instance = new Singleton();
        }
    }
    return instance;
  }
}

咱們先看 instance=newSingleton();

未被編譯器優化的操做:

  1. 指令1:分配一款內存M
  2. 指令2:在內存M上初始化Singleton對象
  3. 指令3:將M的地址賦值給instance變量

編譯器優化後的操做指令:

  1. 指令1:分配一塊內存M
  2. 指令2:將M的地址賦值給instance變量
  3. 指令3:在內存M上初始化Singleton對象

如今有2個線程,恰好執行的代碼被編譯器優化過,過程以下:

最終線程B獲取的instance是沒有初始化的,此時去使用instance可能會產生一些意想不到的錯誤。

能夠使用volatile修飾變量或者換成採用靜態內部內的方式實現單例。

深刻理解進程和線程

進程

進程(Process)是計算機中的程序關於某數據集合上的一次運行活動,是系統進行資源分配和調度的基本單位,是操做系統結構的基礎。程序是指令、數據及其組織形式的描述,進程是程序的實體。

進程具備的特徵:

  • 動態性:進程是程序的一次執行過程,是臨時的,有生命期的,是動態產生,動態消亡的
  • 併發性:任何進程均可以同其餘進行一塊兒併發執行
  • 獨立性:進程是系統進行資源分配和調度的一個獨立單位
  • 結構性:進程由程序,數據和進程控制塊三部分組成

線程

線程是輕量級的進程,是程序執行的最小單元,使用多線程而不是多進程去進行併發程序的設計,是由於線程間的切換和調度的成本遠遠小於進程。

咱們用一張圖來看一下線程的狀態圖:

Java中線程的狀態分爲6種 ,在java.lang.Thread中的State枚舉中有定義,如:

public enum State {    NEW,    RUNNABLE,    BLOCKED,    WAITING,    TIMED_WAITING,    TERMINATED;}

Java線程的6種狀態及切換

1. 初始(NEW):表示剛剛建立的線程,但尚未調用start()方法。
2. 運行(RUNNABLE):運行狀態.Java線程中將就緒(ready)和運行中(running)兩種狀態籠統的稱爲「運行」。
線程對象建立後,其餘線程(好比main線程)調用了該對象的start()方法。該狀態的線程位於可運行線程池中,等待被線程調度選中,獲取CPU的使用權,此時處於就緒狀態(ready)。就緒狀態的線程在得到CPU時間片後變爲運行中狀態(running)。
3. 阻塞(BLOCKED):阻塞狀態,表示線程阻塞於鎖。當線程在執行的過程當中遇到了synchronized同步塊,但這個同步塊被其餘線程已獲取還未釋放時,當前線程將進入阻塞狀態,會暫停執行,直到獲取到鎖。當線程獲取到鎖以後,又會進入到運行狀態(RUNNABLE)(維護在同步隊列中)
4. 等待(WAITING):等待狀態。進入該狀態的線程須要等待其餘線程作出一些特定動做(通知或中斷)。和TIMED_WAITING都表示等待狀態,區別是WAITING會進入一個無時間限制的等,而TIMED_WAITING會進入一個有限的時間等待,那麼等待的線程究竟在等什麼呢?通常來講,WAITING的線程正式在等待一些特殊的事件,好比,經過wait()方法等待的線程在等待notify()方法,而經過join()方法等待的線程則會等待目標線程的終止。一旦等到指望的事件,線程就會再次進入RUNNABLE運行狀態。(維護在等待隊列中)
5. 超時等待(TIMED_WAITING):超時等待狀態。該狀態不一樣於WAITING,它能夠在指定的時間後自行返回。
6. 終止(TERMINATED):結束狀態,表示該線程已經執行完畢。

幾個方法的比較
Thread.sleep(long millis),必定是當前線程調用此方法,當前線程進入TIMED_WAITING狀態,但不釋放對象鎖,millis後線程自動甦醒進入就緒狀態。做用:給其它線程執行機會的最佳方式。
Thread.yield(),必定是當前線程調用此方法,當前線程放棄獲取的CPU時間片,但不釋放鎖資源,由運行狀態變爲就緒狀態,讓OS再次選擇線程。做用:讓相同優先級的線程輪流執行,但並不保證必定會輪流執行。實際中沒法保證yield()達到讓步目的,由於讓步的線程還有可能被線程調度程序再次選中。Thread.yield()不會致使阻塞。該方法與sleep()相似,只是不能由用戶指定暫停多長時間。
thread.join()/thread.join(long millis),當前線程裏調用其它線程t的join方法,當前線程進入WAITING/TIMED_WAITING狀態,當前線程不會釋放已經持有的對象鎖。線程t執行完畢或者millis時間到,當前線程通常狀況下進入RUNNABLE狀態,也有可能進入BLOCKED狀態(由於join是基於wait實現的)。
obj.wait(),當前線程調用對象的wait()方法,當前線程釋放對象鎖,進入等待隊列。依靠notify()/notifyAll()喚醒或者wait(long timeout) timeout時間到自動喚醒。
obj.notify()喚醒在此對象監視器上等待的單個線程,選擇是任意性的。notifyAll()喚醒在此對象監視器上等待的全部線程。
LockSupport.park()/LockSupport.parkNanos(long nanos),LockSupport.parkUntil(long deadlines), 當前線程進入WAITING/TIMED_WAITING狀態。對比wait方法,不須要得到鎖就可讓線程進入WAITING/TIMED_WAITING狀態,須要經過LockSupport.unpark(Thread thread)喚醒。

線程的狀態圖

進程與線程的一個簡單解釋

計算機的核心是CPU,整個操做系統就像一座工廠,時刻在運行 。進程就比如工廠的車間 ,它表明CPU所能處理的單個任務 。線程就比如車間裏的工人。一個進程能夠包括多個線程。 車間的空間是工人們共享的,好比許多房間是每一個工人均可以進出的。這象徵一個進程的內存空間是共享的,每一個線程均可以使用這些共享內存。 每間房間的大小不一樣,有些房間最多隻能容納一我的,好比廁所。裏面有人的時候,其餘人就不能進去了。這表明一個線程使用某些共享內存時,其餘線程必須等它結束,才能使用這一塊內存。 一個防止他人進入的簡單方法,就是門口加一把鎖。先到的人鎖上門,後到的人看到上鎖,就在門口排隊,等鎖打開再進去。這就叫"互斥鎖"(Mutual exclusion,縮寫 Mutex),防止多個線程同時讀寫某一塊內存區域。 還有些房間,能夠同時容納n我的,好比廚房。也就是說,若是人數大於n,多出來的人只能在外面等着。這比如某些內存區域,只能供給固定數目的線程使用。 這時的解決方法,就是在門口掛n把鑰匙。進去的人就取一把鑰匙,出來時再把鑰匙掛回原處。後到的人發現鑰匙架空了,就知道必須在門口排隊等着了。這種作法叫作"信號量"(Semaphore),用來保證多個線程不會互相沖突。

操做系統的設計,所以能夠歸結爲三點:

(1)以多進程形式,容許多個任務同時運行;

(2)以多線程形式,容許單個任務分紅不一樣的部分運行;

(3)提供協調機制,一方面防止進程之間和線程之間產生衝突,另外一方面容許進程之間和線程之間共享資源。

疑問:

Q:thread.join()/thread.join(long millis),當前線程裏調用其它線程t的join方法,當前線程進入WAITING/TIMED_WAITING狀態,當前線程不會釋放已經持有的對象鎖。線程t執行完畢或者millis時間到,當前線程通常狀況下進入RUNNABLE狀態,也有可能進入BLOCKED狀態(由於join是基於wait實現的)。

調用其餘線程的thread.join()方法,當前線程不會釋放已經持有的對象鎖,那若是進入了BLOCKED狀態時會釋放對象鎖麼?

線程的基本操做

新建線程

start方法是啓動一個線程,run方法只會在當前線程中串行的執行run方法中的代碼。

咱們能夠經過繼承Thread類,而後重寫run方法,來自定義一個線程。但考慮java是單繼承的,從擴展性上來講,咱們實現一個接口來自定義一個線程更好一些,java中恰好提供了Runnable接口來自定義一個線程。實現Runnable接口是比較常見的作法,也是推薦的作法。

終止線程——stop()方法已廢棄

stop方法爲什麼會被廢棄而不推薦使用?stop方法過於暴力,強制把正在執行的方法中止了。

你們是否遇到過這樣的場景:聽着歌寫着代碼忽然斷電了。線程正在運行過程當中,被強制結束了,可能會致使一些意想不到的後果。能夠給你們發送一個通知,告訴你們保存一下手頭的工做,將電腦關閉。

線程中斷——interrupt()正確的中斷線程方法

線程中斷並不會使線程當即退出,而是給線程發送一個通知,告知目標線程,有人但願你退出了!至於目標線程接收到通知以後如何處理,則徹底由目標線程本身決定,這點很重要,若是中斷後,線程當即無條件退出,咱們又會到stop方法的老問題。

Thread提供了3個與線程中斷有關的方法,這3個方法容易混淆,你們注意下:

public void interrupt() //中斷線程
public boolean isInterrupted() //判斷線程是否被中斷
public static boolean interrupted()  //判斷線程是否被中斷,並清除當前中斷狀態

interrupt()方法是一個實例方法,它通知目標線程中斷,也就是設置中斷標誌位爲true,中斷標誌位表示當前線程已經被中斷了。

isInterrupted()方法也是一個實例方法,它判斷當前線程是否被中斷(經過檢查中斷標誌位)。

interrupted()是一個靜態方法,返回boolean類型,也是用來判斷當前線程是否被中斷,可是同時會清除當前線程的中斷標誌位的狀態。

Q:經過變量控制和線程自帶的interrupt方法來中斷線程有什麼區別呢?

A:若是一個線程調用了sleep方法,一直處於休眠狀態,經過變量控制,是不能中斷線程麼,由於此時線程處於睡眠狀態沒法執行變量控制語句,此時只能使用線程提供的interrupt方法來中斷線程了。

實例:

public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread() {
        @Override
        public void run() {
            while (true) {
                try {
                    TimeUnit.SECONDS.sleep(20);
                } catch (InterruptedException e) {
                    //sleep方法因爲中斷而拋出異常以後,線程的中斷標誌會被清除(置爲false),因此在異常中須要執行this.interrupt()方法,將中斷標誌位置爲true
                    this.interrupt();
                    System.out.println("exception:"+ e.getMessage());
                }
                System.out.println(Thread.currentThread().getName() + " in the end");
                break;
            }

        }
    };
    t1.setName("interrupt thread");
    t1.start();
    TimeUnit.SECONDS.sleep(1);
    //調用interrupt()方法以後,線程的sleep方法將會拋出 InterruptedException: sleep interrupted異常。
    t1.interrupt();
}

錯誤寫法:

注意:sleep方法因爲中斷而拋出異常以後,線程的中斷標誌會被清除(置爲false),因此在異常中須要執行this.interrupt()方法,將中斷標誌位置爲true

等待(wait)和通知(notify)

爲了支持多線程之間的協做,JDK提供了兩個很是重要的方法:等待wait()方法和通知notify()方法。這2個方法並非在Thread類中的,而是在Object類中定義的。這意味着全部的對象均可以調用者兩個方法。

(即只有這個對象是被當成鎖來做爲多線程之間的協做對象,那麼在同步代碼塊中,線程之間就是經過等待wait()方法和通知notify()方法協做。)

public final void wait() throws InterruptedException;
public final native void notify();

若是一個線程調用了object.wait()方法,那麼它就會進出object對象的等待隊列。這個隊列中,可能會有多個線程,由於系統可能運行多個線程同時等待某一個對象。當object.notify()方法被調用時,它就會從這個隊列中隨機選擇一個線程,並將其喚醒。這裏但願你們注意一下,這個選擇是不公平的,並非先等待線程就會優先被選擇,這個選擇徹底是隨機的。 nofiyAll()方法,它和notify()方法的功能相似,不一樣的是,它會喚醒在這個等待隊列中全部等待的線程,而不是隨機選擇一個。

這裏強調一點,Object.wait()方法並不能隨便調用。它必須包含在對應的synchronize語句塊中,不管是wait()方法或者notify()方法都須要首先獲取目標獨享的一個監視器。

等待wait()方法和通知notify()方法工做過程:

wait()方法和nofiy()方法的工做流程細節:

圖中其中T1和T2表示兩個線程。T1在正確執行wait()方法前,必須得到object對象的監視器。而wait()方法在執行後,會釋放這個監視器。這樣作的目的是使其餘等待在object對象上的線程不至於由於T1的休眠而所有沒法正常執行。

線程T2在notify()方法調用前,也必須得到object對象的監視器。所幸,此時T1已經釋放了這個監視器,所以,T2能夠順利得到object對象的監視器。接着,T2執行了notify()方法嘗試喚醒一個等待線程,這裏假設喚醒了T1。T1在被喚醒後,要作的第一件事並非執行後續代碼,而是要嘗試從新得到object對象的監視器,而這個監視器也正是T1在wait()方法執行前所持有的那個。若是暫時沒法得到,則T1還必須等待這個監視器。當監視器順利得到後,T1才能夠在真正意義上繼續執行。

注意:Object.wait()方法和Thread.sleeep()方法均可以讓現場等待若干時間。除wait()方法能夠被喚醒外,另一個主要的區別就是wait()方法會釋放目標對象的鎖,而Thread.sleep()方法不會釋放鎖。

示例:

public class WaitNotifyTest {
    public static Object lock = new Object();

    public static void main(String[] args) {
        new T1("Thread-1").start();
        new T2("Thread-2").start();
    }

    static class T1 extends Thread {
        public T1(String name) {
            super(name);
        }

        @Override
        public void run() {
            synchronized (lock) {
                System.out.println(this.getName() + " start");
                try {
                    System.out.println(this.getName() + " wait");
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(this.getName() + " end");
            }
        }
    }

    static class T2 extends Thread {
        public T2(String name) {
            super(name);
        }

        @Override
        public void run() {
            synchronized (lock) {
                System.out.println(this.getName() + " start");
                System.out.println(this.getName() + " notify");
                lock.notify();
                System.out.println(this.getName() + " end");
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(this.getName() + " end,2 second later");
            }
        }
    }

}

輸出:

Thread-1 start
Thread-1 wait
Thread-2 start
Thread-2 notify
Thread-2 end
Thread-2 end,2 second later
Thread-1 end
注意下打印結果,T2調用notify方法以後,T1並不能當即繼續執行,而是要等待T2釋放objec投遞鎖以後,T1從新成功獲取鎖後,才能繼續執行。所以最後2行日誌相差了2秒(由於T2調用notify方法後休眠了2秒)。

能夠這麼理解,obj對象上有2個隊列,q1:等待隊列,q2:準備獲取鎖的隊列;

掛起(suspend)和繼續執行(resume)線程——方法已廢棄

Thread類中還有2個方法,即線程掛起(suspend)和繼續執行(resume),這2個操做是一對相反的操做,被掛起的線程,必需要等到resume()方法操做後,才能繼續執行。系統中已經標註着2個方法過期了,不推薦使用。

系統不推薦使用suspend()方法去掛起線程是由於suspend()方法致使線程暫停的同時,並不會釋聽任何鎖資源。此時,其餘任何線程想要訪問被它佔用的鎖時,都會被牽連,致使沒法正常運行(如圖2.7所示)。直到在對應的線程上進行了resume()方法操做,被掛起的線程才能繼續,從而其餘全部阻塞在相關鎖上的線程也能夠繼續執行。可是,若是resume()方法操做意外地在suspend()方法前就被執行了,那麼被掛起的線程可能很難有機會被繼續執行了。而且,更嚴重的是:它所佔用的鎖不會被釋放,所以可能會致使整個系統工做不正常。並且,對於被掛起的線程,從它線程的狀態上看,竟然仍是Runnable狀態,這也會影響咱們對系統當前狀態的判斷。

等待線程結束(join)和謙讓(yeild)

不少時候,一個線程的輸入可能很是依賴於另一個或者多個線程的輸出,此時,這個線程就須要等待依賴的線程執行完畢,才能繼續執行。jdk提供了join()操做來實現等待線程結束。

//表示無限等待,當前線程會一直等待,直到目標線程執行完畢
public final void join() throws InterruptedException;
//millis參數用於指定等待時間,若是超過了給定的時間目標線程還在執行,當前線程也會中止等待,而繼續往下執行。
public final synchronized void join(long millis) throws InterruptedException;

例子:線程T1須要等待T二、T3完成以後才能繼續執行,那麼在T1線程中須要分別調用T2和T3的join()方法。

示例:

public class JoinTest {
    private static int num = 0;

    public static void main(String[] args) throws InterruptedException {
        T1 t1 = new T1("T1");
        t1.start();
        long start = System.currentTimeMillis();
        t1.join();
        long end = System.currentTimeMillis();
        System.out.println(Thread.currentThread().getName() + " end .user time ="+(end-start)+" ,get num=" + num);
    }

    static class T1 extends Thread {
        public T1(String name) {
            super(name);
        }

        @Override
        public void run() {
            System.out.println(this.getName() + " start");
            for (int i = 0; i < 9; i++) {
                num++;
            }
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(this.getName() + " end");
        }
    }
}

輸出:

T1 start
T1 end
main end .user time =3003 ,get num=9

Thread.yield()方法。是屈服,放棄,謙讓的意思。

這是一個靜態方法,一旦執行,它會讓當前線程讓出CPU,但須要注意的是,讓出CPU並非說不讓當前線程執行了,當前線程在出讓CPU後,還會進行CPU資源的爭奪,可是可否再搶到CPU的執行權就不必定了。所以,對Thread.yield()方法的調用好像就是在說:我已經完成了一些主要的工做,我能夠休息一下了,可讓CPU給其餘線程一些工做機會了。

若是以爲一個線程不過重要,或者優先級比較低,而又擔憂此線程會過多的佔用CPU資源,那麼能夠在適當的時候調用一下Thread.yield()方法,給與其餘線程更多的機會。

public static native void yield();

總結

  1. 建立線程的4種方式:繼承Thread類;實現Runnable接口;實現Callable接口;使用線程池建立。
  2. 啓動線程:調用線程的start()方法
  3. 終止線程:調用線程的stop()方法,方法已過期,建議不要使用
  4. 線程中斷相關的方法:調用線程實例interrupt()方法將中斷標誌置爲true;使用線程實例方法isInterrupted()獲取中斷標誌;調用Thread的靜態方法interrupted()獲取線程是否被中斷,此方法調用以後會清除中斷標誌(將中斷標誌置爲false了)
  5. wait、notify、notifyAll方法
  6. 線程掛起使用線程實例方法suspend(),恢復線程使用線程實例方法resume(),這2個方法都過期了,不建議使用
  7. 等待線程結束:調用線程實例方法join()
  8. 讓出cpu資源:調用線程靜態方法yeild()

疑問:

Q:方法interrupted()是一個靜態方法,返回boolean類型,也是用來判斷當前線程是否被中斷,可是同時會清除當前線程的中斷標誌位的狀態。 清除當前線程的中斷標誌位的狀態是表示該線程能夠不中斷了麼?清除當前線程的中斷標誌位的狀態是什麼意思,有什麼做用?怎麼使用?

Q:三個線程交替打印ABC 10次使用wait(),notifyAll()如何實現?

volatile與Java內存模型

volatile解決了共享變量在多線程中可見性的問題,可見性是指一個線程對共享變量的修改,對於另外一個線程來講是不是能夠看到的。

使用volatile保證內存可見性示例:

public class VolatileTest {

    //public static boolean flag = true;
    public static volatile boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        T1 t1 = new T1("T1");
        t1.start();
        //TimeUnit.SECONDS.sleep(3);
        Thread.sleep(2000);
        //將flag置爲false
        flag = false;
    }

    public static class T1 extends Thread {
        public T1(String name) {
            super(name);
        }

        @Override
        public void run() {
            System.out.println(this.getName() + " start");
            while (VolatileTest.flag) {
                //奇怪現象,爲何執行輸出語句在運行一下子後會讓flag=false讀取到,而 ; 空循環卻會致使程序沒法終止呢?
                //我的以爲應該是虛擬機從解釋執行轉換爲編譯執行,這時候會從新讀到flag。
                //System.out.println(this.getName() +"endless loop");
                ;
            }
            System.out.println(this.getName() + " end");
        }
    }
}

不加volatile運行上面代碼,會發現程序沒法終止。

Q:t1線程中爲何看不到被主線程修改以後的flag?

要解釋這個,咱們須要先了解一下java內存模型(JMM),Java線程之間的通訊由Java內存模型(本文簡稱爲JMM)控制,JMM決定一個線程對共享變量的寫入什麼時候對另外一個線程可見。從抽象的角度來看,JMM定義了線程和主內存之間的抽象關係:線程之間的共享變量存儲在主內存(main memory)中,每一個線程都有一個私有的本地內存(local memory),本地內存中存儲了該線程讀/寫共享變量的副本。本地內存是JMM的一個抽象概念,並不真實存在。它涵蓋了緩存,寫緩衝區,寄存器以及其餘的硬件和編譯器優化。

Java內存模型的抽象示意圖:

線程A須要和線程B通訊,必需要經歷下面2個步驟:

  1. 首先,線程A把本地內存A中更新過的共享變量刷新到主內存中去。
  2. 而後,線程B到主內存中去讀取線程A以前已更新過的共享變量。

線程t1中爲什麼看不到被主線程修改成false的flag的值的緣由,有兩種可能:

  1. 主線程修改了flag以後,未將其刷新到主內存,因此t1看不到
  2. 主線程將flag刷新到了主內存,可是t1一直讀取的是本身工做內存中flag的值,沒有去主內存中獲取flag最新的值

使用volatile修飾共享變量,就能夠達到上面的效果,被volatile修改的變量有如下特色:

  1. 線程中讀取的時候,每次讀取都會去主內存中讀取共享變量最新的值,而後將其複製到工做內存
  2. 線程中修改了工做內存中變量的副本,修改以後會當即刷新到主內存

線程組

咱們能夠把線程歸屬到某個線程組中,線程組能夠包含多個線程以及線程組,線程和線程組組成了父子關係,是個樹形結構。使用線程組能夠方便管理線程 。(線程池是否是更實在一點?)

建立線程關聯線程組

建立線程的時候,能夠給線程指定一個線程組。

建立線程組的時候,能夠給其指定一個父線程組,也能夠不指定,若是不指定父線程組,則父線程組爲當前線程的線程組,系統自動獲取當前線程的線程組做爲默認父線程組。java api有2個經常使用的構造方法用來建立線程組:

public ThreadGroup(String name) public ThreadGroup(ThreadGroup parent, String name)

第一個構造方法未指定父線程組,看一下內部的實現:

public ThreadGroup(String name) {        this(Thread.currentThread().getThreadGroup(), name);    }

批量中止線程

調用線程組interrupt(),會將線程組樹下的全部子孫線程中斷標誌置爲true,能夠用來批量中斷線程。

建議建立線程或者線程組的時候,給他們取一個有意義的名字,在系統出問題的時候方面查詢定位。

示例:

public class ThreadGroupTest {
    public static class R1 implements Runnable {
        @Override
        public void run() {
            System.out.println("threadName:" + Thread.currentThread().getName());
            while (!Thread.currentThread().isInterrupted()){
                ;
            }
            System.out.println(Thread.currentThread().getName()+"線程中止了");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        //threadGroup1未指定父線程組,系統獲取了主線程的線程組做爲threadGroup1的父線程組,輸出結果中是:main
        ThreadGroup threadGroup = new ThreadGroup("thread-group-1");
        Thread t1 = new Thread(threadGroup, new R1(), "t1");
        Thread t2 = new Thread(threadGroup, new R1(), "t2");
        t1.start();
        t2.start();
        TimeUnit.SECONDS.sleep(1);
        System.out.println("活動線程數:" + threadGroup.activeCount());
        System.out.println("活動線程組:" + threadGroup.activeGroupCount());
        System.out.println("線程組名稱:" + threadGroup.getName());
        ThreadGroup threadGroup2 = new ThreadGroup(threadGroup, "thread-group-2");
        Thread t3 = new Thread(threadGroup2, new R1(), "t3");
        Thread t4 = new Thread(threadGroup2, new R1(), "t4");
        t3.start();
        t4.start();
        threadGroup.list();
        //java.lang.ThreadGroup[name=main,maxpri=10] 主線程的線程組爲main
        System.out.println(Thread.currentThread().getThreadGroup());
        //java.lang.ThreadGroup[name=system,maxpri=10] 根線程組爲system
        System.out.println(Thread.currentThread().getThreadGroup().getParent());
        //null
        System.out.println(Thread.currentThread().getThreadGroup().getParent().getParent());

        threadGroup.interrupt();
        TimeUnit.SECONDS.sleep(2);
        threadGroup.list();
    }
}

輸出:

threadName:t1
threadName:t2
活動線程數:2
活動線程組:0
線程組名稱:thread-group-1
java.lang.ThreadGroup[name=thread-group-1,maxpri=10]
    Thread[t1,5,thread-group-1]
    Thread[t2,5,thread-group-1]
    java.lang.ThreadGroup[name=thread-group-2,maxpri=10]
        Thread[t3,5,thread-group-2]
        Thread[t4,5,thread-group-2]
java.lang.ThreadGroup[name=main,maxpri=10]
java.lang.ThreadGroup[name=system,maxpri=10]
null
t2線程中止了
t1線程中止了
threadName:t4
threadName:t3
t4線程中止了
t3線程中止了
java.lang.ThreadGroup[name=thread-group-1,maxpri=10]
    java.lang.ThreadGroup[name=thread-group-2,maxpri=10]

用戶線程和守護線程

守護線程是一種特殊的線程,在後臺默默地完成一些系統性的服務,好比垃圾回收線程、JIT線程都是守護線程。與之對應的是用戶線程,用戶線程能夠理解爲是系統的工做線程,它會完成這個程序須要完成的業務操做。若是用戶線程所有結束了,意味着程序須要完成的業務操做已經結束了,系統能夠退出了。因此當系統只剩下守護進程的時候,java虛擬機會自動退出。

java線程分爲用戶線程和守護線程,線程的daemon屬性爲true表示是守護線程,false表示是用戶線程。

線程daemon的默認值

咱們看一下建立線程源碼,位於Thread類的init()方法中:

Thread parent = currentThread();
this.daemon = parent.isDaemon();

dameon的默認值爲爲父線程的daemon,也就是說,父線程若是爲用戶線程,子線程默認也是用戶現場,父線程若是是守護線程,子線程默認也是守護線程。

總結

  1. java中的線程分爲用戶線程和守護線程
  2. 程序中的全部的用戶線程結束以後,無論守護線程處於什麼狀態,java虛擬機都會自動退出
  3. 調用線程的實例方法setDaemon()來設置線程是不是守護線程
  4. setDaemon()方法必須在線程的start()方法以前調用,在後面調用會報異常,而且不起效
  5. 線程的daemon默認值和其父線程同樣

示例:

public class DaemonThreadTest {

    public static class T1 extends Thread {
        public T1(String name) {
            super(name);
        }

        @Override
        public void run() {
            System.out.println(this.getName() + " start ,isDaemon= "+isDaemon());
            while (true) {
                ;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        T1 t1 = new T1("T1");
        // 設置守護線程,須要在start()方法以前進行
        // t1.start()必須在setDaemon(true)以後,不然執行會報異常:Exception in thread "main" java.lang.IllegalThreadStateException
        //t1.start();
        //將t1線程設置爲守護線程
        t1.setDaemon(true);
        t1.start();
        //當程序中全部的用戶線程執行完畢以後,無論守護線程是否結束,系統都會自動退出。
        TimeUnit.SECONDS.sleep(1);
    }
}

疑問:

Q:JIT線程?

A: JIT通常指準時制。準時制生產方式(Just In Time簡稱JIT ).JIT線程在Java中表示即時編譯線程,解釋執行。

在Java編程語言和環境中,即時編譯器(JIT compiler,just-in-time compiler)是一個把Java的字節碼(包括須要被解釋的指令的程序)轉換成能夠直接發送給處理器的指令的程序。

線程安全和synchronized關鍵字

什麼是線程安全?

當多個線程去訪問同一個類(對象或方法)的時候,該類都能表現出一致的行爲,沒有意想不到的不一樣結果,那咱們就能夠所這個類是線程安全的。

形成線程安全問題的主要誘因有兩點:

  1. 一是存在共享數據(也稱臨界資源)
  2. 二是存在多條線程共同操做共享數據

爲了解決這個問題,當存在多個線程操做共享數據時,須要保證同一時刻有且只有一個線程在操做共享數據,這種方式有個高尚的名稱叫互斥鎖,在 Java 中,關鍵字 synchronized能夠保證在同一個時刻,只有一個線程能夠執行某個方法或者某個代碼塊(主要是對方法或者代碼塊中存在共享數據的操做),同時咱們還應該注意到synchronized另一個重要的做用,synchronized可保證一個線程的變化(主要是共享數據的變化)被其餘線程所看到(保證可見性,徹底能夠替代volatile功能)

鎖的互斥性表如今線程嘗試獲取的是不是同一個鎖,相同類型不一樣實例的對象鎖不互斥,而class類對象的鎖與實例鎖之間也不互斥。

synchronized主要有3種使用方式

  1. 修飾實例方法,做用於當前實例,進入同步代碼前須要先獲取實例的鎖
  2. 修飾靜態方法,做用於類的Class對象,進入修飾的靜態方法前須要先獲取類的Class對象的鎖
  3. 修飾代碼塊,須要指定加鎖對象(記作lockobj),在進入同步代碼塊前須要先獲取lockobj的鎖

synchronized做用於實例對象

synchronize做用於實例方法須要注意:

  1. 實例方法上加synchronized,線程安全的前提是,多個線程操做的是同一個實例,若是多個線程做用於不一樣的實例,那麼線程安全是沒法保證的
  2. 同一個實例的多個實例方法上有synchronized,這些方法都是互斥的,同一時間只容許一個線程操做同一個實例的其中的一個synchronized方法

synchronized做用於靜態方法

當synchronized做用於靜態方法時,鎖的對象就是當前類的Class對象。

synchronized同步代碼塊

方法體可能比較大,同時存在一些比較耗時的操做,而須要同步的代碼又只有一小部分時使用。加鎖時能夠使用自定義的對象做爲鎖,也能夠使用this對象(表明當前實例)或者當前類的class對象做爲鎖 。

疑問:

Q:synchronized可保證一個線程的變化(主要是共享數據的變化)被其餘線程所看到(保證可見性,徹底能夠替代volatile功能),synchronized是怎麼保證可見性的呢?

Q:同一個實例的多個實例方法上有synchronized,這些方法都是互斥的,同一時間只容許一個線程操做同一個實例的其中的一個synchronized方法.驗證同一時間只容許一個線程操做同一個實例的其中的一個synchronized方法是對的。

A:示例有下:

public class MethodObject {

    public synchronized void methodA() throws InterruptedException {
        System.out.println("methodA start");
        TimeUnit.SECONDS.sleep(10);
        System.out.println("methodA finish");
    }

    public synchronized void methodB() throws InterruptedException {
        System.out.println("methodB start");
        TimeUnit.SECONDS.sleep(5);
        System.out.println("methodB finish");
    }

}

public class SynchronousTest {

    public static void main(String[] args) throws InterruptedException {
        MethodObject mo = new MethodObject();
        T1 t1 = new T1("T1", mo);
        T2 t2 = new T2("T2", mo);
        t1.start();
        TimeUnit.MILLISECONDS.sleep(300);
        t2.start();
    }

    public static class T1 extends Thread {
        private  MethodObject mo;
        public T1(String name, MethodObject mo) {
            super(name);
            this.mo = mo;
        }

        @Override
        public void run() {
            try {
                mo.methodA();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static class T2 extends Thread {
        private  MethodObject mo;
        public T2(String name, MethodObject mo) {
            super(name);
            this.mo = mo;
        }

        @Override
        public void run() {
            try {
                mo.methodB();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

synchronized實現原理

深刻理解Java併發之synchronized實現原理

線程中斷的2種方式

一、經過一個volatile修飾的變量控制線程中斷

利用volatile控制的變量在多線程中的可見性,Java內存模型實現。

示例:

public class VolatileTest {

    //public static boolean flag = true;
    public static volatile boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        T1 t1 = new T1("T1");
        t1.start();
        //TimeUnit.SECONDS.sleep(3);
        Thread.sleep(3000);
        //將flag置爲false
        flag = false;
    }

    public static class T1 extends Thread {
        public T1(String name) {
            super(name);
        }

        @Override
        public void run() {
            System.out.println(this.getName() + " start");
            while (VolatileTest.flag) {
                ;
            }
            System.out.println(this.getName() + " end");
        }
    }
}

二、經過線程自帶的中斷標誌interrupt() 控制

當調用線程的interrupt()實例方法以後,線程的中斷標誌會被置爲true,能夠經過線程的實例方法isInterrupted()獲取線程的中斷標誌。

當運行的線程處於阻塞狀態時:

  1. 調用線程的interrupt()實例方法,線程的中斷標誌會被置爲true
  2. 當線程處於阻塞狀態時,調用線程的interrupt()實例方法,線程內部會觸發InterruptedException異常,而且會清除線程內部的中斷標誌(即將中斷標誌置爲false)

阻塞狀態處理方法:這時候應該在catch中再調用this.interrupt();一次,將中斷標誌置爲true。而後在run()方法中經過this.isInterrupted()來獲取線程的中斷標誌,退出循環break。

總結

  1. 當一個線程處於被阻塞狀態或者試圖執行一個阻塞操做時,能夠使用 Thread.interrupt()方式中斷該線程,注意此時將會拋出一個InterruptedException的異常,同時中斷狀態將會被複位(由中斷狀態改成非中斷狀態)。阻塞狀態線程要經過線程自帶的中斷標誌interrupt() 控制中斷。
  2. 內部有循環體,能夠經過一個變量來做爲一個信號控制線程是否中斷,注意變量須要volatile修飾。
  3. 文中的2種方式能夠結合起來靈活使用控制線程的中斷.

示例:

public class InterruptTest1 {

    public static class T1 extends Thread {
        public T1(String name) {
            super(name);
        }

        @Override
        public void run() {
            System.out.println(this.getName() + " start");
            while (true) {
                try {
                    //下面模擬阻塞代碼
                    TimeUnit.SECONDS.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    this.interrupt();
                }
                if (this.isInterrupted()) {
                    break;
                }
            }
            System.out.println(this.getName() + " end");
        }
    }

    public static void main(String[] args) throws InterruptedException {
        T1 t1 = new T1("thread1");
        t1.start();
        TimeUnit.SECONDS.sleep(2);
        t1.interrupt();
    }
}

輸出:

thread1 start
thread1 end
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at java.lang.Thread.sleep(Thread.java:340)
	at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
	at com.self.current.InterruptTest1$T1.run(InterruptTest1.java:27)

ReentrantLock重入鎖

synchronized的侷限性

synchronized是java內置的關鍵字,它提供了一種獨佔的加鎖方式。synchronized的獲取和釋放鎖由jvm實現,用戶不須要顯示的釋放鎖,很是方便,然而synchronized也有必定的侷限性,例如:

  1. 當線程嘗試獲取鎖的時候,若是獲取不到鎖會一直阻塞,這個阻塞的過程,用戶沒法控制。(synchronized不能響應中斷?)
  2. 若是獲取鎖的線程進入休眠或者阻塞,除非當前線程異常,不然其餘線程嘗試獲取鎖必須一直等待。(synchronized不能響應中斷?)

ReentrantLock

ReentrantLock是Lock的默認實現,在聊ReentranLock以前,咱們須要先弄清楚一些概念:

  1. 可重入鎖:可重入鎖是指同一個線程能夠屢次得到同一把鎖;ReentrantLock和關鍵字Synchronized都是可重入鎖
  2. 可中斷鎖:可中斷鎖是指線程在獲取鎖的過程當中,是否能夠響應線程中斷操做。synchronized是不可中斷的,ReentrantLock是可中斷的
  3. 公平鎖和非公平鎖:公平鎖是指多個線程嘗試獲取同一把鎖的時候,獲取鎖的順序按照線程到達的前後順序獲取,而不是隨機插隊的方式獲取。synchronized是非公平鎖,而ReentrantLock是兩種均可以實現,不過默認是非公平鎖。

ReentrantLock基本使用

ReentrantLock的使用過程:

  1. 建立鎖:ReentrantLock lock = new ReentrantLock();
  2. 獲取鎖:lock.lock()
  3. 釋放鎖:lock.unlock();

對比上面的代碼,與關鍵字synchronized相比,ReentrantLock鎖有明顯的操做過程,開發人員必須手動的指定什麼時候加鎖,什麼時候釋放鎖,正是由於這樣手動控制,ReentrantLock對邏輯控制的靈活度要遠遠勝於關鍵字synchronized,上面代碼須要注意lock.unlock()必定要放在finally中,不然若程序出現了異常,鎖沒有釋放,那麼其餘線程就再也沒有機會獲取這個鎖了。

ReentrantLock是可重入鎖

假如ReentrantLock是不可重入的鎖,那麼同一個線程第2次獲取鎖的時候因爲前面的鎖還未釋放而致使死鎖,程序是沒法正常結束的。

  1. lock()方法和unlock()方法須要成對出現,鎖了幾回,也要釋放幾回,不然後面的線程沒法獲取鎖了;能夠將add中的unlock刪除一個事實,上面代碼運行將沒法結束
  2. unlock()方法放在finally中執行,保證無論程序是否有異常,鎖一定會釋放

示例:

public class ReentrantLockTest {
    private static int num = 0;
    private static Lock lock = new ReentrantLock();
    public static void add() {
        lock.lock();
        lock.lock();
        try {
            num++;
        } finally {
            //lock()方法和unlock()方法須要成對出現,鎖了幾回,也要釋放幾回,不然後面的線程沒法獲取鎖
            lock.unlock();
            lock.unlock();
        }
    }
    public static class T extends Thread {
        public T(String name) {
            super(name);
        }
        @Override
        public void run() {
            for (int i = 0; i < 1000; i++) {
                ReentrantLockTest.add();
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        T t1 = new T("t1");
        T t2 = new T("t2");
        T t3 = new T("t3");
        t1.start();
        t2.start();
        t3.start();
        t1.join();
        t2.join();
        t3.join();
        System.out.println("get num =" + num);
    }
}
//輸出: get num =3000

ReentrantLock實現公平鎖

在大多數狀況下,鎖的申請都是非公平的。這就比如買票不排隊,上廁所不排隊。最終致使的結果是,有些人可能一直買不到票。而公平鎖,它會按照到達的前後順序得到資源。公平鎖的一大特色是不會產生飢餓現象,只要你排隊,最終仍是能夠等到資源的;synchronized關鍵字默認是有jvm內部實現控制的,是非公平鎖。而ReentrantLock運行開發者本身設置鎖的公平性,能夠實現公平和非公平鎖。

看一下jdk中ReentrantLock的源碼,2個構造方法:

public ReentrantLock() {    sync = new NonfairSync();}
public ReentrantLock(boolean fair) {    sync = fair ? new FairSync() : new NonfairSync();}

默認構造方法建立的是非公平鎖。

第2個構造方法,有個fair參數,當fair爲true的時候建立的是公平鎖,公平鎖看起來很不錯,不過要實現公平鎖,系統內部確定須要維護一個有序隊列,所以公平鎖的實現成本比較高,性能相對於非公平鎖來講相對低一些。所以,在默認狀況下,鎖是非公平的,若是沒有特別要求,則不建議使用公平鎖。

示例:

public class ReentrantLockFairTest {
    private static int num = 0;
    //private static Lock lock = new ReentrantLock(false);
    private static Lock lock = new ReentrantLock(true);
    public static class T extends Thread {
        public T(String name) {
            super(name);
        }
        @Override
        public void run() {
            for (int i = 0; i < 3; i++) {
                lock.lock();
                try {
                    System.out.println(Thread.currentThread().getName()+" got lock");
                } finally {
                    lock.unlock();
                }
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        T t1 = new T("t1");
        T t2 = new T("t2");
        T t3 = new T("t3");
        t1.start();
        t2.start();
        t3.start();
    }
}

輸出:

公平鎖:
t1 got lock                          
t1 got lock
t2 got lock
t2 got lock
t3 got lock
t3 got lock
非公平鎖:
t1 got lock
t3 got lock
t3 got lock
t2 got lock
t2 got lock
t1 got lock

ReentrantLock獲取鎖的過程是可中斷的——使用lockInterruptibly()和tryLock(long time, TimeUnit unit)有參方法時。

對於synchronized關鍵字,若是一個線程在等待獲取鎖,最終只有2種結果:

  1. 要麼獲取到鎖而後繼續後面的操做
  2. 要麼一直等待,直到其餘線程釋放鎖爲止

而ReentrantLock提供了另一種可能,就是在等的獲取鎖的過程當中(發起獲取鎖請求到還未獲取到鎖這段時間內)是能夠被中斷的,也就是說在等待鎖的過程當中,程序能夠根據須要取消獲取鎖的請求。拿李雲龍平安縣圍點打援來講,當平安縣城被拿下後,鬼子救援的部隊再嘗試救援已經沒有意義了,這時候要請求中斷操做。

關於獲取鎖的過程當中被中斷,注意幾點:

  1. ReentrankLock中必須使用實例方法 lockInterruptibly()獲取鎖時,在線程調用interrupt()方法以後,纔會引起 InterruptedException異常
  2. 線程調用interrupt()以後,線程的中斷標誌會被置爲true
  3. 觸發InterruptedException異常以後,線程的中斷標誌有會被清空,即置爲false
  4. 因此當線程調用interrupt()引起InterruptedException異常,中斷標誌的變化是:false->true->false

實例:

public class InterruptTest2 {
    private static ReentrantLock lock1 = new ReentrantLock();
    private static ReentrantLock lock2 = new ReentrantLock();

    public static class T1 extends Thread {
        int lock;

        public T1(String name, Integer lock) {
            super(name);
            this.lock = lock;
        }

        @Override
        public void run() {
            try {
                if (lock == 1) {
                    lock1.lockInterruptibly();
                    TimeUnit.SECONDS.sleep(1);
                    lock2.lockInterruptibly();
                } else {
                    lock2.lockInterruptibly();
                    TimeUnit.SECONDS.sleep(1);
                    lock1.lockInterruptibly();
                }
            } catch (InterruptedException e) {
                //線程發送中斷信號觸發InterruptedException異常以後,中斷標誌將被清空。
                System.out.println(this.getName() + "中斷標誌:" + this.isInterrupted());
                e.printStackTrace();
            } finally {
                //ReentrantLock自有的方法,多態實現的Lock不能用
                if (lock1.isHeldByCurrentThread()) {
                    lock1.unlock();
                }
                if (lock2.isHeldByCurrentThread()) {
                    lock2.unlock();
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        T1 t1 = new T1("thread1", 1);
        T1 t2 = new T1("thread2", 2);
        t1.start();
        t2.start();
        TimeUnit.SECONDS.sleep(1000);
        //不加interrupt()經過jstack查看線程堆棧信息,發現2個線程死鎖了
        //"thread2":
        //  waiting for ownable synchronizer 0x000000076b782028, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
        //  which is held by "thread1"
        //"thread1":
        //  waiting for ownable synchronizer 0x000000076b782058, (a java.util.concurrent.locks.ReentrantLock$NonfairSync),
        //  which is held by "thread2"
        t1.interrupt();
    }
}

ReentrantLock鎖申請等待限時

ReentrantLock恰好提供了這樣功能,給咱們提供了獲取鎖限時等待的方法 tryLock(),能夠選擇傳入時間參數,表示等待指定的時間,無參則表示當即返回鎖申請的結果:true表示獲取鎖成功,false表示獲取鎖失敗。

tryLock無參方法——tryLock()是當即響應的,中間不會有阻塞。

看一下源碼中tryLock方法:

public boolean tryLock()

tryLock有參方法

該方法在指定的時間內無論是否能夠獲取鎖,都會返回結果,返回true,表示獲取鎖成功,返回false表示獲取失敗。 此方法在執行的過程當中,若是調用了線程的中斷interrupt()方法,會觸發InterruptedException異常。

能夠明確設置獲取鎖的超時時間:

public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException

關於tryLock()方法和tryLock(long timeout, TimeUnit unit)方法,說明一下:

  1. 都會返回boolean值,結果表示獲取鎖是否成功。
  2. tryLock()方法,無論是否獲取成功,都會當即返回;而有參的tryLock方法會嘗試在指定的時間內去獲取鎖,中間會阻塞的現象,在指定的時間以後會無論是否可以獲取鎖都會返回結果。
  3. tryLock()方法不會響應線程的中斷方法;而有參的tryLock方法會響應線程的中斷方法,而出發 InterruptedException異常,這個從2個方法的聲明上能夠能夠看出來。

ReentrantLock其餘經常使用的方法

  1. isHeldByCurrentThread:實例方法,判斷當前線程是否持有ReentrantLock的鎖,上面代碼中有使用過。

獲取鎖的4種方法對比

獲取鎖的方法 是否當即響應(不會阻塞) 是否響應中斷
lock() × ×
lockInterruptibly() × √
tryLock() √ ×
tryLock(long timeout, TimeUnit unit) × √

實例:

public class ReentrantLockTest1 {
    private static ReentrantLock lock1 = new ReentrantLock();

    public static class T extends Thread {
        public T(String name) {
            super(name);
        }
        @Override
        public void run() {
            try {
                System.out.println(this.getName()+"嘗試獲取鎖");
                if (lock1.tryLock(2,TimeUnit.SECONDS)){
                    System.out.println(this.getName()+"獲取鎖成功");
                    TimeUnit.SECONDS.sleep(3);
                }else {
                    System.out.println(this.getName()+"獲取鎖失敗");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if(lock1.isHeldByCurrentThread()){
                    lock1.unlock();
                }
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        T t1 = new T("t1");
        T t2 = new T("t2");
        t1.start();
        t2.start();

    }
}

輸出:

lock1.tryLock()
t1嘗試獲取鎖
t1獲取鎖成功
t2嘗試獲取鎖
t2獲取鎖失敗
lock1.tryLock(2,TimeUnit.SECONDS)
t1嘗試獲取鎖
t2嘗試獲取鎖
t1獲取鎖成功
t2獲取鎖失敗

總結

  1. ReentrantLock能夠實現公平鎖和非公平鎖
  2. ReentrantLock默認實現的是非公平鎖
  3. ReentrantLock的獲取鎖和釋放鎖必須成對出現,鎖了幾回,也要釋放幾回
  4. 釋放鎖的操做必須放在finally中執行
  5. lockInterruptibly()實例方法能夠響應線程的中斷方法,調用線程的interrupt()方法時,lockInterruptibly()方法會觸發 InterruptedException異常
  6. 關於 InterruptedException異常說一下,看到方法聲明上帶有 throwsInterruptedException,表示該方法能夠相應線程中斷,調用線程的interrupt()方法時,這些方法會觸發 InterruptedException異常,觸發InterruptedException時,線程的中斷中斷狀態會被清除。因此若是程序因爲調用 interrupt()方法而觸發 InterruptedException異常,線程的標誌由默認的false變爲ture,而後又變爲false
  7. 實例方法tryLock()獲會嘗試獲取鎖,會當即返回,返回值表示是否獲取成功
  8. 實例方法tryLock(long timeout, TimeUnit unit)會在指定的時間內嘗試獲取鎖,指定的時間內是否可以獲取鎖,都會返回,返回值表示是否獲取鎖成功,該方法會響應線程的中斷

疑問

Q:可中斷鎖:可中斷鎖時只線程在獲取鎖的過程當中,是否能夠相應線程中斷操做。爲何synchronized是不可中斷的,ReentrantLock是可中斷的?

JUC中的Condition對象

Condition使用簡介——實現等待/通知機制

注意:在使用使用Condition.await()方法時,須要先獲取Condition對象關聯的ReentrantLock的鎖;就像使用Object.wait()時必須在synchronized同步代碼塊內。

從總體上來看Object的wait和notify/notify是與對象監視器配合完成線程間的等待/通知機制,而Condition與Lock配合完成等待通知機制,前者是java底層級別的,後者是語言級別的,具備更高的可控制性和擴展性。二者除了在使用方式上不一樣外,在功能特性上仍是有不少的不一樣:

  1. Condition可以支持不響應中斷,而經過使用Object方式不支持
  2. Condition可以支持多個等待隊列(new 多個Condition對象),而Object方式只能支持一個
  3. Condition可以支持超時時間的設置,而Object不支持

Condition由ReentrantLock對象建立,而且能夠同時建立多個,Condition接口在使用前必須先調用ReentrantLock的lock()方法得到鎖,以後調用Condition接口的await()將釋放鎖,而且在該Condition上等待,直到有其餘線程調用Condition的signal()方法喚醒線程,使用方式和wait()、notify()相似。

須要注意的時,當一個線程被signal()方法喚醒線程時,它第一個動做是去獲取同步鎖,注意這一點,而這把鎖目前在調用signal()方法喚醒他的線程上,必須等其釋放鎖後才能獲得爭搶鎖的機會。

實例:

public class ConditionTest {

    private static ReentrantLock lock = new ReentrantLock();
    private static Condition condition = lock.newCondition();

    public static void main(String[] args) {
        T1 t1 = new T1("TT1");
        T2 t2 = new T2("TT2");
        t1.start();
        t2.start();
    }

    static class T1 extends Thread {
        public T1(String name) {
            super(name);
        }

        @Override
        public void run() {
            lock.lock();
            System.out.println(this.getName() + " start");
            try {
                System.out.println(this.getName() + " wait");
                condition.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
            System.out.println(this.getName() + " end");
        }
    }

    static class T2 extends Thread {
        public T2(String name) {
            super(name);
        }

        @Override
        public void run() {
            lock.lock();
            System.out.println(this.getName() + " start");
            System.out.println(this.getName() + " signal");
            condition.signal();
            System.out.println(this.getName() + " end");
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(this.getName() + " end,2 second later");
        }
    }
}

輸出:

TT1 start
TT1 wait
TT2 start
TT2 signal
TT2 end
TT2 end,2 second later

Condition經常使用方法

和Object中wait相似的方法

  1. void await() throws InterruptedException:當前線程進入等待狀態,若是在等待狀態中被中斷會拋出被中斷異常;
  2. long awaitNanos(long nanosTimeout):當前線程進入等待狀態直到被通知,中斷或者超時;
  3. boolean await(long time, TimeUnit unit) throws InterruptedException:同第二種,支持自定義時間單位,false:表示方法超時以後自動返回的,true:表示等待還未超時時,await方法就返回了(超時以前,被其餘線程喚醒了)
  4. boolean awaitUntil(Date deadline) throws InterruptedException:當前線程進入等待狀態直到被通知,中斷或者到了某個時間
  5. void awaitUninterruptibly();:當前線程進入等待狀態,不會響應線程中斷操做,只能經過喚醒的方式讓線程繼續

和Object的notify/notifyAll相似的方法

  1. void signal():喚醒一個等待在condition上的線程,將該線程從等待隊列中轉移到同步隊列中,若是在同步隊列中可以競爭到Lock則能夠從等待方法中返回。
  2. void signalAll():與1的區別在於可以喚醒全部等待在condition上的線程

Condition.await()過程當中被打斷

調用condition.await()以後,線程進入阻塞中,調用t1.interrupt(),給t1線程發送中斷信號,await()方法內部會檢測到線程中斷信號,而後觸發 InterruptedException異常,線程中斷標誌被清除。從輸出結果中能夠看出,線程t1中斷標誌的變換過程:false->true->false

await(long time, TimeUnit unit)超時以後自動返回

t1線程等待2秒以後,自動返回繼續執行,最後await方法返回false,await返回false表示超時以後自動返回

await(long time, TimeUnit unit)超時以前被喚醒

t1線程中調用 condition.await(5,TimeUnit.SECONDS);方法會釋放鎖,等待5秒,主線程休眠1秒,而後獲取鎖,以後調用signal()方法喚醒t1,輸出結果中發現await後過了1秒(一、3行輸出結果的時間差),await方法就返回了,而且返回值是true。true表示await方法超時以前被其餘線程喚醒了。

long awaitNanos(long nanosTimeout)超時返回

t1調用await方法等待5秒超時返回,返回結果爲負數,表示超時以後返回的。

//awaitNanos參數爲納秒,能夠調用TimeUnit中的一些方法將時間轉換爲納秒。
long nanos = TimeUnit.SECONDS.toNanos(2);

waitNanos(long nanosTimeout)超時以前被喚醒

t1中調用await休眠5秒,主線程休眠1秒以後,調用signal()喚醒線程t1,await方法返回正數,表示返回時距離超時時間還有多久,將近4秒,返回正數表示,線程在超時以前被喚醒了。

其餘幾個有參的await方法和無參的await方法同樣,線程調用interrupt()方法時,這些方法都會觸發InterruptedException異常,而且線程的中斷標誌會被清除。

同一個鎖支持建立多個Condition

使用兩個Condition來實現一個阻塞隊列的例子:

public class MyBlockingQueue<E> {

    //阻塞隊列最大容量
    private int size;
    //隊列底層實現
    private LinkedList<E> list = new LinkedList<>();
    private static Lock lock = new ReentrantLock();
    //隊列滿時的等待條件
    private static Condition fullFlag = lock.newCondition();
    //隊列空時的等待條件
    private static Condition emptyFlag = lock.newCondition();

    public MyBlockingQueue(int size) {
        this.size = size;
    }

    public void enqueue(E e) throws InterruptedException {
        lock.lock();
        try {
            //隊列已滿,在fullFlag條件上等待
            while (list.size() == size) {
                fullFlag.await();
            }
            //入隊:加入鏈表末尾
            list.add(e);
            System.out.println("生產了" + e);
            //通知在emptyFlag條件上等待的線程
            emptyFlag.signal();
        } finally {
            lock.unlock();
        }
    }


    public E dequeue() throws InterruptedException {
        lock.lock();
        try {
            while (list.size() == 0) {
                emptyFlag.await();
            }
            E e = list.removeFirst();
            System.out.println("消費了" + e);
            //通知在fullFlag條件上等待的線程
            fullFlag.signal();
            return e;
        } finally {
            lock.unlock();
        }
    }

    /**
     * 建立了一個阻塞隊列,大小爲3,隊列滿的時候,會被阻塞,等待其餘線程去消費,隊列中的元素被消費以後,會喚醒生產者,生產數據進入隊列。上面代碼將隊列大小置爲1,能夠實現同步阻塞隊列,生產1個元素以後,生產者會被阻塞,待消費者消費隊列中的元素以後,生產者才能繼續工做。
     * @param args
     */
    public static void main(String[] args) {
        MyBlockingQueue<Integer> queue = new MyBlockingQueue<>(1);
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            Thread producer = new Thread(() -> {
                try {
                    queue.enqueue(finalI);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            producer.start();
        }
        for (int i = 0; i < 10; i++) {
            Thread consumer = new Thread(() -> {
                try {
                    queue.dequeue();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            consumer.start();
        }
    }
}

輸出:

生產了0
消費了0
生產了1
消費了1
。。。。
生產了9
消費了9

Object的監視器方法與Condition接口的對比

注意同步隊列和等待隊列的區別,同步隊列表示在競爭一把鎖的隊列中,是處於阻塞或運行狀態的隊列。

而等待隊列是指被置爲等待、超時等待狀態的線程,這些是沒有競爭鎖的權限的,處於等待被喚醒的狀態中。

對比項 Object 監視器方法 Condition
前置條件 獲取對象的鎖 調用Lock.lock獲取鎖,調用Lock.newCondition()獲取Condition對象
調用方式 直接調用,如:object.wait() 直接調用,如:condition.await()
等待隊列個數 一個 多個,使用多個condition實現
當前線程釋放鎖並進入等待狀態 支持 支持
當前線程釋放鎖進入等待狀態中不響應中斷 不支持 支持
當前線程釋放鎖並進入超時等待狀態 支持 支持
當前線程釋放鎖並進入等待狀態到未來某個時間 不支持 支持
喚醒等待隊列中的一個線程 支持 支持
喚醒等待隊列中的所有線程 支持 支持

總結

  1. 使用condition的步驟:建立condition對象,獲取鎖,而後調用condition的方法
  2. 一個ReentrantLock支持建立多個condition對象
  3. void await() throws InterruptedException;方法會釋放鎖,讓當前線程等待,支持喚醒,支持線程中斷
  4. void awaitUninterruptibly();方法會釋放鎖,讓當前線程等待,支持喚醒,不支持線程中斷
  5. long awaitNanos(longnanosTimeout)throws InterruptedException;參數爲納秒,此方法會釋放鎖,讓當前線程等待,支持喚醒,支持中斷。超時以後返回的,結果爲負數;超時以前被喚醒返回的,結果爲正數(表示返回時距離超時時間相差的納秒數)
  6. boolean await (longtime,TimeUnitunit)throws InterruptedException;方法會釋放鎖,讓當前線程等待,支持喚醒,支持中斷。超時以後返回的,結果爲false;超時以前被喚醒返回的,結果爲true
  7. boolean awaitUntil(Datedeadline)throws InterruptedException;參數表示超時的截止時間點,方法會釋放鎖,讓當前線程等待,支持喚醒,支持中斷。超時以後返回的,結果爲false;超時以前被喚醒返回的,結果爲true
  8. void signal();會喚醒一個等待中的線程,而後被喚醒的線程會被加入同步隊列,去嘗試獲取鎖
  9. void signalAll();會喚醒全部等待中的線程,將全部等待中的線程加入同步隊列,而後去嘗試獲取鎖

疑問:

Q:Condition可以支持超時時間的設置,而Object不支持。Object不是有wait(long timeout)超時時間設置麼?這句話是否是錯了?

Q:在Condition.await()方法被調用時,當前線程會釋放這個鎖,而且當前線程會進行等待(處於阻塞狀態)?是阻塞狀態不是等待狀態?是由於這事api語言級別的等待/通知機制麼?

JUC中的LockSupport工具類——無需加鎖及考慮等待通知順序

使用Object類中的方法實現線程等待和喚醒

關於Object類中的用戶線程等待和喚醒的方法,總結一下:

  1. wait()/notify()/notifyAll()方法都必須放在同步代碼(必須在synchronized內部執行)中執行,須要先獲取鎖(不然拋出了 IllegalMonitorStateException異常 )
  2. 線程喚醒的方法(notify、notifyAll)須要在等待的方法(wait)以後執行,等待中的線程纔可能會被喚醒,不然沒法喚醒

使用Condition實現線程的等待和喚醒

關於Condition中方法使用總結:

  1. 使用Condtion中的線程等待和喚醒方法以前,須要先獲取鎖。否者會報 IllegalMonitorStateException異常
  2. signal()方法先於await()方法以前調用,線程沒法被喚醒

Object和Condition的侷限性

關於Object和Condtion中線程等待和喚醒的侷限性,有如下幾點:

  1. 兩種方式中的讓線程等待和喚醒的方法可以執行的先決條件是:線程須要先獲取鎖
  2. 喚醒方法須要在等待方法以後調用,線程纔可以被喚醒

關於這2點,LockSupport都不須要,就能實現線程的等待和喚醒。

LockSupport類介紹

LockSupport類能夠阻塞當前線程以及喚醒指定被阻塞的線程。主要是經過park()和unpark(thread)方法來實現阻塞和喚醒線程的操做的。(注意park方法等待不釋放鎖)

每一個線程都有一個許可(permit),permit只有兩個值1和0,默認是0。

  1. 當調用unpark(thread)方法,就會將thread線程的許可permit設置成1(注意屢次調用unpark方法,不會累加,permit值仍是1)。
  2. 當調用park()方法,若是當前線程的permit是1,那麼將permit設置爲0,並當即返回。若是當前線程的permit是0,那麼當前線程就會阻塞,直到別的線程將當前線程的permit設置爲1時,park方法會被喚醒,而後會將permit再次設置爲0,並返回。

注意:由於permit默認是0,因此一開始調用park()方法,線程一定會被阻塞。調用unpark(thread)方法後,會自動喚醒thread線程,即park方法當即返回。

LockSupport中經常使用的方法

阻塞線程

  • void park():阻塞當前線程,若是調用unpark方法或者當前線程被中斷,從能從park()方法中返回
  • void park(Object blocker):功能同方法1,入參增長一個Object對象,用來記錄致使線程阻塞的阻塞對象,方便進行問題排查
  • void parkNanos(long nanos):阻塞當前線程,最長不超過nanos納秒,增長了超時返回的特性
  • void parkNanos(Object blocker, long nanos):功能同方法3,入參增長一個Object對象,用來記錄致使線程阻塞的阻塞對象,方便進行問題排查
  • void parkUntil(long deadline):阻塞當前線程,直到deadline,deadline是一個絕對時間,表示某個時間的毫秒格式
  • void parkUntil(Object blocker, long deadline):功能同方法5,入參增長一個Object對象,用來記錄致使線程阻塞的阻塞對象,方便進行問題排查;

喚醒線程

  • void unpark(Thread thread):喚醒處於阻塞狀態的指定線程

一、LockSupport調用park、unpark方法執行喚醒等待無需加鎖。

二、LockSupport中,喚醒的方法無論是在等待以前仍是在等待以後調用,線程都可以被喚醒。 喚醒方法在等待方法以前執行,線程也可以被喚醒,這點是另外兩種方法沒法作到的。而Object和Condition中的喚醒必須在等待以後調用,線程才能被喚醒。

三、park方法能夠相應線程中斷。

LockSupport.park方法讓線程等待以後,喚醒方式有2種:

  1. 調用LockSupport.unpark方法
  2. 調用等待線程的 interrupt()方法,給等待的線程發送中斷信號,能夠喚醒線程

線程t1和t2的不一樣點是,t2中調用park方法傳入了一個BlockerDemo對象,從上面的線程堆棧信息中,發現t2線程的堆棧信息中多了一行 -parking to waitfor<0x00000007180bfeb0>(a com.itsoku.chat10.Demo10$BlockerDemo),恰好是傳入的BlockerDemo對象,park傳入的這個參數可讓咱們在線程堆棧信息中方便排查問題,其餘暫無他用。

線程等待和喚醒的3種方式作個對比

  1. 方式1:Object中的wait、notify、notifyAll方法
  2. 方式2:juc中Condition接口提供的await、signal、signalAll方法
  3. 方式3:juc中的LockSupport提供的park、unpark方法

3種方式對比:

Object            	Condtion   	LockSupport

前置條件 須要在synchronized中運行 須要先獲取Lock的鎖 無
無限等待 支持 支持 支持
超時等待 支持 支持 支持
等待到未來某個時間返回 不支持 支持 支持
等待狀態中釋放鎖 會釋放 會釋放 不會釋放
喚醒方法先於等待方法執行,可否喚醒線程 否 否 能夠
是否能響應線程中斷 是 是 是
線程中斷是否會清除中斷標誌 是 是 否
是否支持等待狀態中不響應中斷 不支持 支持 不支持

實例:

public class LockSupportTest {

    /**
     * 輸出:
     * create a thread start
     * 主線程執行完畢!
     * thread 被喚醒
     */
    public static void main1(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            System.out.println("create a thread start");
            LockSupport.park();
            System.out.println("thread 被喚醒");
        });
        t.start();

        TimeUnit.SECONDS.sleep(3);
        //喚醒處於阻塞狀態的指定線程
        LockSupport.unpark(t);
        //響應線程中斷
        //t.interrupt();
        System.out.println("主線程執行完畢!");
    }

    /**
     * 喚醒方法在等待方法以前執行,線程也可以被喚醒
     *輸出:
     * create a thread start
     * 主線程執行完畢!
     * thread 被喚醒
     */
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            System.out.println("create a thread start");
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            LockSupport.park();
            System.out.println("thread 被喚醒");
        });
        t.start();

        TimeUnit.SECONDS.sleep(1);
        //喚醒處於阻塞狀態的指定線程
        LockSupport.unpark(t);
        //響應線程中斷
        //t.interrupt();
        System.out.println("主線程執行完畢!");
    }
}

疑問:

Q:LockSupport類能夠阻塞當前線程以及喚醒指定被阻塞的線程。主要是經過park()和unpark(thread)方法來實現阻塞和喚醒線程的操做的。(注意park方法等待不釋放鎖)不釋放鎖的等待喚醒是在什麼場景下使用?爲何要這樣?

JUC中的Semaphore(信號量)——多把鎖控制,用於限流

synchronized和重入鎖ReentrantLock,這2種鎖一次都只能容許一個線程訪問一個資源,而信號量能夠控制有多少個線程能夠訪問特定的資源。

Semaphore經常使用場景:限流

舉個例子:

好比有個停車場(臨界值,共享資源),有5個空位,門口有個門衛,手中5把鑰匙分別對應5個車位上面的鎖,來一輛車,門衛會給司機一把鑰匙,而後進去找到對應的車位停下來,出去的時候司機將鑰匙歸還給門衛。停車場生意比較好,同時來了100兩車,門衛手中只有5把鑰匙,同時只能放5輛車進入,其餘車只能等待,等有人將鑰匙歸還給門衛以後,才能讓其餘車輛進入。

上面的例子中門衛就至關於Semaphore,車鑰匙就至關於許可證,車就至關於線程。

Semaphore主要方法

Semaphore(int permits):構造方法,參數表示許可證數量,用來建立信號量

Semaphore(int permits,boolean fair):構造方法,當fair等於true時,建立具備給定許可數的計數信號量並設置爲公平信號量

void acquire() throws InterruptedException:今後信號量獲取1個許可前線程將一直阻塞,至關於一輛車佔了一個車位,此方法會響應線程中斷,表示調用線程的interrupt方法,會使該方法拋出InterruptedException異常

void acquire(int permits) throws InterruptedException :和acquire()方法相似,參數表示須要獲取許可的數量;好比一個大卡車要入停車場,因爲車比較大,須要申請3個車位才能夠停放

void acquireUninterruptibly(int permits) :和acquire(int permits) 方法相似,只是不會響應線程中斷

boolean tryAcquire():嘗試獲取1個許可,無論是否可以獲取成功,都當即返回,true表示獲取成功,false表示獲取失敗

boolean tryAcquire(int permits):和tryAcquire(),表示嘗試獲取permits個許可

boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException:嘗試在指定的時間內獲取1個許可,獲取成功返回true,指定的時間事後仍是沒法獲取許可,返回false

boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException:和tryAcquire(long timeout, TimeUnit unit)相似,多了一個permits參數,表示嘗試獲取permits個許可

void release():釋放一個許可,將其返回給信號量,至關於車從停車場出去時將鑰匙歸還給門衛

void release(int n):釋放n個許可

int availablePermits():當前可用的許可數

獲取許可以後不釋放

取許可後,沒有釋放許可的代碼,最終致使,可用許可數量爲0,其餘線程沒法獲取許可,會在 semaphore.acquire();處等待,致使程序沒法結束。

沒有獲取到許可卻執行釋放(沒有獲取到許可卻在finally中直接執行release方法)

若是獲取鎖的過程當中發生異常,致使獲取鎖失敗,最後finally裏面也釋放了許可,最終會致使許可數量憑空增加了。

釋放許可正確的姿式

程序中增長了一個變量 acquireSuccess用來標記獲取許但是否成功,在finally中根據這個變量是否爲true,來肯定是否釋放許可。

在規定的時間內但願獲取許可

Semaphore內部2個方法能夠提供超時獲取許可的功能:

public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedExceptionpublic boolean tryAcquire(int permits, long timeout, TimeUnit unit)        throws InterruptedException

在指定的時間內去嘗試獲取許可,若是可以獲取到,返回true,獲取不到返回false。

其餘一些使用說明

  1. Semaphore默認建立的是非公平的信號量,什麼意思呢?這個涉及到公平與非公平。讓新來的去排隊就表示公平,直接去插隊爭搶第一個,就表示不公平。對於停車場,排隊確定更好一些。不過對於信號量來講不公平的效率更高一些,因此默認是不公平的。
  2. 方法中帶有 throwsInterruptedException聲明的,表示這個方法會響應線程中斷信號,什麼意思?表示調用線程的 interrupt()方法後,會讓這些方法觸發 InterruptedException異常,即便這些方法處於阻塞狀態,也會當即返回,並拋出 InterruptedException異常,線程中斷信號也會被清除。

示例:

public class SemaphoreTest {

    private static Semaphore semaphore = new Semaphore(2);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 5; i++) {
            Thread t = new T1("T" + i);
            t.start();
            if (i > 2) {
                TimeUnit.SECONDS.sleep(1);
                t.interrupt();
            }
        }
    }

    public static void main2(String[] args) {
        for (int i = 0; i < 5; i++) {
            Thread t = new T2("T" + i);
            t.start();
        }
    }

    /**
     * 在指定的時間內去嘗試獲取許可,若是可以獲取到,返回true,獲取不到返回false。
     */
    public static class T1 extends Thread {
        public T1(String name) {
            super(name);
        }

        @Override
        public void run() {
            Boolean hasTicket = false;
            Thread thread = Thread.currentThread();
            try {
                //semaphore.acquire();
                hasTicket = semaphore.tryAcquire(1, TimeUnit.SECONDS);
                if (hasTicket) {
                    System.out.println(thread + "獲取到停車位!");
                } else {
                    System.out.println(thread + "獲取不到停車位!走了");
                }
                //hasTicket = true;
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if (hasTicket) {
                    semaphore.release();
                    //沒有獲取到許可卻在finally中直接執行release方法
                    //Thread[T6,5,main]離開停車位!當前空餘停車位數量10
                    System.out.println(thread + "離開停車位!當前空餘停車位數量" + semaphore.availablePermits());
                }
            }
        }
    }

    /**
     * 正確的釋放鎖的方式
     */
    public static class T2 extends Thread {
        public T2(String name) {
            super(name);
        }

        @Override
        public void run() {
            Boolean hasTicket = false;
            Thread thread = Thread.currentThread();
            try {
                semaphore.acquire();
                hasTicket = true;
                System.out.println(thread + "獲取到停車位!");
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if (hasTicket) {
                    semaphore.release();
                    System.out.println(thread + "離開停車位!當前空餘停車位數量" + semaphore.availablePermits());
                }
            }
        }
    }
}

輸出:

test1
Thread[T0,5,main]獲取到停車位!
Thread[T1,5,main]獲取到停車位!
Thread[T2,5,main]獲取不到停車位!走了
java.lang.InterruptedException
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireSharedNanos(AbstractQueuedSynchronizer.java:1039)
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.tryAcquireSharedNanos(AbstractQueuedSynchronizer.java:1328)
	at java.util.concurrent.Semaphore.tryAcquire(Semaphore.java:409)
	at com.self.current.SemaphoreTest$T1.run(SemaphoreTest.java:52)
java.lang.InterruptedException: sleep interrupted
Thread[T1,5,main]離開停車位!當前空餘停車位數量1
	at java.lang.Thread.sleep(Native Method)
Thread[T4,5,main]獲取到停車位!
	at java.lang.Thread.sleep(Thread.java:340)
Thread[T0,5,main]離開停車位!當前空餘停車位數量1
	at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
Thread[T4,5,main]離開停車位!當前空餘停車位數量2
	at com.self.current.SemaphoreTest$T1.run(SemaphoreTest.java:59)

test2
Thread[T0,5,main]獲取到停車位!
Thread[T1,5,main]獲取到停車位!
Thread[T1,5,main]離開停車位!當前空餘停車位數量2
Thread[T2,5,main]獲取到停車位!
Thread[T0,5,main]離開停車位!當前空餘停車位數量2
Thread[T3,5,main]獲取到停車位!
Thread[T2,5,main]離開停車位!當前空餘停車位數量1
Thread[T4,5,main]獲取到停車位!
Thread[T3,5,main]離開停車位!當前空餘停車位數量1
Thread[T4,5,main]離開停車位!當前空餘停車位數量2

示例2:

經過判斷byteBuffer != null得出是否獲取到了許可。
semaphore = new Semaphore(maxBufferCount);
 byteBuffer = allocator.allocate();// semaphore.acquire();
  finally {
            if (byteBuffer != null) {
                byteBuffer.clear();
                allocator.release(byteBuffer);
            }
        }

疑問:

Q:Semaphore內部排隊等待資源的隊列是怎麼實現的,公平信號量與非公平的隊列類型都是哪一種的?

JUC中等待多線程完成的工具類CountDownLatch(閉鎖 )——等待多線程完成後執行操做或者實現最大的併發線程數同時執行

CountDownLatch介紹

CountDownLatch稱之爲閉鎖,它能夠使一個或一批線程在閉鎖上等待,等到其餘線程執行完相應操做後,閉鎖打開,這些等待的線程才能夠繼續執行。確切的說,閉鎖在內部維護了一個倒計數器。經過該計數器的值來決定閉鎖的狀態,從而決定是否容許等待的線程繼續執行。

一批線程等待閉鎖通常用於同步併發,如跑步比賽時做爲發令槍做用;

一個線程等待閉鎖通常用於等待併發線程或資源的獲取知足,用於執行收尾工做,如多任務執行完後合併結果等。

經常使用方法:

public CountDownLatch(int count):構造方法,count表示計數器的值,不能小於0,否者會報異常。

public void await() throws InterruptedException:調用await()會讓當前線程等待,直到計數器爲0的時候,方法纔會返回,此方法會響應線程中斷操做。

public boolean await(long timeout, TimeUnit unit) throws InterruptedException:限時等待,在超時以前,計數器變爲了0,方法返回true,否者直到超時,返回false,此方法會響應線程中斷操做。

public void countDown():讓計數器減1

CountDownLatch(做爲一個參數,鎖傳遞到方法中)使用步驟:

  1. 建立CountDownLatch對象
  2. 調用其實例方法 await(),讓當前線程等待
  3. 調用 countDown()方法,讓計數器減1
  4. 當計數器變爲0的時候, await()方法會返回

假若有這樣一個需求,當咱們須要解析一個Excel裏多個sheet的數據時,能夠考慮使用多線程,每一個線程解析一個sheet裏的數據,等到全部的sheet都解析完以後,程序須要統計解析總耗時。分析一下:解析每一個sheet耗時可能不同,總耗時就是最長耗時的那個操做。

方法一:使用join實現。

此方法會讓當前線程等待被調用的線程完成以後才能繼續。能夠看一下join的源碼,內部實際上是在synchronized方法中調用了線程的wait方法,最後被調用的線程執行完畢以後,由jvm自動調用其notifyAll()方法,喚醒全部等待中的線程。這個notifyAll()方法是由jvm內部自動調用的,jdk源碼中是看不到的,須要看jvm源碼,有興趣的同窗能夠去查一下。因此JDK不推薦在線程上調用wait、notify、notifyAll方法。

方法二:使用CountDownLatch實現。

示例:

public class CountDownLatchTest {

    public static void main(String[] args) throws InterruptedException {
        long start = System.currentTimeMillis();
        CountDownLatch latch = new CountDownLatch(2);
        T1 t1 = new T1("sheet1", 3, latch);
        T1 t2 = new T1("sheet2", 5, latch);
        t1.start();
        t2.start();
        //調用await()會讓當前線程等待,直到計數器爲0的時候,方法纔會返回,此方法會響應線程中斷操做。
        //latch.await();
        //限時等待,在超時以前,計數器變爲了0,方法返回true,否者直到超時,返回false,此方法會響應線程中斷操做。
        boolean result = latch.await(4, TimeUnit.SECONDS);
        long end = System.currentTimeMillis();
        //System.out.println("主線程結束,耗時" + (end - start));
        System.out.println("主線程結束,耗時" + (end - start)+"是否返回結果:"+result);
    }

    public static class T1 extends Thread {
        private int workTime;
        private CountDownLatch countDownLatch;

        public T1(String name, int workTime, CountDownLatch countDownLatch) {
            super(name);
            this.workTime = workTime;
            this.countDownLatch = countDownLatch;
        }

        @Override
        public void run() {
            long start = System.currentTimeMillis();
            Thread t = Thread.currentThread();
            System.out.println(t.getName() + "線程開始執行!");
            try {
                TimeUnit.SECONDS.sleep(workTime);
                long end = System.currentTimeMillis();
                System.out.println(t.getName() + "結束,耗時" + (end - start));
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                countDownLatch.countDown();
            }
        }
    }
}

輸出:

sheet1線程開始執行!
sheet2線程開始執行!
sheet1結束,耗時3000
主線程結束,耗時4004是否返回結果:false
sheet2結束,耗時5001
主線程中調用 countDownLatch.await();會讓主線程等待,t一、t2線程中模擬執行耗時操做,最終在finally中調用了 countDownLatch.countDown();,此方法每調用一次,CountDownLatch內部計數器會減1,當計數器變爲0的時候,主線程中的await()會返回,而後繼續執行。注意:上面的 countDown()這個是必需要執行的方法,因此放在finally中執行。

2個CountDown結合使用的示例——跑步比賽耗時統計

有3我的參見跑步比賽,須要先等指令員發指令槍後才能開跑,全部人都跑完以後,指令員喊一聲,你們跑完了,計算耗時。

示例:

public class CountDownLatchImplRacingTest {

    public static void main(String[] args) throws InterruptedException {
        CountDownLatch fireGun = new CountDownLatch(1);
        CountDownLatch latch = new CountDownLatch(3);
        new T1("劉翔", 3, fireGun, latch).start();
        new T1("拼嘻嘻", 5, fireGun, latch).start();
        new T1("蠟筆小新", 7, fireGun, latch).start();
        System.out.println("比賽 wait for ready!");
        ////主線程休眠3秒,模擬指令員準備發槍耗時操做
        TimeUnit.SECONDS.sleep(3);
        long start = System.currentTimeMillis();
        System.out.println("發令槍響,比賽開始!");
        fireGun.countDown();
        latch.await();
        long end = System.currentTimeMillis();
        System.out.println("比賽結束,總耗時" + (end - start));
    }

    public static class T1 extends Thread {
        private int workTime;
        private CountDownLatch fireGun;
        private CountDownLatch countDownLatch;


        public T1(String name, int workTime, CountDownLatch fireGun, CountDownLatch countDownLatch) {
            super(name);
            this.workTime = workTime;
            this.fireGun = fireGun;
            this.countDownLatch = countDownLatch;
        }

        @Override
        public void run() {
            try {
                fireGun.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            long start = System.currentTimeMillis();
            Thread t = Thread.currentThread();
            System.out.println(t.getName() + "運動員開始賽跑!");
            try {
                //模擬耗時操做,休眠workTime秒
                TimeUnit.SECONDS.sleep(workTime);
                long end = System.currentTimeMillis();
                System.out.println(t.getName() + "跑徹底程,耗時" + (end - start));
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                countDownLatch.countDown();
            }
        }
    }
}

輸出:

比賽 wait for ready!
發令槍響,比賽開始!
劉翔運動員開始賽跑!
蠟筆小新運動員開始賽跑!
拼嘻嘻運動員開始賽跑!
劉翔跑徹底程,耗時3000
拼嘻嘻跑徹底程,耗時5002
蠟筆小新跑徹底程,耗時7001
比賽結束,總耗時7001

手寫一個並行處理任務的工具類

示例:

public class TaskDisposeUtils {

    private static final Integer POOL_SIZE = Integer.max(Runtime.getRuntime().availableProcessors(), 5);

    public static void main(String[] args) {
        List<Integer> list = Stream.iterate(1, a -> a + 1).limit(10).collect(Collectors.toList());
        try {
            TaskDisposeUtils.dispose(list, item -> {
                long start = System.currentTimeMillis();
                //模擬耗時操做,休眠workTime秒
                try {
                    TimeUnit.SECONDS.sleep(item);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                long end = System.currentTimeMillis();
                System.out.println(item + "任務完成,耗時" + (end - start));
            });
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        //上面全部任務處理完畢完畢以後,程序才能繼續
        System.out.println(list + "任務所有執行完畢!");
    }

    public static <T> void dispose(List<T> taskList, Consumer<T> consumer) throws InterruptedException {
        dispose(true, POOL_SIZE, taskList, consumer);
    }

    private static <T> void dispose(boolean moreThread, Integer poolSize, List<T> taskList, Consumer<T> consumer) throws InterruptedException {
        if (CollectionUtils.isEmpty(taskList)) {
            return;
        }

        if (moreThread && poolSize > 1) {
            poolSize = Math.min(poolSize, taskList.size());
            ExecutorService executorService = null;
            try {
                executorService = Executors.newFixedThreadPool(poolSize);
                CountDownLatch latch = new CountDownLatch(taskList.size());
                for (T t : taskList) {
                    executorService.execute(() -> {
                        try {
                            consumer.accept(t);
                        } finally {
                            latch.countDown();
                        }
                    });
                }
                latch.await();
            } finally {
                if (executorService != null) {
                    executorService.shutdown();
                }
            }

        } else {
            for (T t : taskList) {
                consumer.accept(t);
            }
        }
    }
}

輸出:

1任務完成,耗時1376
2任務完成,耗時2014
3任務完成,耗時3179
4任務完成,耗時4449
5任務完成,耗時5000
6任務完成,耗時6001
7任務完成,耗時7000
8任務完成,耗時8000
9任務完成,耗時9000
10任務完成,耗時10001
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]任務所有執行完畢!

在實時系統中的使用場景

  1. 實現最大的並行性:有時咱們想同時啓動多個線程,實現最大程度的並行性。例如,咱們想測試一個單例類。若是咱們建立一個初始計數爲1的CountDownLatch,並讓全部線程都在這個鎖上等待,那麼咱們能夠很輕鬆地完成測試。咱們只需調用 一次countDown()方法就可讓全部的等待線程同時恢復執行。
  2. 開始執行前等待n個線程完成各自任務:例如應用程序啓動類要確保在處理用戶請求前,全部N個外部系統已經啓動和運行了。
  3. 死鎖檢測:一個很是方便的使用場景是,你能夠使用n個線程訪問共享資源,在每次測試階段的線程數目是不一樣的,並嘗試產生死鎖

疑問:

Q:這個notifyAll()方法是由jvm內部自動調用的,jdk源碼中是看不到的,須要看jvm源碼,有興趣的同窗能夠去查一下。因此JDK不推薦在線程上調用wait、notify、notifyAll方法。 是由於沒有源碼因此纔不推薦使用wait、notify、notifyAll方法麼?還有其餘缺點麼?

JUC中的循環柵欄CyclicBarrier的6種使用場景

CyclicBarrier簡介

CyclicBarrier一般稱爲循環屏障。它和CountDownLatch很類似,均可以使線程先等待而後再執行。不過CountDownLatch是使一批(一個)線程等待另外一批(一個)線程執行完後再執行;而CyclicBarrier只是使等待的線程達到必定數目後再讓它們繼續執行。故而CyclicBarrier內部也有一個計數器,計數器的初始值在建立對象時經過構造參數指定,以下所示:

public CyclicBarrier(int parties) {
    this(parties, null);
}

每調用一次await()方法都將使阻塞的線程數+1,只有阻塞的線程數達到設定值時屏障纔會打開,容許阻塞的全部線程繼續執行。除此以外,CyclicBarrier還有幾點須要注意的地方:

  • CyclicBarrier的計數器能夠重置而CountDownLatch不行,這意味着CyclicBarrier實例能夠被重複使用而CountDownLatch只能被使用一次。而這也是循環屏障循環二字的語義所在。

  • CyclicBarrier容許用戶自定義barrierAction操做,這是個可選操做,能夠在建立CyclicBarrier對象時指定

    public CyclicBarrier(int parties, Runnable barrierAction) {
    if (parties <= 0) throw new IllegalArgumentException();
    this.parties = parties;
    this.count = parties;
    this.barrierCommand = barrierAction;
    }

一旦用戶在建立CyclicBarrier對象時設置了barrierAction參數,則在阻塞線程數達到設定值屏障打開前,會調用barrierAction的run()方法完成用戶自定義的操做。

CyclicBarrier內部至關於有個計數器(構造方法傳入的),每次調用await();後,計數器會減1,而且await()方法會讓當前線程阻塞,等待計數器減爲0的時候,全部在await()上等待的線程被喚醒,而後繼續向下執行,此時計數器又會被還原爲建立時的值,而後能夠繼續再次使用。

CountDownLatch和CyclicBarrier的區別

CountDownLatch示例

主管至關於 CountDownLatch,幹活的小弟至關於作事情的線程。

老闆交給主管了一個任務,讓主管搞完以後當即上報給老闆。主管下面有10個小弟,接到任務以後將任務劃分爲10個小任務分給每一個小弟去幹,主管一直處於等待狀態(主管會調用await()方法,此方法會阻塞當前線程),讓每一個小弟幹完以後通知一下主管(調用countDown()方法通知主管,此方法會當即返回),主管等到全部的小弟都作完了,會被喚醒,從await()方法上甦醒,而後將結果反饋給老闆。期間主管會等待,會等待全部小弟將結果彙報給本身。

而CyclicBarrier是一批線程讓本身等待,等待全部的線程都準備好了,全部的線程才能繼續。

CountDownLatch: 一個線程(或者多個), 等待另外N個線程完成某個事情以後才能執行。

CyclicBrrier: N個線程相互等待,任何一個線程完成以前,全部的線程都必須等待。

重複使用CyclicBarrier、自定義一個全部線程到齊後的處理動做實例:

public class CyclicBarrierTest {

    /**
     * 能夠自定義一個全部線程到齊後的處理動做,再喚醒全部線程工做
     */
    private static CyclicBarrier cyclicBarrier = new CyclicBarrier(6,()->{
        System.out.println("人都到齊了,你們high起來!");
    });

    public static void main(String[] args) {
        for (int i = 0; i < 6; i++) {
            new T1("驢友"+i, i).start();
        }
    }

    public static class T1 extends Thread {
        private int workTime;

        public T1(String name, int workTime) {
            super(name);
            this.workTime = workTime;
        }

        @Override
        public void run() {
            //等待人齊吃飯
            eat();
            //等待人齊上車下一站旅遊
            travel();
        }
        private void eat(){
            Thread t = Thread.currentThread();
            //System.out.println(t.getName() + "號旅客開始準備吃飯!");
            try {
                TimeUnit.SECONDS.sleep(workTime);
                long start = System.currentTimeMillis();
                cyclicBarrier.await();
                long end = System.currentTimeMillis();
                System.out.println(t.getName() + "號旅客吃飯了,sleep:"+workTime+",等待耗時" + (end - start));
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e1) {
                e1.printStackTrace();
            }
        }

        private void travel(){
            Thread t = Thread.currentThread();
            //System.out.println(t.getName() + "號旅客開始準備吃飯!");
            try {
                TimeUnit.SECONDS.sleep(workTime);
                long start = System.currentTimeMillis();
                cyclicBarrier.await();
                long end = System.currentTimeMillis();
                System.out.println(t.getName() + "號旅客上車了,sleep:"+workTime+",等待耗時" + (end - start));
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e1) {
                e1.printStackTrace();
            }
        }
    }
}

輸出:

人都到齊了,你們high起來!
驢友5號旅客吃飯了,sleep:5,等待耗時0
驢友0號旅客吃飯了,sleep:0,等待耗時5002
驢友1號旅客吃飯了,sleep:1,等待耗時4000
驢友4號旅客吃飯了,sleep:4,等待耗時1000
驢友3號旅客吃飯了,sleep:3,等待耗時2000
驢友2號旅客吃飯了,sleep:2,等待耗時3000
人都到齊了,你們high起來!
驢友5號旅客上車了,sleep:5,等待耗時0
驢友4號旅客上車了,sleep:4,等待耗時999
驢友3號旅客上車了,sleep:3,等待耗時2000
驢友2號旅客上車了,sleep:2,等待耗時3000
驢友1號旅客上車了,sleep:1,等待耗時3999
驢友0號旅客上車了,sleep:0,等待耗時5000

其中一個線程被interrupt()打斷實例:

public class CyclicBarrierBreakTest {

    private static CyclicBarrier cyclicBarrier = new CyclicBarrier(6);

    public static class T1 extends Thread {
        private int workTime;

        public T1(String name, int workTime) {
            super(name);
            this.workTime = workTime;
        }

        @Override
        public void run() {
            //等待人齊吃飯
            long start = 0, end = 0;
            Thread t = Thread.currentThread();
            try {
                TimeUnit.SECONDS.sleep(workTime);
                start = System.currentTimeMillis();
                System.out.println(t.getName() + "號旅客開始準備吃飯!");
                cyclicBarrier.await();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
            end = System.currentTimeMillis();
            System.out.println(t.getName() + "號旅客吃飯了,sleep:" + workTime + ",等待耗時" + (end - start));
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 1; i <= 6; i++) {
            int sleep = 0;
            //若是線程只是在睡眠過程時,中斷的就不是cyclicBarrier.await();觸發的,而是 TimeUnit.SECONDS.sleep(workTime);這時候就達不到效果
            T1 t = new T1("驢友" + i, sleep);
            t.start();
            if (i == 3) {
                TimeUnit.SECONDS.sleep(1);
                System.out.println(t.getName() + ",有點急事,我先吃了!");
                t.interrupt();
                TimeUnit.SECONDS.sleep(2);
            }
        }
    }
}

輸出:

驢友2號旅客開始準備吃飯!
驢友1號旅客開始準備吃飯!
驢友3號旅客開始準備吃飯!
驢友3,有點急事,我先吃了!
驢友3號旅客吃飯了,sleep:0,等待耗時1003
驢友2號旅客吃飯了,sleep:0,等待耗時1004
驢友1號旅客吃飯了,sleep:0,等待耗時1004
java.lang.InterruptedException
	at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.reportInterruptAfterWait(A
java.util.concurrent.BrokenBarrierException
驢友4號旅客開始準備吃飯!
	at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:207)
驢友5號旅客開始準備吃飯!
	at java.util.concurrent.CyclicBarrier.await(CyclicBarrier.java:362)
驢友6號旅客開始準備吃飯!
	at com.self.current.CyclicBarrierBreakTest$T1.run(CyclicBarrierBreakTest.java:38)
驢友4號旅客吃飯了,sleep:0,等待耗時0
java.util.concurrent.BrokenBarrierException
驢友6號旅客吃飯了,sleep:0,等待耗時0
	at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:207)
驢友5號旅客吃飯了,sleep:0,等待耗時1
java.util.concurrent.BrokenBarrierException

結論:

  1. 內部有一我的把規則破壞了(接收到中斷信號),其餘人都不按規則來了,不會等待了
  2. 接收到中斷信號的線程,await方法會觸發InterruptedException異常,而後被喚醒向下運行
  3. 其餘等待中 或者後面到達的線程,會在await()方法上觸發BrokenBarrierException異常,而後繼續執行

其中一個線程執行cyclicBarrier.await(2, TimeUnit.SECONDS);只執行超時等待2秒:

結論:

  1. 等待超時的方法
    public int await(long timeout, TimeUnit unit) throws InterruptedException,BrokenBarrierException,TimeoutException

  2. 內部有一我的把規則破壞了(等待超時),其餘人都不按規則來了,不會等待了

  3. 等待超時的線程,await方法會觸發TimeoutException異常,而後被喚醒向下運行

  4. 其餘等待中或者後面到達的線程,會在await()方法上觸發BrokenBarrierException異常,而後繼續執行

重建規則示例:

第一次規則被打亂了,過了一會導遊重建了規則(cyclicBarrier.reset();),接着又重來來了一次模擬等待吃飯的操做,正常了。

public class CyclicBarrierResetTest {

    private static CyclicBarrier cyclicBarrier = new CyclicBarrier(6);

    private static boolean onOrder = false;

    public static class T1 extends Thread {
        private int workTime;

        public T1(String name, int workTime) {
            super(name);
            this.workTime = workTime;
        }

        @Override
        public void run() {
            //等待人齊吃飯
            long start = 0, end = 0;
            Thread t = Thread.currentThread();
            try {
                TimeUnit.SECONDS.sleep(workTime);
                start = System.currentTimeMillis();
                System.out.println(t.getName() + "號旅客開始準備吃飯!");
                if (!onOrder) {
                    if (this.getName().equals("驢友1")) {
                        cyclicBarrier.await(2, TimeUnit.SECONDS);
                    } else {
                        cyclicBarrier.await();
                    }
                } else {
                    cyclicBarrier.await();

                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            } catch (TimeoutException e){
                e.printStackTrace();
            }
            end = System.currentTimeMillis();
            System.out.println(t.getName() + "號旅客吃飯了,sleep:" + workTime + ",等待耗時" + (end - start));
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 1; i <= 6; i++) {
            T1 t = new T1("驢友" + i, i);
            t.start();
        }
        //等待7秒以後,重置,重建規則
        TimeUnit.SECONDS.sleep(7);
        cyclicBarrier.reset();
        onOrder = true;
        System.out.println("---------------從新按按規則來,不遵照規則的沒飯吃!------------------");
        //再來一次
        for (int i = 1; i <= 6; i++) {
            T1 t = new T1("驢友" + i, i);
            t.start();
        }
    }
}

輸出:

驢友1號旅客開始準備吃飯!
驢友2號旅客開始準備吃飯!
驢友3號旅客開始準備吃飯!
java.util.concurrent.BrokenBarrierException
驢友3號旅客吃飯了,sleep:3,等待耗時3
java.util.concurrent.TimeoutException
驢友1號旅客吃飯了,sleep:1,等待耗時2005
java.util.concurrent.BrokenBarrierException
驢友2號旅客吃飯了,sleep:2,等待耗時1006
java.util.concurrent.BrokenBarrierException
驢友4號旅客開始準備吃飯!
	at java.util.concurrent.CyclicBarrier.dowait(CyclicBarrier.java:207)
驢友4號旅客吃飯了,sleep:4,等待耗時0
驢友5號旅客開始準備吃飯!
驢友5號旅客吃飯了,sleep:5,等待耗時0
java.util.concurrent.BrokenBarrierException
java.util.concurrent.BrokenBarrierException
驢友6號旅客開始準備吃飯!
驢友6號旅客吃飯了,sleep:6,等待耗時0

---------------從新按按規則來,不遵照規則的沒飯吃!------------------
驢友1號旅客開始準備吃飯!
驢友2號旅客開始準備吃飯!
驢友3號旅客開始準備吃飯!
驢友4號旅客開始準備吃飯!
驢友5號旅客開始準備吃飯!
驢友6號旅客開始準備吃飯!
驢友6號旅客吃飯了,sleep:6,等待耗時0
驢友5號旅客吃飯了,sleep:5,等待耗時1000
驢友4號旅客吃飯了,sleep:4,等待耗時2000
驢友3號旅客吃飯了,sleep:3,等待耗時3000
驢友2號旅客吃飯了,sleep:2,等待耗時3999
驢友1號旅客吃飯了,sleep:1,等待耗時5000

JAVA線程池

線程池實現原理

相似於一個工廠的運做。

當向線程池提交一個任務以後,線程池的處理流程以下:

  1. 判斷是否達到核心線程數,若未達到,則直接建立新的線程處理當前傳入的任務,不然進入下個流程
  2. 線程池中的工做隊列是否已滿,若未滿,則將任務丟入工做隊列中先存着等待處理,不然進入下個流程
  3. 是否達到最大線程數,若未達到,則建立新的線程處理當前傳入的任務,不然交給線程池中的飽和策略進行處理。

java中的線程池

jdk中提供了線程池的具體實現,實現類是:java.util.concurrent.ThreadPoolExecutor,主要構造方法:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

corePoolSize:核心線程大小,當提交一個任務到線程池時,線程池會建立一個線程來執行任務,即便有其餘空閒線程能夠處理任務也會創新線程,等到工做的線程數大於核心線程數時就不會在建立了。若是調用了線程池的prestartAllCoreThreads方法,線程池會提早把核心線程都創造好,並啓動。(prestartCoreThread:啓動一個核心線程或 prestartAllCoreThreads:啓動所有核心線程 )

maximumPoolSize:線程池容許建立的最大線程數。若是隊列滿了,而且以建立的線程數小於最大線程數,則線程池會再建立新的線程執行任務。若是咱們使用了無界隊列(或者大小是Integer.MAX_VALUE,可能還沒達到就OOM了),那麼全部的任務會加入隊列,這個參數就沒有什麼效果了。

keepAliveTime:線程池的工做線程空閒後,保持存活的時間。若是沒有任務處理了,有些線程會空閒,空閒的時間超過了這個值,會被回收掉。若是任務不少,而且每一個任務的執行時間比較短,避免線程重複建立和回收,能夠調大這個時間,提升線程的利用率

unit:keepAliveTIme的時間單位,能夠選擇的單位有天、小時、分鐘、毫秒、微妙、千分之一毫秒和納秒。類型是一個枚舉java.util.concurrent.TimeUnit,這個枚舉也常用,有興趣的能夠看一下其源碼

workQueue:工做隊列,用於緩存待處理任務的阻塞隊列,常見的有4種(ArrayBlockingQueue 、LinkedBlockingQueue 、SynchronousQueue  、PriorityBlockingQueue )

threadFactory:線程池中建立線程的工廠,能夠經過線程工廠給每一個建立出來的線程設置更有意義的名字

handler:飽和策略,當線程池沒法處理新來的任務了,那麼須要提供一種策略處理提交的新任務,默認有4種策略(AbortPolicy 、CallerRunsPolicy 、DiscardOldestPolicy、DiscardPolicy )

調用線程池的execute方法處理任務,執行execute方法的過程:

  1. 判斷線程池中運行的線程數是否小於corepoolsize,是:則建立新的線程來處理任務,否:執行下一步
  2. 試圖將任務添加到workQueue指定的隊列中,若是沒法添加到隊列,進入下一步
  3. 判斷線程池中運行的線程數是否小於maximumPoolSize,是:則新增線程處理當前傳入的任務,否:將任務傳遞給handler對象rejectedExecution方法處理

線程池的使用步驟:

  1. 調用構造方法建立線程池
  2. 調用線程池的方法處理任務
  3. 關閉線程池

線程池中常見5種工做隊列

任務太多的時候,工做隊列用於暫時緩存待處理的任務,jdk中常見的4種阻塞隊列:

ArrayBlockingQueue:是一個基於數組結構的有界阻塞隊列,此隊列按照先進先出原則對元素進行排序

LinkedBlockingQueue:是一個基於鏈表結構的阻塞隊列,此隊列按照先進先出排序元素,吞吐量一般要高於ArrayBlockingQueue。靜態工廠方法Executors.newFixedThreadPool使用了這個隊列。

SynchronousQueue :一個不存儲元素的阻塞隊列,每一個插入操做必須等到另一個線程調用移除操做,不然插入操做一直處理阻塞狀態,吞吐量一般要高於LinkedBlockingQueue,靜態工廠方法Executors.newCachedThreadPool使用這個隊列。

PriorityBlockingQueue:優先級隊列,進入隊列的元素按照優先級會進行排序。

SynchronousQueue隊列的線程池

使用Executors.newCachedThreadPool()建立線程池,看一下的源碼:

public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

newCachedThreadPool()使用了SynchronousQueue同步隊列,這種隊列比較特殊,放入元素必需要有另一個線程去獲取這個元素,不然放入元素會失敗或者一直阻塞在那裏直到有線程取走,示例中任務處理休眠了指定的時間,致使已建立的工做線程都忙於處理任務,因此新來任務以後,將任務丟入同步隊列會失敗,丟入隊列失敗以後,會嘗試新建線程處理任務。使用上面的方式建立線程池須要注意,若是須要處理的任務比較耗時,會致使新來的任務都會建立新的線程進行處理,可能會致使建立很是多的線程,最終耗盡系統資源,觸發OOM。

//SynchronousQueue隊列默認是false,採用先進後出的棧處理,也能夠是公平隊列先進先出。
public SynchronousQueue(boolean fair) {
    transferer = fair ? new TransferQueue<E>() : new TransferStack<E>();
}

PriorityBlockingQueue優先級隊列的線程池

輸出中,除了第一個任務,其餘任務按照優先級高低按順序處理。緣由在於:建立線程池的時候使用了優先級隊列,進入隊列中的任務會進行排序,任務的前後順序由Task中的i變量決定。向PriorityBlockingQueue加入元素的時候,內部會調用代碼中Task的compareTo方法決定元素的前後順序。

示例:

public class ThreadPoolExecutorPriorityTest {
    /**
     * 優先級隊列執行的任務要實現Comparable比較
     */
    static class Task implements Runnable, Comparable<Task> {
        private int i;
        private String name;
        public Task(int i, String name) {
            this.i = i;
            this.name = name;
        }
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "處理" + this.name);
        }
        @Override
        public int compareTo(Task o) {
            return Integer.compare(o.i, this.i);
        }
    }

    //自定義線程工廠,優先級隊列
    private static ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 1, 60,
            TimeUnit.SECONDS, new PriorityBlockingQueue<>(), new DemoThreadFactory("訂單建立組"), new ThreadPoolExecutor.AbortPolicy());

    public static void main(String[] args) {
        for (int i = 1; i <= 10; i++) {
            int j = i;
            String taskName = "task" + j;
            executor.execute(new Task(j,taskName));
        }

        for (int i = 90; i <= 100; i++) {
            int j = i;
            String taskName = "task" + j;
            executor.execute(new Task(j,taskName));
        }
        executor.shutdown();
    }
}

輸出:

From DemoThreadFactory's 訂單建立組-Worker-1處理task1
From DemoThreadFactory's 訂單建立組-Worker-1處理task100
From DemoThreadFactory's 訂單建立組-Worker-1處理task99
From DemoThreadFactory's 訂單建立組-Worker-1處理task98
From DemoThreadFactory's 訂單建立組-Worker-1處理task97
From DemoThreadFactory's 訂單建立組-Worker-1處理task96
From DemoThreadFactory's 訂單建立組-Worker-1處理task95
From DemoThreadFactory's 訂單建立組-Worker-1處理task94
From DemoThreadFactory's 訂單建立組-Worker-1處理task93
From DemoThreadFactory's 訂單建立組-Worker-1處理task92
From DemoThreadFactory's 訂單建立組-Worker-1處理task91
From DemoThreadFactory's 訂單建立組-Worker-1處理task90
From DemoThreadFactory's 訂單建立組-Worker-1處理task10
From DemoThreadFactory's 訂單建立組-Worker-1處理task9
From DemoThreadFactory's 訂單建立組-Worker-1處理task8
From DemoThreadFactory's 訂單建立組-Worker-1處理task7
From DemoThreadFactory's 訂單建立組-Worker-1處理task6
From DemoThreadFactory's 訂單建立組-Worker-1處理task5
From DemoThreadFactory's 訂單建立組-Worker-1處理task4
From DemoThreadFactory's 訂單建立組-Worker-1處理task3
From DemoThreadFactory's 訂單建立組-Worker-1處理task2

自定義建立線程的工廠

給線程池中線程起一個有意義的名字,在系統出現問題的時候,經過線程堆棧信息能夠更容易發現系統中問題所在。經過jstack查看線程的堆棧信息,也能夠看到咱們自定義的名稱 。

自定義建立工廠須要實現java.util.concurrent.ThreadFactory接口中的Thread newThread(Runnable r)方法,參數爲傳入的任務,須要返回一個工做線程。

示例:

public class ThreadPoolExecutorTest {

    //默認線程建立
    /*  private static ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 6, 60,
              TimeUnit.SECONDS, new LinkedBlockingQueue<>(15), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());*/

    //自定義線程工廠1
/*    private static final AtomicInteger nextId = new AtomicInteger(1);
    private static ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 60,
            TimeUnit.SECONDS, new LinkedBlockingQueue<>(12), (r -> {
        Thread t = new Thread(r);
        t.setName("示範線程" + nextId.getAndIncrement());
        return t;
    }), new ThreadPoolExecutor.AbortPolicy());*/

    //自定義線程工廠2 ,推薦
    private static ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 60,
            TimeUnit.SECONDS, new LinkedBlockingQueue<>(15), new DemoThreadFactory("訂單建立組"), new ThreadPoolExecutor.AbortPolicy());

    public static void main(String[] args) {
        //提早啓動全部核心線程
        executor.prestartAllCoreThreads();
        //提早啓動一個核心線程
        executor.prestartCoreThread();
        for (int i = 1; i <= 20; i++) {
            int j = i;
            String taskName = "task" + j;
            executor.execute(() -> {
                try {
                    TimeUnit.SECONDS.sleep(j);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "線程執行" + taskName + "完畢!");
            });
        }
        executor.shutdown();
    }
}

輸出:

From DemoThreadFactory's 訂單建立組-Worker-1線程執行task1完畢!
From DemoThreadFactory's 訂單建立組-Worker-3線程執行task2完畢!
From DemoThreadFactory's 訂單建立組-Worker-2線程執行task3完畢!
From DemoThreadFactory's 訂單建立組-Worker-4線程執行task4完畢!
From DemoThreadFactory's 訂單建立組-Worker-5線程執行task5完畢!
From DemoThreadFactory's 訂單建立組-Worker-1線程執行task6完畢!
From DemoThreadFactory's 訂單建立組-Worker-3線程執行task7完畢!
From DemoThreadFactory's 訂單建立組-Worker-2線程執行task8完畢!
From DemoThreadFactory's 訂單建立組-Worker-4線程執行task9完畢!
From DemoThreadFactory's 訂單建立組-Worker-5線程執行task10完畢!
From DemoThreadFactory's 訂單建立組-Worker-6線程執行task17完畢!
From DemoThreadFactory's 訂單建立組-Worker-1線程執行task11完畢!
From DemoThreadFactory's 訂單建立組-Worker-7線程執行task20完畢!
From DemoThreadFactory's 訂單建立組-Worker-3線程執行task12完畢!
From DemoThreadFactory's 訂單建立組-Worker-2線程執行task13完畢!
From DemoThreadFactory's 訂單建立組-Worker-4線程執行task14完畢!
From DemoThreadFactory's 訂單建立組-Worker-5線程執行task15完畢!

四種常見飽和策略

當線程池中隊列已滿,而且線程池已達到最大線程數,線程池會將任務傳遞給飽和策略進行處理。這些策略都實現了RejectedExecutionHandler接口。接口中有個方法:

void rejectedExecution(Runnable r, ThreadPoolExecutor executor)

參數說明:

r:須要執行的任務

executor:當前線程池對象

JDK中提供了4種常見的飽和策略:

AbortPolicy:直接拋出異常。

CallerRunsPolicy:在當前調用者的線程中運行任務,即隨丟來的任務,由他本身去處理。

DiscardOldestPolicy:丟棄隊列中最老的一個任務,即丟棄隊列頭部的一個任務,而後執行當前傳入的任務。

DiscardPolicy:不處理,直接丟棄掉,方法內部爲空。

解釋:

//自定義線程工廠
private static ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 5, 60,
        TimeUnit.SECONDS, new LinkedBlockingQueue<>(5),
        new DemoThreadFactory("訂單建立組"), new ThreadPoolExecutor.CallerRunsPolicy());
        
AbortPolicy:直接拋出異常。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    throw new RejectedExecutionException("Task " + r.toString() +
    " rejected from " +
    e.toString());
}
輸出:到飽和策略時拋出異常記錄,丟棄掉任務11個。
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task com.self.current.ThreadPoolExecutorTest$$Lambda$1/1915503092@50134894 rejected from java.util.concurrent.ThreadPoolExecutor@2957fcb0[Running, pool size = 5, active threads = 4, queued tasks = 5, completed tasks = 0]
	at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
	at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
	at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
	at com.self.current.ThreadPoolExecutorTest.main(ThreadPoolExecutorTest.java:47)
From DemoThreadFactory's 訂單建立組-Worker-1線程執行task1完畢!
From DemoThreadFactory's 訂單建立組-Worker-1線程執行task2完畢!
From DemoThreadFactory's 訂單建立組-Worker-2線程執行task6完畢!
From DemoThreadFactory's 訂單建立組-Worker-1線程執行task3完畢!
From DemoThreadFactory's 訂單建立組-Worker-3線程執行task7完畢!
From DemoThreadFactory's 訂單建立組-Worker-4線程執行task8完畢!
From DemoThreadFactory's 訂單建立組-Worker-5線程執行task9完畢!
From DemoThreadFactory's 訂單建立組-Worker-2線程執行task4完畢!
From DemoThreadFactory's 訂單建立組-Worker-1線程執行task5完畢!

        
CallerRunsPolicy:在當前調用者的線程中運行任務,即隨丟來的任務,由他本身去處理。如main方法調用的線程池,則若是走到飽和策略處理時,由main方法處理這個任務。不會丟棄任何一個任務,但執行會變得很慢。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                r.run();
            }
        }    
輸出:
From DemoThreadFactory's 訂單建立組-Worker-1線程執行task1完畢!
From DemoThreadFactory's 訂單建立組-Worker-1線程執行task2完畢!
From DemoThreadFactory's 訂單建立組-Worker-2線程執行task6完畢!
From DemoThreadFactory's 訂單建立組-Worker-1線程執行task3完畢!
From DemoThreadFactory's 訂單建立組-Worker-3線程執行task8完畢!
From DemoThreadFactory's 訂單建立組-Worker-4線程執行task9完畢!
From DemoThreadFactory's 訂單建立組-Worker-2線程執行task4完畢!
From DemoThreadFactory's 訂單建立組-Worker-5線程執行task10完畢!
main線程執行task11完畢!
From DemoThreadFactory's 訂單建立組-Worker-1線程執行task5完畢!
From DemoThreadFactory's 訂單建立組-Worker-3線程執行task7完畢!
From DemoThreadFactory's 訂單建立組-Worker-4線程執行task12完畢!
From DemoThreadFactory's 訂單建立組-Worker-2線程執行task13完畢!
From DemoThreadFactory's 訂單建立組-Worker-5線程執行task14完畢!
From DemoThreadFactory's 訂單建立組-Worker-1線程執行task15完畢!
main線程執行task17完畢!
From DemoThreadFactory's 訂單建立組-Worker-3線程執行task16完畢!

DiscardOldestPolicy:丟棄隊列中最老的一個任務,即丟棄隊列頭部的一個任務,而後執行當前傳入的任務。這時候線程池會在執行到飽和策略時丟棄掉頭部最老的認爲,沒有任何記錄,任務就丟掉了。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    if (!e.isShutdown()) {
    e.getQueue().poll();
    e.execute(r);
    }
}

輸出:20個任務被無聲無息地丟掉了11個
From DemoThreadFactory's 訂單建立組-Worker-2線程執行task6完畢!
From DemoThreadFactory's 訂單建立組-Worker-3線程執行task7完畢!
From DemoThreadFactory's 訂單建立組-Worker-4線程執行task8完畢!
From DemoThreadFactory's 訂單建立組-Worker-5線程執行task9完畢!
From DemoThreadFactory's 訂單建立組-Worker-1線程執行task16完畢!
From DemoThreadFactory's 訂單建立組-Worker-2線程執行task17完畢!
From DemoThreadFactory's 訂單建立組-Worker-3線程執行task18完畢!
From DemoThreadFactory's 訂單建立組-Worker-4線程執行task19完畢!
From DemoThreadFactory's 訂單建立組-Worker-5線程執行task20完畢!

DiscardPolicy:不處理,直接丟棄掉,方法內部爲空。沒處理Runnable r就表示丟棄了。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
輸出:20個任務被無聲無息地丟掉了10個
From DemoThreadFactory's 訂單建立組-Worker-1線程執行task1完畢!
From DemoThreadFactory's 訂單建立組-Worker-1線程執行task2完畢!
From DemoThreadFactory's 訂單建立組-Worker-1線程執行task3完畢!
From DemoThreadFactory's 訂單建立組-Worker-2線程執行task7完畢!
From DemoThreadFactory's 訂單建立組-Worker-3線程執行task8完畢!
From DemoThreadFactory's 訂單建立組-Worker-4線程執行task9完畢!
From DemoThreadFactory's 訂單建立組-Worker-5線程執行task10完畢!
From DemoThreadFactory's 訂單建立組-Worker-1線程執行task4完畢!
From DemoThreadFactory's 訂單建立組-Worker-2線程執行task5完畢!
From DemoThreadFactory's 訂單建立組-Worker-3線程執行task6完畢!

自定義飽和策略

須要實現RejectedExecutionHandler接口。任務沒法處理的時候,咱們想記錄一下日誌,咱們須要自定義一個飽和策略。記錄了任務的日誌,對於沒法處理多任務,咱們最好可以記錄一下,讓開發人員可以知道。 任務進入了飽和策略,說明線程池的配置可能不是太合理,或者機器的性能有限,須要作一些優化調整。

實例:

public class ThreadPoolExecutorRejectHandlerTest {
    static class Task implements Runnable {
        String name;

        public Task(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "處理" + this.name);
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        @Override
        public String toString() {
            return "Task{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }
    //自定義包含策略:能夠直接用函數式方法定義,也能夠實現RejectedExecutionHandler自定義
    private static ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 5, 60,
            TimeUnit.SECONDS, new LinkedBlockingQueue<>(5),
            new DemoThreadFactory("訂單建立組"), (r,executor)->{
        //自定義飽和策略
        //記錄一下沒法處理的任務
        System.out.println("沒法處理的任務:" + r.toString());
    });

    public static void main(String[] args) {
        //提早啓動全部核心線程
        executor.prestartAllCoreThreads();
        //提早啓動一個核心線程
        executor.prestartCoreThread();
        for (int i = 1; i <= 20; i++) {
            int j = i;
            String taskName = "task" + j;
            executor.execute(new Task(taskName));
        }
        executor.shutdown();
    }
}

輸出:

沒法處理的任務:Task{name='task10'}
沒法處理的任務:Task{name='task11'}
沒法處理的任務:Task{name='task12'}
沒法處理的任務:Task{name='task13'}
沒法處理的任務:Task{name='task14'}
沒法處理的任務:Task{name='task15'}
沒法處理的任務:Task{name='task16'}
沒法處理的任務:Task{name='task17'}
沒法處理的任務:Task{name='task18'}
沒法處理的任務:Task{name='task19'}
沒法處理的任務:Task{name='task20'}
From DemoThreadFactory's 訂單建立組-Worker-1處理task1
From DemoThreadFactory's 訂單建立組-Worker-2處理task6
From DemoThreadFactory's 訂單建立組-Worker-3處理task7
From DemoThreadFactory's 訂單建立組-Worker-4處理task8
From DemoThreadFactory's 訂單建立組-Worker-5處理task9
From DemoThreadFactory's 訂單建立組-Worker-2處理task2
From DemoThreadFactory's 訂單建立組-Worker-1處理task3
From DemoThreadFactory's 訂單建立組-Worker-4處理task5
From DemoThreadFactory's 訂單建立組-Worker-3處理task4

線程池中的2個關閉方法

線程池提供了2個關閉方法:shutdown和shutdownNow,當調用者兩個方法以後,線程池會遍歷內部的工做線程,而後調用每一個工做線程的interrrupt方法給線程發送中斷信號,內部若是沒法響應中斷信號的可能永遠沒法終止,因此若是內部有無線循環的,最好在循環內部檢測一下線程的中斷信號,合理的退出。調用者兩個方法中任意一個,線程池的isShutdown方法(是否執行了關閉線程池命令)就會返回true,當全部的任務線程都關閉以後,才表示線程池關閉成功,這時調用isTerminaed方法(是否關閉成功)會返回true。

調用shutdown方法以後,線程池將再也不接受新任務,內部會將全部已提交的任務處理完畢,處理完畢以後,工做線程自動退出。

而調用shutdownNow方法後,線程池會將還未處理的(在隊裏等待處理的任務)任務移除,將正在處理中的處理完畢以後,工做線程自動退出。

至於調用哪一個方法來關閉線程,應該由提交到線程池的任務特性決定,多數狀況下調用shutdown方法來關閉線程池,若是任務不必定要執行完,則能夠調用shutdownNow方法。

擴展線程池

ThreadPoolExecutor內部提供了幾個方法beforeExecute、afterExecute、terminated,能夠由開發人員本身去重寫實現這些方法。

看一下線程池內部的源碼:

try {
    beforeExecute(wt, task);//任務執行以前調用的方法
    Throwable thrown = null;
    try {
        task.run();
    } catch (RuntimeException x) {
        thrown = x;
        throw x;
    } catch (Error x) {
        thrown = x;
        throw x;
    } catch (Throwable x) {
        thrown = x;
        throw new Error(x);
    } finally {
        afterExecute(task, thrown);//任務執行完畢以後調用的方法
    }
} finally {
    task = null;
    w.completedTasks++;
    w.unlock();
}

beforeExecute:任務執行以前調用的方法,有2個參數,第1個參數是執行任務的線程,第2個參數是任務

protected void beforeExecute(Thread t, Runnable r) { }

afterExecute:任務執行完成以後調用的方法,2個參數,第1個參數表示任務,第2個參數表示任務執行時的異常信息,若是無異常,第二個參數爲null

protected void afterExecute(Runnable r, Throwable t) { }

terminated:線程池最終關閉以後調用的方法。全部的工做線程都退出了,最終線程池會退出,退出時調用該方法

實例:

public class ThreadPoolExecutorExtensionTest {
    static class Task implements Runnable {
        String name;

        public Task(String name) {
            this.name = name;
        }

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "處理" + this.name);
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        @Override
        public String toString() {
            return "Task{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }
    //擴展線程池,能夠繼承也能夠直接重寫
    private static ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 60,
            TimeUnit.SECONDS, new LinkedBlockingQueue<>(15),
            new DemoThreadFactory("訂單建立組"), new ThreadPoolExecutor.AbortPolicy()){
        @Override
        protected void beforeExecute(Thread t, Runnable r) {
            System.out.println(t.getName() + ",開始執行任務:" + r.toString());
        }

        @Override
        protected void afterExecute(Runnable r, Throwable t) {
            System.out.println(Thread.currentThread().getName() + ",任務:" + r.toString() + ",執行完畢!");
        }

        @Override
        protected void terminated() {
            System.out.println(Thread.currentThread().getName() + ",關閉線程池!");
        }
    };

    public static void main(String[] args) {
        for (int i = 1; i <= 3; i++) {
            int j = i;
            String taskName = "task" + j;
            executor.execute(new Task(taskName));
        }
        executor.shutdown();
    }
}

輸出:

From DemoThreadFactory's 訂單建立組-Worker-1,開始執行任務:Task{name='task1'}
From DemoThreadFactory's 訂單建立組-Worker-1處理task1
From DemoThreadFactory's 訂單建立組-Worker-2,開始執行任務:Task{name='task2'}
From DemoThreadFactory's 訂單建立組-Worker-2處理task2
From DemoThreadFactory's 訂單建立組-Worker-3,開始執行任務:Task{name='task3'}
From DemoThreadFactory's 訂單建立組-Worker-3處理task3
From DemoThreadFactory's 訂單建立組-Worker-1,任務:Task{name='task1'},執行完畢!
From DemoThreadFactory's 訂單建立組-Worker-2,任務:Task{name='task2'},執行完畢!
From DemoThreadFactory's 訂單建立組-Worker-3,任務:Task{name='task3'},執行完畢!
From DemoThreadFactory's 訂單建立組-Worker-3,關閉線程池!

合理地配置線程池

要想合理的配置線程池,須要先分析任務的特性,能夠衝一下四個角度分析:

  • 任務的性質:CPU密集型任務、IO密集型任務和混合型任務
  • 任務的優先級:高、中、低
  • 任務的執行時間:長、中、短
  • 任務的依賴性:是否依賴其餘的系統資源,如數據庫鏈接。

性質不一樣任務能夠用不一樣規模的線程池分開處理。CPU密集型任務應該儘量小的線程,如配置cpu數量+1個線程的線程池。因爲IO密集型任務並非一直在執行任務,不能讓cpu閒着,則應配置儘量多的線程,如:cup數量*2。混合型的任務,若是能夠拆分,將其拆分紅一個CPU密集型任務和一個IO密集型任務,只要這2個任務執行的時間相差不是太大,那麼分解後執行的吞吐量將高於串行執行的吞吐量。能夠經過Runtime.getRuntime().availableProcessors()方法獲取cpu數量。優先級不一樣任務能夠對線程池採用優先級隊列來處理,讓優先級高的先執行。

使用隊列的時候建議使用有界隊列,有界隊列增長了系統的穩定性,若是採用無界隊列,任務太多的時候可能致使系統OOM,直接讓系統宕機。

線程池中線程數量的配置

線程池中總線程大小對系統的性能有必定的影響,咱們的目標是但願系統可以發揮最好的性能,過多或者太小的線程數量沒法有效的使用機器的性能。在Java Concurrency in Practice書中給出了估算線程池大小的公式:

Ncpu = CUP的數量
Ucpu = 目標CPU的使用率,0<=Ucpu<=1
W/C = 等待時間與計算時間的比例
爲保存處理器達到指望的使用率,最優的線程池的大小等於:
Nthreads = Ncpu × Ucpu × (1+W/C)
線程池數量 = CUP的數量 * 目標CPU的使用率 * 等待時間與計算時間的比例

使用建議

在《阿里巴巴java開發手冊》中指出了線程資源必須經過線程池提供,不容許在應用中自行顯示的建立線程,這樣一方面是線程的建立更加規範,能夠合理控制開闢線程的數量;另外一方面線程的細節管理交給線程池處理,優化了資源的開銷。而線程池不容許使用Executors去建立,而要經過ThreadPoolExecutor方式,這一方面是因爲jdk中Executor框架雖然提供瞭如newFixedThreadPool()、newSingleThreadExecutor()、newCachedThreadPool()等建立線程池的方法,但都有其侷限性,不夠靈活;另外因爲前面幾種方法內部也是經過ThreadPoolExecutor方式實現,使用ThreadPoolExecutor有助於你們明確線程池的運行規則,建立符合本身的業務場景須要的線程池,避免資源耗盡的風險。

線程池不容許使用Executors去建立,而是經過ThreadPoolExecutor的方式,這樣的處理方式讓寫的同窗更加明確線程池的運行規則,規避資源耗盡的風險。

說明:Executors返回的線程池對象的弊端以下:

1) FixedThreadPool和SingleThreadPool:

容許的請求隊列:LinkedBlockingQueue長度爲Integer.MAX_VALUE,可能會堆積大量的請求,從而致使OOM。

2) CachedThreadPool:

容許的建立線程數量爲Integer.MAX_VALUE,可能會建立大量的線程,從而致使OOM。

疑問:

Q:LinkedBlockingQueue吞吐量一般要高於ArrayBlockingQueue,爲何?

JUC中的Executor框架

Excecutor框架主要包含3部分的內容:

  1. 任務相關的:包含被執行的任務要實現的接口:Runnable接口或Callable接口
  2. 任務的執行相關的:包含任務執行機制的核心接口Executor,以及繼承自Executor的ExecutorService接口。Executor框架中有兩個關鍵的類實現了ExecutorService接口(ThreadPoolExecutor和ScheduleThreadPoolExecutor)
  3. 異步計算結果相關的:包含接口Future和實現Future接口的FutureTask類

Executors框架包括:

  • Executor
  • ExecutorService
  • ThreadPoolExecutor
  • Executors
  • Future
  • Callable
  • FutureTask
  • CompletableFuture
  • CompletionService
  • ExecutorCompletionService

Executor接口

Executor接口中定義了方法execute(Runable able)接口,該方法接受一個Runable實例,他來執行一個任務,任務即實現一個Runable接口的類。

ExecutorService接口

ExecutorService繼承於Executor接口,他提供了更爲豐富的線程實現方法,好比ExecutorService提供關閉本身的方法,以及爲跟蹤一個或多個異步任務執行情況而生成Future的方法。

ExecutorService有三種狀態:運行、關閉、終止。建立後便進入運行狀態,當調用了shutdown()方法時,便進入了關閉狀態,此時意味着ExecutorService再也不接受新的任務,可是他仍是會執行已經提交的任務,當全部已經提交了的任務執行完後,便達到終止狀態。若是不調用shutdown方法,ExecutorService方法會一直運行下去,系統通常不會主動關閉。

ThreadPoolExecutor類

線程池類,實現了ExecutorService接口中全部方法,參考線程池的使用。

ScheduleThreadPoolExecutor定時器

ScheduleThreadPoolExecutor繼承自ThreadPoolExecutor(實現了線程池的核心功能),實現了ScheduledExecutorService(實現了定時器調度功能),他主要用來延遲執行任務,或者定時執行任務。功能和Timer相似,可是ScheduleThreadPoolExecutor更強大、更靈活一些。Timer後臺是單個線程,而ScheduleThreadPoolExecutor能夠在建立的時候指定多個線程。

public class ScheduledThreadPoolExecutor
        extends ThreadPoolExecutor
        implements ScheduledExecutorService {
            public <V> ScheduledFuture<V> schedule(Callable<V> callable,
                                           long delay, TimeUnit unit);
        }

schedule:延遲執行任務1次

使用ScheduleThreadPoolExecutor的schedule方法,看一下這個方法的聲明:

public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit)

3個參數:

command:須要執行的任務

delay:須要延遲的時間

unit:參數2的時間單位,是個枚舉,能夠是天、小時、分鐘、秒、毫秒、納秒等

實例:

//只延遲調度一次
public static void main(String[] args) {
    ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3,
            new DemoThreadFactory("延遲調度線程池"));
    scheduledThreadPool.schedule(()->{
        System.out.println(System.currentTimeMillis()+"開始執行調度!");
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(System.currentTimeMillis()+"執行調度結束!");
    },3,TimeUnit.SECONDS);
}

輸出:

1598509985652開始執行調度!
1598509990653執行調度結束!

scheduleAtFixedRate:固定的頻率執行任務

使用ScheduleThreadPoolExecutor的scheduleAtFixedRate方法,該方法設置了執行週期,下一次執行時間至關因而上一次的執行時間加上period,任務每次執行完畢以後纔會計算下次的執行時間。

看一下這個方法的聲明:

public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit);

4個參數:

command:表示要執行的任務

initialDelay:表示延遲多久執行第一次

period:連續執行之間的時間間隔

unit:參數2和參數3的時間單位,是個枚舉,能夠是天、小時、分鐘、秒、毫秒、納秒等

假設系統調用scheduleAtFixedRate的時間是T1,那麼執行時間以下:

第1次:T1+initialDelay

第2次:T1+initialDelay+period(這時候若是第一次執行完後時間大於固定頻率的時間,就會被立刻調度起來)

第3次:T1+initialDelay+2*period

第n次:T1+initialDelay+(n-1)*period

實例:

//scheduleAtFixedRate()表示每次方法的執行週期是多久關注的是執行週期,若是已經到了執行週期,就會當即開啓調度任務,時間間隔是調度任務開始時間加週期
public static void main2(String[] args) throws ExecutionException, InterruptedException {
    //任務執行計數器
    AtomicInteger count = new AtomicInteger(1);
    ScheduledExecutorService scheduledThreadPool = new ScheduledThreadPoolExecutor(3,
            new DemoThreadFactory("延遲調度線程池"),new ThreadPoolExecutor.AbortPolicy());
    ScheduledFuture<?> schedule = scheduledThreadPool.scheduleAtFixedRate(() -> {
        int currCount = count.getAndIncrement();
        System.out.println(Thread.currentThread().getName()+":"+new Date(System.currentTimeMillis()) + " 第" + currCount + "次" + "開始執行");
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+":"+new Date(System.currentTimeMillis()) + " 第" + currCount + "次" + "結束執行");
    }, 1,3, TimeUnit.SECONDS);

}

輸出:

From DemoThreadFactory's 延遲調度線程池-Worker-1:Thu Aug 27 14:36:17 CST 2020 第1次開始執行
From DemoThreadFactory's 延遲調度線程池-Worker-1:Thu Aug 27 14:36:22 CST 2020 第1次結束執行
From DemoThreadFactory's 延遲調度線程池-Worker-1:Thu Aug 27 14:36:22 CST 2020 第2次開始執行
From DemoThreadFactory's 延遲調度線程池-Worker-1:Thu Aug 27 14:36:27 CST 2020 第2次結束執行
From DemoThreadFactory's 延遲調度線程池-Worker-2:Thu Aug 27 14:36:27 CST 2020 第3次開始執行
任務當前執行完畢以後會計算下次執行時間,下次執行時間爲上次執行的開始時間+period,這個時間小於第一次結束的時間了,說明小於系統當前時間了,會當即執行。

scheduleWithFixedDelay:固定的間隔執行任務

使用ScheduleThreadPoolExecutor的scheduleWithFixedDelay方法,該方法設置了執行週期,與scheduleAtFixedRate方法不一樣的是,下一次執行時間是上一次任務執行完的系統時間加上period,於是具體執行時間不是固定的,但週期是固定的,是採用相對固定的延遲來執行任務。看一下這個方法的聲明:

public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                     long initialDelay,
                                                     long delay,
                                                     TimeUnit unit);

4個參數:

command:表示要執行的任務

initialDelay:表示延遲多久執行第一次

period:表示下次執行時間和上次執行結束時間之間的間隔時間

unit:參數2和參數3的時間單位,是個枚舉,能夠是天、小時、分鐘、秒、毫秒、納秒等

假設系統調用scheduleAtFixedRate的時間是T1,那麼執行時間以下:

第1次:T1+initialDelay,執行結束時間:E1(執行結束時間是不固定的)

第2次:E1+period,執行結束時間:E2

第3次:E2+period,執行結束時間:E3

第4次:E3+period,執行結束時間:E4

第n次:上次執行結束時間+period

實例:

//scheduleWithFixedDelay()表示每次方法執行完後延遲多久執行,關注的是延遲時間,時間間隔是調度任務結束時間加延遲時間
public static void main(String[] args) throws ExecutionException, InterruptedException {
    //任務執行計數器
    AtomicInteger count = new AtomicInteger(1);
    ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(4,
            new DemoThreadFactory("延遲調度線程池"));
    ScheduledFuture<?> schedule = scheduledThreadPool.scheduleWithFixedDelay(() -> {
        int currCount = count.getAndIncrement();
        System.out.println(Thread.currentThread().getName()+":"+new Date(System.currentTimeMillis()) + " 第" + currCount + "次" + "開始執行");
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+":"+new Date(System.currentTimeMillis()) + " 第" + currCount + "次" + "結束執行");
    }, 1,3, TimeUnit.SECONDS);

}

輸出:

From DemoThreadFactory's 延遲調度線程池-Worker-1:Thu Aug 27 14:39:16 CST 2020 第1次開始執行
From DemoThreadFactory's 延遲調度線程池-Worker-1:Thu Aug 27 14:39:22 CST 2020 第1次結束執行
From DemoThreadFactory's 延遲調度線程池-Worker-1:Thu Aug 27 14:39:25 CST 2020 第2次開始執行
From DemoThreadFactory's 延遲調度線程池-Worker-1:Thu Aug 27 14:39:30 CST 2020 第2次結束執行
From DemoThreadFactory's 延遲調度線程池-Worker-2:Thu Aug 27 14:39:33 CST 2020 第3次開始執行
延遲1秒以後執行第1次,後面每次的執行時間和上次執行結束時間間隔3秒。

定時任務有異常——沒有對異常處理則定時任務會結束

先說補充點知識:schedule、scheduleAtFixedRate、scheduleWithFixedDelay這幾個方法有個返回值ScheduledFuture,經過ScheduledFuture能夠對執行的任務作一些操做,如判斷任務是否被取消、是否執行完成。

再回到上面代碼,任務中有個10/0的操做,會觸發異常,發生異常以後沒有任何現象,被ScheduledExecutorService內部給吞掉了,而後這個任務不再會執行了,scheduledFuture.isDone()輸出true,表示這個任務已經結束了,不再會被執行了。因此若是程序有異常,開發者本身注意try-catch處理一下,否則跑着跑着發現任務怎麼不跑了,也沒有異常輸出。

實例:

public static void main(String[] args) throws ExecutionException, InterruptedException {
    //任務執行計數器
    AtomicInteger count = new AtomicInteger(1);
    ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(4,
            new DemoThreadFactory("延遲調度線程池"));
    ScheduledFuture<?> schedule = scheduledThreadPool.scheduleWithFixedDelay(() -> {
        int currCount = count.getAndIncrement();
        System.out.println(Thread.currentThread().getName()+":"+new Date(System.currentTimeMillis()) + " 第" + currCount + "次" + "開始執行");
    /*    try {
            System.out.println(10/0);
        } catch (Exception e) {
            e.printStackTrace();
        }*/
        System.out.println(10/0);
        System.out.println(Thread.currentThread().getName()+":"+new Date(System.currentTimeMillis()) + " 第" + currCount + "次" + "結束執行");
    }, 1,3, TimeUnit.SECONDS);
    TimeUnit.SECONDS.sleep(3);
    System.out.println(schedule.isCancelled());
    System.out.println(schedule.isDone());
}

輸出:

From DemoThreadFactory's 延遲調度線程池-Worker-1:Thu Aug 27 14:45:09 CST 2020 第1次開始執行
false
true

取消定時任務的執行——調用ScheduledFuture的cancel方法

可能任務執行一會,想取消執行,能夠調用ScheduledFuture的cancel方法,參數表示是否給任務發送中斷信號。

示例:

public static void main(String[] args) throws ExecutionException, InterruptedException {
        //任務執行計數器
        AtomicInteger count = new AtomicInteger(1);
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(4,
                new DemoThreadFactory("延遲調度線程池"));
        ScheduledFuture<?> schedule = scheduledThreadPool.scheduleWithFixedDelay(() -> {
            int currCount = count.getAndIncrement();
            System.out.println(Thread.currentThread().getName()+":"+new Date(System.currentTimeMillis()) + " 第" + currCount + "次" + "開始執行");
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+":"+new Date(System.currentTimeMillis()) + " 第" + currCount + "次" + "結束執行");
        }, 1,3, TimeUnit.SECONDS);
        TimeUnit.SECONDS.sleep(5);
        schedule.cancel(false);
        TimeUnit.SECONDS.sleep(1);
        System.out.println("任務是否被取消:"+schedule.isCancelled());
        System.out.println("任務是否已完成:"+schedule.isDone());
    }
}

輸出:

From DemoThreadFactory's 延遲調度線程池-Worker-1:Thu Aug 27 14:53:12 CST 2020 第1次開始執行
任務是否被取消:true
任務是否已完成:true
From DemoThreadFactory's 延遲調度線程池-Worker-1:Thu Aug 27 14:53:17 CST 2020 第1次結束執行

Executors類——線程池工具類

Executors類,提供了一系列工廠方法用於建立線程池,返回的線程池都實現了ExecutorService接口。經常使用的方法有:

newSingleThreadExecutor

public static ExecutorService newSingleThreadExecutor()
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory)

建立一個單線程的線程池。這個線程池只有一個線程在工做,也就是至關於單線程串行執行全部任務。若是這個惟一的線程由於異常結束,那麼會有一個新的線程來替代它。此線程池保證全部任務的執行順序按照任務的提交順序執行。內部使用了無限容量的LinkedBlockingQueue阻塞隊列來緩存任務,任務若是比較多,單線程若是處理不過來,會致使隊列堆滿,引起OOM。

newFixedThreadPool

public static ExecutorService newFixedThreadPool(int nThreads)
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory)

建立固定大小的線程池。每次提交一個任務就建立一個線程,直到線程達到線程池的最大大小。線程池的大小一旦達到最大值就會保持不變,在提交新任務,任務將會進入等待隊列中等待。若是某個線程由於執行異常而結束,那麼線程池會補充一個新線程。內部使用了無限容量的LinkedBlockingQueue阻塞隊列來緩存任務,任務若是比較多,若是處理不過來,會致使隊列堆滿,引起OOM。

newCachedThreadPool

public static ExecutorService newCachedThreadPool()
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory)

建立一個可緩存的線程池。若是線程池的大小超過了處理任務所須要的線程,

那麼就會回收部分空閒(60秒處於等待任務到來)的線程,當任務數增長時,此線程池又能夠智能的添加新線程來處理任務。此線程池的最大值是Integer的最大值(2^31-1)。內部使用了SynchronousQueue同步隊列來緩存任務,此隊列的特性是放入任務時必需要有對應的線程獲取任務,任務才能夠放入成功。若是處理的任務比較耗時,任務來的速度也比較快,會建立太多的線程引起OOM。

newScheduledThreadPool

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory)

建立一個大小無限的線程池。此線程池支持定時以及週期性執行任務的需求。

在《阿里巴巴java開發手冊》中指出了線程資源必須經過線程池提供,不容許在應用中自行顯示的建立線程,這樣一方面是線程的建立更加規範,能夠合理控制開闢線程的數量;另外一方面線程的細節管理交給線程池處理,優化了資源的開銷。而線程池不容許使用Executors去建立,而要經過ThreadPoolExecutor方式,這一方面是因爲jdk中Executor框架雖然提供瞭如newFixedThreadPool()、newSingleThreadExecutor()、newCachedThreadPool()等建立線程池的方法,但都有其侷限性,不夠靈活;另外因爲前面幾種方法內部也是經過ThreadPoolExecutor方式實現,使用ThreadPoolExecutor有助於你們明確線程池的運行規則,建立符合本身的業務場景須要的線程池,避免資源耗盡的風險。

Future、Callable接口

Future、Callable接口須要結合ExecutorService來使用,須要有線程池的支持。

Future接口定義了操做異步異步任務執行一些方法,如獲取異步任務的執行結果、取消任務的執行、判斷任務是否被取消、判斷任務執行是否完畢等。

Callable接口中定義了須要有返回的任務須要實現的方法。——至關於有返回值的Runnable

@FunctionalInterface
public interface Callable<V> {
    V call() throws Exception;
}

好比主線程讓一個子線程去執行任務,子線程可能比較耗時,啓動子線程開始執行任務後,主線程就去作其餘事情了,過了一會纔去獲取子任務的執行結果。

Future其餘方法介紹一下

cancel:取消在執行的任務,參數表示是否對執行的任務發送中斷信號,方法聲明以下:

boolean cancel(boolean mayInterruptIfRunning);

isCancelled:用來判斷任務是否被取消

isDone:判斷任務是否執行完畢。

調用線程池的submit方法執行任務,submit參數爲Callable接口:表示須要執行的任務有返回值,submit方法返回一個Future對象,Future至關於一個憑證,能夠在任意時間拿着這個憑證去獲取對應任務的執行結果(調用其get方法),代碼中調用了result.get()方法以後,此方法會阻塞當前線程直到任務執行結束。

實例:

public static void main(String[] args) throws ExecutionException, InterruptedException {
            String taskName = "task";
        Future<String> future = executor.submit(() -> {
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
           // System.out.println(Thread.currentThread().getName() + "線程執行" + taskName + "完畢!");
            return "finished";
        });
        TimeUnit.SECONDS.sleep(1);
        //取消正在執行的任務,mayInterruptIfRunning:是否發送中斷信息
        future.cancel(false);
        System.out.println(future.isCancelled());
        System.out.println(future.isDone());
        //System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ",結果:" + future.get());
        try {
            //超時獲取異步任務執行結果
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ",結果:" + future.get(10,TimeUnit.SECONDS));
        } catch (TimeoutException e) {
            e.printStackTrace();
        }
        executor.shutdown();
    }
}

輸出:

Exception in thread "main" java.util.concurrent.CancellationException
	at java.util.concurrent.FutureTask.report(FutureTask.java:121)
	at java.util.concurrent.FutureTask.get(FutureTask.java:206)
	at com.self.current.FutureTest.main(FutureTest.java:46)
true
true

FutureTask類

FutureTask除了實現Future接口,還實現了Runnable接口,所以FutureTask能夠交給Executor執行,也能夠交給線程執行執行(Thread有個Runnable的構造方法),FutureTask表示帶返回值結果的任務。線程池的submit方法返回的Future實際類型正是FutureTask對象 .

疑問:

Q:線程池執行submit()方法是如何調用Callable任務的?

A:Callable經過線程池執行的過程,封裝爲Runnable。線程池執行submit()方法會把Callable包裝成FutrueTask對象,此對象實現了Runnable接口,當調用FutrueTask的run方法時,會把其屬性中的Callable拿出來執行call()方法。示例代碼以下:

public <T> Future<T> submit(Callable<T> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task);
        execute(ftask);
        return ftask;
    }
    
        public void run() {
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    setException(ex);
                }
                if (ran)
                    set(result);
            }
        }
    }

Q:多線程並行處理定時任務時,Timer運行多個TimeTask時,只要其中之一沒有捕獲拋出的異常,其它任務便會自動終止運行,使用ScheduledExecutorService則沒有這個問題。是由於ScheduledExecutorService是多線程麼?

A:是由於Timer只有一個線程在運行,while(true)循環不斷地從隊列中獲取任務執行,而當線程被被殺死或者中斷時,就至關於關閉了Timer.

Q: ScheduleThreadPoolExecutor定時器並不關心線程數多少,他不是併發的執行多任務,只關心調度一個定時任務,線程數的多少隻是影響多個任務再調度時須要多個線程,這樣理解對麼?

A:我認爲這樣理解是對的,而這樣也能夠解釋上面Timer運行多個TimeTask時,只要其中之一沒有捕獲拋出的異常,其它任務便會自動終止運行的緣由,是由於Timer只有一個線程在運行,while(true)循環不斷地從隊列中獲取任務執行,而當線程被被殺死或者中斷時,就至關於關閉了Timer.下面是多個任務調度時會建立多個線程去執行。

From DemoThreadFactory's 延遲調度線程池-Worker-1:Thu Aug 27 14:22:22 CST 2020 第1次開始執行
From DemoThreadFactory's 延遲調度線程池-Worker-2:Thu Aug 27 14:22:22 CST 2020 第2次開始執行
From DemoThreadFactory's 延遲調度線程池-Worker-3:Thu Aug 27 14:22:22 CST 2020 第3次開始執行
From DemoThreadFactory's 延遲調度線程池-Worker-1:Thu Aug 27 14:22:27 CST 2020 第1次結束執行
From DemoThreadFactory's 延遲調度線程池-Worker-2:Thu Aug 27 14:22:27 CST 2020 第2次結束執行
From DemoThreadFactory's 延遲調度線程池-Worker-3:Thu Aug 27 14:22:27 CST 2020 第3次結束執行
From DemoThreadFactory's 延遲調度線程池-Worker-1:Thu Aug 27 14:22:30 CST 2020 第4次開始執行
From DemoThreadFactory's 延遲調度線程池-Worker-4:Thu Aug 27 14:22:30 CST 2020 第5次開始執行
From DemoThreadFactory's 延遲調度線程池-Worker-2:Thu Aug 27 14:22:30 CST 2020 第6次開始執行

CompletionService接口——獲取線程池中已經完成的任務

CompletionService至關於一個執行任務的服務,經過submit丟任務給這個服務,服務內部去執行任務,能夠經過服務提供的一些方法獲取服務中已經完成的任務。

接口內的幾個方法:

Future<V> submit(Callable<V> task);

用於向服務中提交有返回結果的任務,並返回Future對象

Future<V> submit(Runnable task, V result);

用戶向服務中提交有返回值的任務去執行,並返回Future對象。Runnable會被包裝成有返回值的Callable,返回值爲傳入的result。

Future<V> take() throws InterruptedException;

從服務中返回並移除一個已經完成的任務,若是獲取不到,會一致阻塞到有返回值爲止。此方法會響應線程中斷。

Future<V> poll();

從服務中返回並移除一個已經完成的任務,若是內部沒有已經完成的任務,則返回空,此方法會當即響應。

Future<V> poll(long timeout, TimeUnit unit) throws InterruptedException;

嘗試在指定的時間內從服務中返回並移除一個已經完成的任務,等待的時間超時仍是沒有獲取到已完成的任務,則返回空。此方法會響應線程中斷

經過submit向內部提交任意多個任務,經過take方法能夠獲取已經執行完成的任務,若是獲取不到將等待。

ExecutorCompletionService

ExecutorCompletionService類是CompletionService接口的具體實現。

說一下其內部原理,ExecutorCompletionService建立的時候會傳入一個線程池,調用submit方法傳入須要執行的任務,任務由內部的線程池來處理;ExecutorCompletionService內部有個阻塞隊列,任意一個任務完成以後,會將任務的執行結果(Future類型)放入阻塞隊列中,而後其餘線程能夠調用它take、poll方法從這個阻塞隊列中獲取一個已經完成的任務,獲取任務返回結果的順序和任務執行完成的前後順序一致,因此最早完成的任務會先返回。

看一下構造方法:

public ExecutorCompletionService(Executor executor) {
        if (executor == null)
            throw new NullPointerException();
        this.executor = executor;
        this.aes = (executor instanceof AbstractExecutorService) ?
            (AbstractExecutorService) executor : null;
        this.completionQueue = new LinkedBlockingQueue<Future<V>>();
    }

構造方法須要傳入一個Executor對象,這個對象表示任務執行器,全部傳入的任務會被這個執行器執行。

completionQueue是用來存儲任務結果的阻塞隊列,默認用採用的是LinkedBlockingQueue,也支持開發本身設置。經過submit傳入須要執行的任務,任務執行完成以後,會放入completionQueue中。

任務完成入隊操做原理:

仍是經過線程池execute()方法執行一個FutureTask包裝的Callable任務,FutureTask裏的run方法會調用Callable任務call()方法執行具體的認爲,並在執行結算後執行set(result);設置返回值操做,而設置返回值操做中的finishCompletion()方法會調用鉤子方法done(),ExecutorCompletionService裏定義的QueueingFuture繼承了FutureTask,重寫了鉤子方法,把完成的方法入隊保存起來了。

場景:買新房了,而後在網上下單買冰箱、洗衣機,電器商家不一樣,因此送貨耗時不同,而後等他們送貨,快遞只願送到樓下,而後咱們本身將其搬到樓上的家中。 這時候咱們須要根據異步先完成的快遞,拿個先到對其獲取作處理——搬上樓。

示例:

public class ExecutorCompletionServiceTest {

    static class GoodsModel {
        //商品名稱
        String name;
        //購物開始時間
        long startime;
        //送到的時間
        long endtime;

        public GoodsModel(String name, long startime, long endtime) {
            this.name = name;
            this.startime = startime;
            this.endtime = endtime;
        }

        @Override
        public String toString() {
            return name + ",下單時間[" + this.startime + "," + endtime + "],耗時:" + (this.endtime - this.startime);
        }
    }
    /**
     * 將商品搬上樓
     *
     * @param goodsModel
     * @throws InterruptedException
     */
    static void moveUp(GoodsModel goodsModel) throws InterruptedException {
        //休眠5秒,模擬搬上樓耗時
        TimeUnit.SECONDS.sleep(5);
        System.out.println("將商品搬上樓,商品信息:" + goodsModel);
    }

    /**
     * 模擬下單
     *
     * @param name     商品名稱
     * @param costTime 耗時
     * @return
     */
    static Callable<GoodsModel> buyGoods(String name, long costTime) {
        return () -> {
            long startTime = System.currentTimeMillis();
            System.out.println(startTime + "購買" + name + "下單!");
            //模擬送貨耗時
            try {
                TimeUnit.SECONDS.sleep(costTime);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            long endTime = System.currentTimeMillis();
            System.out.println(endTime + name + "送到了!");
            return new GoodsModel(name, startTime, endTime);
        };
    }

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        long st = System.currentTimeMillis();
        System.out.println(st + "開始購物!");
        ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 60,
                TimeUnit.SECONDS, new LinkedBlockingQueue<>(10),
                new DemoThreadFactory("訂單建立組"), new ThreadPoolExecutor.AbortPolicy());
        ExecutorCompletionService<GoodsModel> completionService = new ExecutorCompletionService<>(executor);
        //異步下單購買
        completionService.submit(buyGoods("電視機", 3));
        completionService.submit(buyGoods("洗碗機", 5));
        executor.shutdown();
        for (int i = 0; i < 2; i++) {
            //能夠獲取到最早到的商品
            GoodsModel goodsModel = completionService.take().get();
            //將最早到的商品送上樓
            moveUp(goodsModel);
        }
        long et = System.currentTimeMillis();
        System.out.println(et + "貨物已送到家裏咯,哈哈哈!");
        System.out.println("總耗時:" + (et - st));
    }
}

1598583792616開始購物!
1598583792707購買電視機下單!
1598583792708購買洗碗機下單!
1598583795708電視機送到了!
1598583797709洗碗機送到了!
將商品搬上樓,商品信息:電視機,下單時間[1598583792707,1598583795708],耗時:3001
將商品搬上樓,商品信息:洗碗機,下單時間[1598583792708,1598583797709],耗時:5001
1598583805710貨物已送到家裏咯,哈哈哈!
總耗時:13094

異步執行一批任務,有一個完成當即返回,其餘取消——線程池invokeAny ()方法

若是是要返回全部的任務結果,則調用 invokeAll(Collection<? extends Callable > tasks)方法,invokeAny ()和invokeAll()都有超時調用方法。若是超時時間到了,調用結束後尚未所有完成,會對全部工做線程發送中斷信號中斷操做。

方法聲明以下:

<T> T invokeAny(Collection<? extends Callable<T>> tasks)
        throws InterruptedException, ExecutionException;

示例:

public static void main(String[] args) throws InterruptedException, ExecutionException {
        long st = System.currentTimeMillis();
        ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 60,
                TimeUnit.SECONDS, new LinkedBlockingQueue<>(10),
                new DemoThreadFactory("訂單建立組"), new ThreadPoolExecutor.AbortPolicy());
        List<Callable<Integer>> list = new ArrayList<>();
        int taskCount = 5;
        for (int i = taskCount; i > 0; i--) {
            int j = i * 2;
            String taskName = "任務"+i;
            list.add(() -> {
                TimeUnit.SECONDS.sleep(j);
                System.out.println(taskName+"執行完畢!");
                return j;
            });
        }
        //Integer integer = invokeAny(executor, list);
        //ExecutorService提供異步執行一批任務,有一個完成當即返回,其餘取消
        Integer integer = executor.invokeAny(list);
        System.out.println("耗時:" + (System.currentTimeMillis() - st) + ",執行結果:" + integer);
        executor.shutdown();
    }

    private static <T> T invokeAny(ThreadPoolExecutor executor, List<Callable<T>> list) throws InterruptedException, ExecutionException {
        ExecutorCompletionService<T> completionService = new ExecutorCompletionService(executor);
        List<Future<T>> futureList = new ArrayList<>();
        for (Callable<T> s : list) {
            futureList.add(completionService.submit(s));
        }
        int n = list.size();
        try {
            for (int i = 0; i < n; ++i) {
                T r = completionService.take().get();
                if (r != null) {
                    return r;
                }
            }
        } finally {
            for (Future<T> future : futureList) {
                future.cancel(true);
            }
        }
        return null;
    }
}

輸出:

任務1執行完畢!
耗時:2053,執行結果:2

CompletableFuture——當異步任務完成或者發生異常時,自動調用回調對象的回調方法,主線程無需等待獲取結果,異步是以守護線程執行的,若是是用線程池做爲執行器則不是守護線程

使用Future得到異步執行結果時,要麼調用阻塞方法get(),要麼輪詢看isDone()是否爲true,這兩種方法都不是很好,由於主線程也會被迫等待。

從Java 8開始引入了CompletableFuture,它針對Future作了改進,能夠傳入回調對象,當異步任務完成或者發生異常時,自動調用回調對象的回調方法。

咱們以獲取股票價格爲例,看看如何使用CompletableFuture:

CompletableFuture的優勢是:

  • 異步任務結束時,會自動回調某個對象的方法;
  • 異步任務出錯時,會自動回調某個對象的方法;
  • 主線程設置好回調後,再也不關心異步任務的執行。

若是隻是實現了異步回調機制,咱們還看不出CompletableFuture相比Future的優點。CompletableFuture更強大的功能是,多個CompletableFuture能夠串行執行,多個CompletableFuture還能夠並行執行。

除了anyOf()能夠實現「任意個CompletableFuture只要一個成功」,allOf()能夠實現「全部CompletableFuture都必須成功」,這些組合操做能夠實現很是複雜的異步流程控制。

最後咱們注意CompletableFuture的命名規則:

  • xxx():表示該方法將繼續在已有的線程中執行;
  • xxxAsync():表示將異步在線程池中執行。

示例:

public class CompletableFutureTest {

    public static void main(String[] args) throws Exception {
        // 建立異步執行任務:
        CompletableFuture<Double> cf = CompletableFuture.supplyAsync(CompletableFutureTest::fetchPrice);
        // 若是執行成功:
        cf.thenAccept((result) -> {
            System.out.println("price: " + result);
        });
        // 若是執行異常:
        cf.exceptionally((e) -> {
            e.printStackTrace();
            return null;
        });
        // 主線程不要馬上結束,不然CompletableFuture默認使用的線程池會馬上關閉:
        Thread.sleep(200);
    }

    static Double fetchPrice() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
        }
        if (Math.random() < 0.3) {
            throw new RuntimeException("fetch price failed!");
        }
        return 5 + Math.random() * 20;
    }

}

定義兩個CompletableFuture,第一個CompletableFuture根據證券名稱查詢證券代碼,第二個CompletableFuture根據證券代碼查詢證券價格,這兩個CompletableFuture實現串行操做以下:

public class CompletableFutureSerialTest {

    public static void main(String[] args) throws InterruptedException {
        //先獲取股票代碼
        CompletableFuture<String> tesla = CompletableFuture.supplyAsync(() -> {
            return CompletableFutureSerialTest.queryCode("tesla");
        });
        //再獲取股票代碼對應的股價
        CompletableFuture<Double> priceFuture = tesla.thenApplyAsync((code) -> {
            return CompletableFutureSerialTest.fetchPrice(code);
        });
        //打印結果
        priceFuture.thenAccept((price)->{
            System.out.println("price: " + price);
        });
        // 主線程不要馬上結束,不然CompletableFuture默認使用的線程池會馬上關閉:
        Thread.sleep(2000);
    }

    static String queryCode(String name) {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
        }
        return "601857";
    }

    static Double fetchPrice(String code) {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
        }
        return 5 + Math.random() * 20;
    }
}

輸出:

price: 23.116752498711122

示例:同時重新浪和網易查詢證券代碼,只要任意一個返回結果,就進行下一步查詢價格,查詢價格也同時重新浪和網易查詢,只要任意一個返回結果,就完成操做。

public class CompletableFutureParallelTest {

    public static void main(String[] args) throws InterruptedException {
        // 兩個CompletableFuture執行異步查詢:
        CompletableFuture<String> teslaSina = CompletableFuture.supplyAsync(() -> {
            return CompletableFutureParallelTest.queryCode("tesla","https://finance.sina.com.cn/code/");
        });

        CompletableFuture<String> tesla163 = CompletableFuture.supplyAsync(() -> {
            return CompletableFutureParallelTest.queryCode("tesla","https://money.163.com/code/");
        });
        // 用anyOf合併爲一個新的CompletableFuture:
        CompletableFuture<Object> stockFuture = CompletableFuture.anyOf(tesla163, teslaSina);

        //再獲取股票代碼對應的股價
        // 兩個CompletableFuture執行異步查詢:
        CompletableFuture<Double> priceSina = stockFuture.thenApplyAsync((code) -> {
            return CompletableFutureParallelTest.fetchPrice(String.valueOf(code),"https://money.163.com/code/");
        });
        CompletableFuture<Double> price163 = stockFuture.thenApplyAsync((code) -> {
            return CompletableFutureParallelTest.fetchPrice(String.valueOf(code),"https://money.163.com/code/");
        });
        // 用anyOf合併爲一個新的CompletableFuture:
        CompletableFuture<Object> priceFuture = CompletableFuture.anyOf(priceSina, price163);

        //打印結果
        priceFuture.thenAccept((price)->{
            System.out.println("price: " + price);
        });
        // 主線程不要馬上結束,不然CompletableFuture默認使用的線程池會馬上關閉:
        Thread.sleep(2000);
    }

    static String queryCode(String name, String url) {
        System.out.println("query code from " + url + "...");
        try {
            Thread.sleep((long) (Math.random() * 100));
        } catch (InterruptedException e) {
        }
        return "601857";
    }

    static Double fetchPrice(String code, String url) {
        System.out.println("query price from " + url + "...");
        try {
            Thread.sleep((long) (Math.random() * 100));
        } catch (InterruptedException e) {
        }
        return 5 + Math.random() * 20;
    }
}

query code from https://finance.sina.com.cn/code/...
query code from https://money.163.com/code/...
query price from https://money.163.com/code/...
query price from https://money.163.com/code/...
price: 17.34369661842006

java中的CAS

需求:咱們開發了一個網站,須要對訪問量進行統計,用戶每次發一次請求,訪問量+1,如何實現呢?

咱們在看一下count++操做,count++操做其實是被拆分爲3步驟執行:

1. 獲取count的值,記作A:A=count
2. 將A的值+1,獲得B:B = A+1
3. 讓B賦值給count:count = B

方式2中咱們經過加鎖的方式讓上面3步驟同時只能被一個線程操做,從而保證結果的正確性。

咱們是否能夠只在第3步加鎖,減小加鎖的範圍,對第3步作如下處理:

獲取鎖
第三步獲取一下count最新的值,記作LV
判斷LV是否等於A,若是相等,則將B的值賦給count,並返回true,否者返回false
釋放鎖

若是咱們發現第3步返回的是false,咱們就再次去獲取count,將count賦值給A,對A+1賦值給B,而後再將A、B的值帶入到上面的過程當中執行,直到上面的結果返回true爲止。

示例:(本身實現一個CAS)

public class CASTest {

    private static volatile int count = 0;

    private static void request() throws InterruptedException {
        //模擬耗時5毫秒
        TimeUnit.MILLISECONDS.sleep(5);
        int execeptVal;
        do {
            execeptVal = getCount();
        } while (!compareAndSet(execeptVal, execeptVal + 1));
    }

    private static synchronized boolean compareAndSet(int execeptVal, int newVal) {
        if (getCount() == execeptVal) {
            count = newVal;
            return true;
        }
        return false;
    }

    public static int getCount() {
        return count;
    }

    public static void main(String[] args) throws InterruptedException {
        long starTime = System.currentTimeMillis();
        ExecutorService threadPool = Executors.newCachedThreadPool();
        int userCount = 100;
        CountDownLatch latch = new CountDownLatch(100);
        for (int i = 0; i < userCount; i++) {
            threadPool.execute(() -> {
                try {
                    for (int j = 0; j < 10; j++) {
                        CASTest.request();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    latch.countDown();
                }

            });
        }
        latch.await();
        long endTime = System.currentTimeMillis();
        System.out.println(Thread.currentThread().getName() + ",耗時:" + (endTime - starTime) + ",count=" + count);
        threadPool.shutdown();
    }
}

輸出:

main,耗時:133,count=1000

代碼中用了volatile關鍵字修飾了count,能夠保證count在多線程狀況下的可見性。

我們再看一下代碼,compareAndSwap方法,咱們給起個簡稱吧叫CAS.這個方法使用synchronized修飾了,能保證此方法是線程安全的,多線程狀況下此方法是串行執行的。方法由兩個參數,expectCount:表示指望的值,newCount:表示要給count設置的新值。方法內部經過getCount()獲取count當前的值,而後與指望的值expectCount比較,若是指望的值和count當前的值一致,則將新值newCount賦值給count。

再看一下request()方法,方法中有個do-while循環,循環內部獲取count當前值賦值給了expectCount,循環結束的條件是compareAndSwap返回true,也就是說若是compareAndSwap若是不成功,循環再次獲取count的最新值,而後+1,再次調用compareAndSwap方法,直到compareAndSwap返回成功爲止。

代碼中至關於將count++拆分開了,只對最後一步加鎖了,減小了鎖的範圍,此代碼的性能是否是比方式2快很多,還能保證結果的正確性。你們是否是感受這個compareAndSwap方法挺好的,這東西確實很好,java中已經給咱們提供了CAS的操做,功能很是強大,咱們繼續向下看。

CAS

CAS,compare and swap的縮寫,中文翻譯成比較並交換。

CAS 操做包含三個操做數 —— 內存位置(V)、預期原值(A)和新值(B)。若是內存位置的值與預期原值相匹配,那麼處理器會自動將該位置值更新爲新值 。不然,處理器不作任何操做。不管哪一種狀況,它都會在 CAS 指令以前返回該 位置的值。(在 CAS 的一些特殊狀況下將僅返回 CAS 是否成功,而不提取當前 值。)CAS 有效地說明了「我認爲位置 V 應該包含值 A;若是包含該值,則將 B 放到這個位置;不然,不要更改該位置,只告訴我這個位置如今的值便可。」

一般將 CAS 用於同步的方式是從地址 V 讀取值 A,執行多步計算來得到新 值 B,而後使用 CAS 將 V 的值從 A 改成 B。若是 V 處的值還沒有同時更改,則 CAS 操做成功。

不少地方說CAS操做是非阻塞的,其實系統底層進行CAS操做的時候,會判斷當前系統是否爲多核系統,若是是就給總線加鎖,因此同一芯片上的其餘處理器就暫時不能經過總線訪問內存,保證了該指令在多處理器環境下的原子性。總線上鎖的,其餘線程執行CAS仍是會被阻塞一下,只是時間可能會很是短暫,因此說CAS是非阻塞的並不正確,只能說阻塞的時間是很是短的。

java中提供了對CAS操做的支持,具體在sun.misc.Unsafe類中,聲明以下:

public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

上面三個方法都是相似的,主要對4個參數作一下說明。

var1:表示要操做的對象

var2:表示要操做對象中屬性地址的偏移量

var4:表示須要修改數據的指望的值

var5:表示須要修改成的新值

悲觀鎖 (ReentrantLock)VS 樂觀鎖 (CAS)

synchronized、ReentrantLock這種獨佔鎖屬於悲觀鎖,它是在假設須要操做的代碼必定會發生衝突的,執行代碼的時候先對代碼加鎖,讓其餘線程在外面等候排隊獲取鎖。悲觀鎖若是鎖的時間比較長,會致使其餘線程一直處於等待狀態,像咱們部署的web應用,通常部署在tomcat中,內部經過線程池來處理用戶的請求,若是不少請求都處於等待獲取鎖的狀態,可能會耗盡tomcat線程池,從而致使系統沒法處理後面的請求,致使服務器處於不可用狀態。

除此以外,還有樂觀鎖,樂觀鎖的含義就是假設系統沒有發生併發衝突,先按無鎖方式執行業務,到最後了檢查執行業務期間是否有併發致使數據被修改了,若是有併發致使數據被修改了 ,就快速返回失敗,這樣的操做使系統併發性能更高一些。cas中就使用了這樣的操做。

關於樂觀鎖這塊,想必你們在數據庫中也有用到過,給你們舉個例子,可能之後會用到。

若是大家的網站中有調用支付寶充值接口的,支付寶那邊充值成功了會回調商戶系統,商戶系統接收到請求以後怎麼處理呢?假設用戶經過支付寶在商戶系統中充值100,支付寶那邊會從用戶帳戶中扣除100,商戶系統接收到支付寶請求以後應該在商戶系統中給用戶帳戶增長100,而且把訂單狀態置爲成功。

那咱們能夠用樂觀鎖來實現,給訂單表加個版本號version,要求每次更新訂單數據,將版本號+1,那麼上面的過程能夠改成:

獲取訂單信息,將version的值賦值給V_A
if(訂單狀態==待處理){
    開啓事務
    給用戶帳戶增長100
    update影響行數 = update 訂單表 set version = version + 1 where id = 訂單號 and version = V_A;
    if(update影響行數==1){
        提交事務
    }else{
        回滾事務
    }
}
返回訂單處理成功

上面的update語句至關於咱們說的CAS操做,執行這個update語句的時候,多線程狀況下,數據庫會對當前訂單記錄加鎖,保證只有一條執行成功,執行成功的,影響行數爲1,執行失敗的影響行數爲0,根據影響行數來決定提交仍是回滾事務。上面操做還有一點是將事務範圍縮小了,也提高了系統併發處理的性能。

CAS 的問題

cas這麼好用,那麼有沒有什麼問題呢?

ABA問題

CAS須要在操做值的時候檢查下值有沒有發生變化,若是沒有發生變化則更新,可是若是一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,可是實際上卻變化了。這就是CAS的ABA問題。常見的解決思路是使用版本號。在變量前面追加上版本號,每次變量更新的時候把版本號加一,那麼A-B-A 就會變成1A-2B-3A。目前在JDK的atomic包裏提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法做用是首先檢查當前引用是否等於預期引用,而且當前標誌是否等於預期標誌,若是所有相等,則以原子方式將該引用和該標誌的值設置爲給定的更新值。

循環時間長開銷大

上面咱們說過若是CAS不成功,則會原地循環(自旋操做),若是長時間自旋會給CPU帶來很是大的執行開銷。併發量比較大的狀況下,CAS成功機率可能比較低,可能會重試不少次纔會成功。

併發量大的狀況下應該改成悲觀鎖避免自旋帶來的CPU的大量開銷。

使用JUC中的類實現計數器

juc框架中提供了一些原子操做,底層是經過Unsafe類中的cas操做實現的。經過原子操做能夠保證數據在併發狀況下的正確性。

示例:

public class CASTest1 {

    private static AtomicInteger count = new AtomicInteger(0);

    private static void request() throws InterruptedException {
        //模擬耗時5毫秒
        TimeUnit.MILLISECONDS.sleep(5);
        count.getAndIncrement();
    }

    public static void main(String[] args) throws InterruptedException {
        long starTime = System.currentTimeMillis();
        ExecutorService threadPool = Executors.newCachedThreadPool();
        int userCount = 100;
        CountDownLatch latch = new CountDownLatch(100);
        for (int i = 0; i < userCount; i++) {
            threadPool.execute(() -> {
                try {
                    for (int j = 0; j < 10; j++) {
                        CASTest1.request();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    latch.countDown();
                }

            });
        }
        latch.await();
        long endTime = System.currentTimeMillis();
        System.out.println(Thread.currentThread().getName() + ",耗時:" + (endTime - starTime) + ",count=" + count);
        threadPool.shutdown();
    }
}

JUC底層工具類Unsafe

juc中大部分類都是依賴於Unsafe來實現的,主要用到了Unsafe中的CAS、線程掛起、線程恢復等相關功能。因此若是打算深刻了解JUC原理的,必須先了解一下Unsafe類。

Unsafe類的功能圖:

Unsafe是位於sun.misc包下的一個類,主要提供一些用於執行低級別、不安全操做的方法,如直接訪問系統內存資源、自主管理內存資源等,這些方法在提高Java運行效率、加強Java語言底層資源操做能力方面起到了很大的做用。但因爲Unsafe類使Java語言擁有了相似C語言指針同樣操做內存空間的能力,這無疑也增長了程序發生相關指針問題的風險。在程序中過分、不正確使用Unsafe類會使得程序出錯的機率變大,使得Java這種安全的語言變得再也不「安全」,所以對Unsafe的使用必定要慎重。

從Unsafe功能圖上看出,Unsafe提供的API大體可分爲內存操做、CAS、Class相關、對象操做、線程調度、系統信息獲取、內存屏障、數組操做等幾類,本文主要介紹3個經常使用的操做:CAS、線程調度、對象操做。

看一下UnSafe的源碼部分:

public final class Unsafe {
  // 單例對象
  private static final Unsafe theUnsafe;

  private Unsafe() {
  }
  @CallerSensitive
  public static Unsafe getUnsafe() {
    Class var0 = Reflection.getCallerClass();
    // 僅在引導類加載器`BootstrapClassLoader`加載時才合法
    if(!VM.isSystemDomainLoader(var0.getClassLoader())) {    
      throw new SecurityException("Unsafe");
    } else {
      return theUnsafe;
    }
  }
}

從代碼中能夠看出,Unsafe類爲單例實現,提供靜態方法getUnsafe獲取Unsafe實例,內部會判斷當前調用者是不是由系統類加載器加載的,若是不是系統類加載器加載的,會拋出SecurityException異常。

獲取Unsafe的兩種方式:

  1. 能夠把咱們的類放在jdk的lib目錄下,那麼啓動的時候會自動加載,這種方式不是很好。
  2. 經過反射能夠獲取到Unsafe中的theUnsafe字段的值,這樣能夠獲取到Unsafe對象的實例。

經過反射獲取Unsafe實例

public class UnsafeTest {
    //經過反射獲取Unsafe實例
    private static Unsafe unsafe;

    static {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            //基本上你經過反射獲得的字段就像任何其餘字段同樣,可是當你調用get方法時,你傳遞的是null,由於沒有實例能夠做用。
            //field.get(null)方法參數傳遞的是實例,而靜態域是沒有實例的,獲取靜態變量直接用field.get(null)。
            unsafe = (Unsafe) field.get(null);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        System.out.println(unsafe);
    }
}

Unsafe中的CAS操做

看一下Unsafe中CAS相關方法定義:

/**
 * CAS 操做
 *
 * @param o        包含要修改field的對象
 * @param offset   對象中某field的偏移量
 * @param expected 指望值
 * @param update   更新值
 * @return true | false
 */
public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update);

public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update);

public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update);

什麼是CAS? 即比較並替換,實現併發算法時經常使用到的一種技術。CAS操做包含三個操做數——內存位置、預期原值及新值。執行CAS操做的時候,將內存位置的值與預期原值比較,若是相匹配,那麼處理器會自動將該位置值更新爲新值,不然,處理器不作任何操做,多個線程同時執行cas操做,只有一個會成功。咱們都知道,CAS是一條CPU的原子指令(cmpxchg指令:讀做compare and change),不會形成所謂的數據不一致問題,Unsafe提供的CAS方法(如compareAndSwapXXX)底層實現即爲CPU指令cmpxchg。執行cmpxchg指令的時候,會判斷當前系統是否爲多核系統,若是是就給總線加鎖,只有一個線程會對總線加鎖成功,加鎖成功以後會執行cas操做,也就是說CAS的原子性其實是CPU實現的, 其實在這一點上仍是有排他鎖的,只是比起用synchronized, 這裏的排他時間要短的多, 因此在多線程狀況下性能會比較好。

說一下offset,offeset爲字段的偏移量,每一個對象有個地址,offset是字段相對於對象地址的偏移量,對象地址記爲baseAddress,字段偏移量記爲offeset,那麼字段對應的實際地址就是baseAddress+offeset,因此cas經過對象、偏移量就能夠去操做字段對應的值了。

CAS在AtomicInteger上的應用

public class AtomicInteger extends Number implements java.io.Serializable {
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

static {
    try {
    	//初始化時獲取到AtomicInteger的字段value的字段的偏移量offeset
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}

private volatile int value;
    
    //原子加1,並返回加以前的值
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }   
    //原子加delta,並返回加以前的值
    public final int getAndAdd(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }
}

Unsafe中原子操做相關方法介紹

5個方法,內部經過自旋的CAS操做實現的,這些方法均可以保證操做的數據在多線程環境中的原子性,正確性。 看一下實現:

/**
 * int類型值原子操做,對var2地址對應的值作原子增長操做(增長var4)
 *
 * @param var1 操做的對象
 * @param var2 var2字段內存地址偏移量
 * @param var4 須要加的值
 * @return
 */
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while (!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

/**
 * long類型值原子操做,對var2地址對應的值作原子增長操做(增長var4)
 *
 * @param var1 操做的對象
 * @param var2 var2字段內存地址偏移量
 * @param var4 須要加的值
 * @return 返回舊值
 */
public final long getAndAddLong(Object var1, long var2, long var4) {
    long var6;
    do {
        var6 = this.getLongVolatile(var1, var2);
    } while (!this.compareAndSwapLong(var1, var2, var6, var6 + var4));

    return var6;
}

/**
 * int類型值原子操做方法,將var2地址對應的值置爲var4
 *
 * @param var1 操做的對象
 * @param var2 var2字段內存地址偏移量
 * @param var4 新值
 * @return 返回舊值
 */
public final int getAndSetInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while (!this.compareAndSwapInt(var1, var2, var5, var4));

    return var5;
}

/**
 * long類型值原子操做方法,將var2地址對應的值置爲var4
 *
 * @param var1 操做的對象
 * @param var2 var2字段內存地址偏移量
 * @param var4 新值
 * @return 返回舊值
 */
public final long getAndSetLong(Object var1, long var2, long var4) {
    long var6;
    do {
        var6 = this.getLongVolatile(var1, var2);
    } while (!this.compareAndSwapLong(var1, var2, var6, var4));

    return var6;
}

/**
 * Object類型值原子操做方法,將var2地址對應的值置爲var4
 *
 * @param var1 操做的對象
 * @param var2 var2字段內存地址偏移量
 * @param var4 新值
 * @return 返回舊值
 */
public final Object getAndSetObject(Object var1, long var2, Object var4) {
    Object var5;
    do {
        var5 = this.getObjectVolatile(var1, var2);
    } while (!this.compareAndSwapObject(var1, var2, var5, var4));

    return var5;
}

使用Unsafe實現一個網站計數功能:

public class UnsafeCountTest {
    //經過反射獲取Unsafe實例
    private static Unsafe unsafe;
    //count在Demo.class對象中的地址偏移量
    private static long valueOffset;
    //用來記錄網站訪問量,每次訪問+1
    private  static int count;
    //private volatile static int count;

    static {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            //基本上你經過反射獲得的字段就像任何其餘字段同樣,可是當你調用get方法時,你傳遞的是null,由於沒有實例能夠做用。
            //field.get(null)方法參數傳遞的是實例,而靜態域是沒有實例的,獲取靜態變量直接用field.get(null)。
            unsafe = (Unsafe) field.get(null);

            Field fieldC = UnsafeCountTest.class.getDeclaredField("count");
            valueOffset = unsafe.staticFieldOffset(fieldC);

        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    private static void request() throws InterruptedException {
        //模擬耗時5毫秒
        TimeUnit.MILLISECONDS.sleep(5);
        //對count原子加1
        unsafe.getAndAddInt(UnsafeCountTest.class,valueOffset,1);
    }

    public static void main(String[] args) throws InterruptedException {
        long starTime = System.currentTimeMillis();
        ExecutorService threadPool = Executors.newCachedThreadPool();
        int userCount = 100;
        CountDownLatch latch = new CountDownLatch(100);
        for (int i = 0; i < userCount; i++) {
            threadPool.execute(() -> {
                try {
                    for (int j = 0; j < 10; j++) {
                        UnsafeCountTest.request();
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    latch.countDown();
                }

            });
        }
        latch.await();
        long endTime = System.currentTimeMillis();
        System.out.println(Thread.currentThread().getName() + ",耗時:" + (endTime - starTime) + ",count=" + count);
        threadPool.shutdown();
    }
}

輸出:

main,耗時:157,count=1000

Unsafe中線程調度相關方法

這部分,包括線程掛起、恢復、鎖機制等方法。

//取消阻塞線程
public native void unpark(Object thread);
//阻塞線程,isAbsolute:是不是絕對時間,若是爲true,time是一個絕對時間,若是爲false,time是一個相對時間,time表示納秒
public native void park(boolean isAbsolute, long time);
//得到對象鎖(可重入鎖)
@Deprecated
public native void monitorEnter(Object o);
//釋放對象鎖
@Deprecated
public native void monitorExit(Object o);
//嘗試獲取對象鎖
@Deprecated
public native boolean tryMonitorEnter(Object o);

調用park後,線程將被阻塞,直到unpark調用或者超時,若是以前調用過unpark,不會進行阻塞,即park和unpark不區分前後順序。monitorEnter、monitorExit、tryMonitorEnter 3個方法已過時,不建議使用了。

線程中至關於有個許可,許可默認是0,調用park的時候,發現是0會阻塞當前線程,調用unpark以後,許可會被置爲1,並會喚醒當前線程。若是在park以前先調用了unpark方法,執行park方法的時候,不會阻塞。park方法被喚醒以後,許可又會被置爲0。屢次調用unpark的效果是同樣的,許可仍是1。

juc中的LockSupport類是經過unpark和park方法實現的。

實例:

//表示一直阻塞等待
 unsafe.park(false, 0);
//取消阻塞線程
unsafe.unpark(thread);

 //線程掛起3秒,超時等待
 unsafe.park(false, TimeUnit.SECONDS.toNanos(3));

Unsafe鎖示例——已廢棄

//模擬訪問一次
    public static void request() {
        unsafe.monitorEnter(Demo4.class);
        try {
            count++;
        } finally {
            unsafe.monitorExit(Demo4.class);
        }
    }

注意:

  1. monitorEnter、monitorExit、tryMonitorEnter 3個方法已過時,不建議使用了
  2. monitorEnter、monitorExit必須成對出現,出現的次數必須一致,也就是說鎖了n次,也必須釋放n次,不然會形成死鎖

Unsafe中保證變量的可見性的方法——至關於對要讀取和修改的變量加volatile

關於變量可見性須要先了解java內存模型JMM。

java中操做內存分爲主內存和工做內存,共享數據在主內存中,線程若是須要操做主內存的數據,須要先將主內存的數據複製到線程獨有的工做內存中,操做完成以後再將其刷新到主內存中。如線程A要想看到線程B修改後的數據,須要知足:線程B修改數據以後,須要將數據從本身的工做內存中刷新到主內存中,而且A須要去主內存中讀取數據。

被關鍵字volatile修飾的數據,有2點語義:

  1. 若是一個變量被volatile修飾,讀取這個變量時候,會強制從主內存中讀取,而後將其複製到當前線程的工做內存中使用
  2. 給volatile修飾的變量賦值的時候,會強制將賦值的結果從工做內存刷新到主內存

上面2點語義保證了被volatile修飾的數據在多線程中的可見性。

Unsafe中提供了和volatile語義同樣的功能的方法,以下:

//設置給定對象的int值,使用volatile語義,即設置後立馬更新到內存對其餘線程可見
public native void  putIntVolatile(Object o, long offset, int x);
//得到給定對象的指定偏移量offset的int值,使用volatile語義,總能獲取到最新的int值。
public native int getIntVolatile(Object o, long offset);

putIntVolatile方法,2個參數:

o:表示須要操做的對象

offset:表示操做對象中的某個字段地址偏移量

x:將offset對應的字段的值修改成x,而且當即刷新到主存中

調用這個方法,會強制將工做內存中修改的數據刷新到主內存中。

getIntVolatile方法,2個參數

o:表示須要操做的對象

offset:表示操做對象中的某個字段地址偏移量

每次調用這個方法都會強制從主內存讀取值,將其複製到工做內存中使用。

其餘的還有幾個putXXXVolatile、getXXXVolatile方法和上面2個相似。

JUC中原子類

JUC中原子類介紹

什麼是原子操做?

atomic 翻譯成中文是原子的意思。在化學上,咱們知道原子是構成通常物質的最小單位,在化學反應中是不可分割的。在咱們這裏 atomic 是指一個操做是不可中斷的。即便是在多個線程一塊兒執行的時候,一個操做一旦開始,就不會被其餘線程干擾,因此,所謂原子類說簡單點就是具備原子操做特徵的類,原子操做類提供了一些修改數據的方法,這些方法都是原子操做的,在多線程狀況下能夠確保被修改數據的正確性。

JUC中對原子操做提供了強大的支持,這些類位於java.util.concurrent.atomic包中.

JUC中原子類思惟導圖

基本類型原子類

使用原子的方式更新基本類型

  • AtomicInteger:int類型原子類
  • AtomicLong:long類型原子類
  • AtomicBoolean :boolean類型原子類

上面三個類提供的方法幾乎相同,這裏以 AtomicInteger 爲例子來介紹。

AtomicInteger 類經常使用方法

public final int get() //獲取當前的值
public final int getAndSet(int newValue)//獲取當前的值,並設置新的值
public final int getAndIncrement()//獲取當前的值,並自增
public final int getAndDecrement() //獲取當前的值,並自減
public final int getAndAdd(int delta) //獲取當前的值,並加上預期的值
boolean compareAndSet(int expect, int update) //若是輸入的數值等於預期值,則以原子方式將該值設置爲輸入值(update)
public final void lazySet(int newValue)//最終設置爲newValue,使用 lazySet 設置以後可能致使其餘線程在以後的一小段時間內仍是能夠讀到舊的值。

部分源碼

private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;

static {
    try {
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}

private volatile int value;

2個關鍵字段說明:

value:使用volatile修飾,能夠確保value在多線程中的可見性。

valueOffset:value屬性在AtomicInteger中的偏移量,經過這個偏移量能夠快速定位到value字段,這個是實現AtomicInteger的關鍵。

getAndIncrement源碼:

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

內部調用的是Unsafe類中的getAndAddInt方法,咱們看一下getAndAddInt源碼:

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

說明:

this.getIntVolatile:能夠確保從主內存中獲取變量最新的值。

compareAndSwapInt:CAS操做,CAS的原理是拿指望的值和本來的值做比較,若是相同則更新成新的值,能夠確保在多線程狀況下只有一個線程會操做成功,不成功的返回false。

上面有個do-while循環,compareAndSwapInt返回false以後,會再次從主內存中獲取變量的值,繼續作CAS操做,直到成功爲止。

getAndAddInt操做至關於線程安全的count++操做,如同:

synchronize(lock){

count++;

}

count++操做其實是被拆分爲3步驟執行:

  1. 獲取count的值,記作A:A=count
  2. 將A的值+1,獲得B:B = A+1
  3. 讓B賦值給count:count = B
    多線程狀況下會出現線程安全的問題,致使數據不許確。

synchronize的方式會致使佔時沒法獲取鎖的線程處於阻塞狀態,性能比較低。CAS的性能比synchronize要快不少。

數組類型原子類介紹

使用原子的方式更新數組裏的某個元素,能夠確保修改數組中數據的線程安全性。

  • AtomicIntegerArray:整形數組原子操做類
  • AtomicLongArray:長整形數組原子操做類
  • AtomicReferenceArray :引用類型數組原子操做類

上面三個類提供的方法幾乎相同,因此咱們這裏以 AtomicIntegerArray 爲例子來介紹。

AtomicIntegerArray 類經常使用方法

public final int get(int i) //獲取 index=i 位置元素的值
public final int getAndSet(int i, int newValue)//返回 index=i 位置的當前的值,並將其設置爲新值:newValue
public final int getAndIncrement(int i)//獲取 index=i 位置元素的值,並讓該位置的元素自增
public final int getAndDecrement(int i) //獲取 index=i 位置元素的值,並讓該位置的元素自減
public final int getAndAdd(int i, int delta) //獲取 index=i 位置元素的值,並加上預期的值
boolean compareAndSet(int i, int expect, int update) //若是輸入的數值等於預期值,則以原子方式將 index=i 位置的元素值設置爲輸入值(update)
public final void lazySet(int i, int newValue)//最終 將index=i 位置的元素設置爲newValue,使用 lazySet 設置以後可能致使其餘線程在以後的一小段時間內仍是能夠讀到舊的值。

示例

統計網站頁面訪問量,假設網站有10個頁面,如今模擬100我的並行訪問每一個頁面10次,而後將每一個頁面訪問量輸出,應該每一個頁面都是1000次,代碼以下:

public class AtomicIntegerArrayTest {

    private static AtomicIntegerArray array = new AtomicIntegerArray(10);

    private static void request(int page) throws InterruptedException {
        //模擬耗時5毫秒
        TimeUnit.MILLISECONDS.sleep(5);
        array.getAndIncrement(page-1);
    }

    public static void main(String[] args) throws InterruptedException {
        long starTime = System.currentTimeMillis();
        ExecutorService threadPool = Executors.newCachedThreadPool();
        int userCount = 100;
        CountDownLatch latch = new CountDownLatch(100);
        for (int i = 0; i < userCount; i++) {
            threadPool.execute(() -> {
                try {
                    for (int j = 0; j < 10; j++) {
                        for (int k = 1; k <= 10; k++) {
                            AtomicIntegerArrayTest.request(k);
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    latch.countDown();
                }

            });
        }
        latch.await();
        long endTime = System.currentTimeMillis();
        System.out.println(Thread.currentThread().getName() + ",耗時:" + (endTime - starTime) + ",array=" + array.toString());
        threadPool.shutdown();
    }
}

輸出:

main,耗時:672,array=[1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000]

引用類型原子類介紹

基本類型原子類只能更新一個變量,若是須要原子更新多個變量,須要使用 引用類型原子類。

  • AtomicReference:引用類型原子類
  • AtomicStampedRerence:原子更新引用類型裏的字段原子類
  • AtomicMarkableReference :原子更新帶有標記位的引用類型

AtomicReference 和 AtomicInteger 很是相似,不一樣之處在於 AtomicInteger是對整數的封裝,而AtomicReference則是對應普通的對象引用,它能夠確保你在修改對象引用時的線程安全性。在介紹AtomicReference的同時,咱們先來了解一個有關原子操做邏輯上的不足。

ABA問題

以前咱們說過,線程判斷被修改對象是否能夠正確寫入的條件是對象的當前值和指望值是否一致。這個邏輯從通常意義上來講是正確的,可是可能出現一個小小的例外,就是當你得到當前數據後,在準備修改成新值錢,對象的值被其餘線程連續修改了兩次,而通過這2次修改後,對象的值又恢復爲舊值,這樣,當前線程就沒法正確判斷這個對象到底是否被修改過,這就是所謂的ABA問題,可能會引起一些問題。

舉個例子

有一家蛋糕店,爲了挽留客戶,決定爲貴賓卡客戶一次性贈送20元,刺激客戶充值和消費,但條件是,每一位客戶只能被贈送一次,如今咱們用AtomicReference來實現這個功能,代碼以下:

public class AtomicReferenceTest1 {
    //帳戶原始餘額
    static int accountMoney = 19;
    //用於對帳戶餘額作原子操做
    static AtomicReference<Integer> money = new AtomicReference<>(accountMoney);

    /**
     * 模擬2個線程同時更新後臺數據庫,爲用戶充值
     */
    static void recharge() {
        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                for (int j = 0; j < 5; j++) {
                    Integer m = money.get();
                    if (m == accountMoney) {
                        if (money.compareAndSet(m, m + 20)) {
                            System.out.println("當前餘額:" + m + ",充值20元成功,餘額:" + money.get() + "元");
                        }
                    }
                    //休眠100ms
                    try {
                        TimeUnit.MILLISECONDS.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }

    /**
     * 模擬用戶消費
     */
    static void consume() throws InterruptedException {
        for (int i = 0; i < 5; i++) {
            Integer m = money.get();
            if (m > 20) {
                if (money.compareAndSet(m, m - 20)) {
                    System.out.println("當前餘額:" + m + ",成功消費10元,餘額:" + money.get() + "元");
                }
            }
            //休眠50ms
            TimeUnit.MILLISECONDS.sleep(50);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        recharge();
        consume();
    }
}

輸出:

當前餘額:19,充值20元成功,餘額:39元
當前餘額:39,成功消費10元,餘額:19元
當前餘額:39,成功消費10元,餘額:19元
當前餘額:19,充值20元成功,餘額:19元
當前餘額:19,充值20元成功,餘額:39元
當前餘額:39,成功消費10元,餘額:19元
當前餘額:19,充值20元成功,餘額:39元

從輸出中能夠看到,這個帳戶被前後反覆屢次充值。其緣由是帳戶餘額被反覆修改,修改後的值和原有的數值19同樣,使得CAS操做沒法正確判斷當前數據是否被修改過(是否被加過20)。雖然這種狀況出現的機率不大,可是依然是有可能出現的,所以,當業務上確實可能出現這種狀況時,咱們必須多加防範。JDK也爲咱們考慮到了這種狀況,使用AtomicStampedReference能夠很好地解決這個問題。

AtomicStampedReference內部不只維護了對象的值,還維護了一個版本號(咱們這裏把他稱爲時間戳,實際上它能夠使用任何一個整形來表示狀態值),當AtomicStampedReference對應的數值被修改時,除了更新數據自己外,還必需要更新版本號。當AtomicStampedReference設置對象值時,對象值及版本號都必須知足指望值,寫入纔會成功。所以,即便對象值被反覆讀寫,寫回原值,只要版本號發生變化,就能防止不恰當的寫入。

AtomicStampedReference的幾個Api在AtomicStampedReference的基礎上新增了有關版本號的信息。

//比較設置,參數依次爲:指望值、寫入新值、指望版本號、新版本號
public boolean compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp);
//得到當前對象引用
public V getReference();
//得到當前版本號
public int getStamp();
//設置當前對象引用和版本號
public void set(V newReference, int newStamp);

AtomicStampedReference內部維護了一個Pair對象存放值,綁定了當前值和版本號。

public AtomicStampedReference(V initialRef, int initialStamp) {
        pair = Pair.of(initialRef, initialStamp);
    }
      private static class Pair<T> {
        final T reference;
        final int stamp;
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }

    private volatile Pair<V> pair;

如今咱們使用AtomicStampedRerence來修改一下上面充值的問題,代碼以下:

public class AtomicStampedReferenceTest1 {

    //帳戶原始餘額
    static int accountMoney = 19;
    //用於對帳戶餘額作原子操做
    static AtomicStampedReference<Integer> money = new AtomicStampedReference<>(accountMoney, 0);

    /**
     * 模擬2個線程同時更新後臺數據庫,爲用戶充值
     */
    static void recharge() {
        for (int i = 0; i < 2; i++) {
            int stamp = money.getStamp();
            new Thread(() -> {
                for (int j = 0; j < 50; j++) {
                    Integer m = money.getReference();
                    if (m == accountMoney) {
                        if (money.compareAndSet(m, m + 20, stamp, stamp + 1)) {
                            System.out.println("當前時間戳:" + money.getStamp() + ",當前餘額:" + m + ",充值20元成功,餘額:" + money.getReference() + "元");
                        }
                    }
                    //休眠100ms
                    try {
                        TimeUnit.MILLISECONDS.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }

    /**
     * 模擬用戶消費
     */
    static void consume() throws InterruptedException {
        for (int i = 0; i < 50; i++) {
            Integer m = money.getReference();
            int stamp = money.getStamp();
            if (m > 20) {
                if (money.compareAndSet(m, m - 20, stamp, stamp + 1)) {
                    System.out.println("當前時間戳:" + money.getStamp() + ",當前餘額:" + m + ",成功消費20元,餘額:" + money.getReference() + "元");
                }
            }
            //休眠50ms
            TimeUnit.MILLISECONDS.sleep(50);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        recharge();
        consume();
    }
}

輸出:

當前時間戳:1,當前餘額:19,充值20元成功,餘額:39元
當前時間戳:2,當前餘額:39,成功消費20元,餘額:19元

關於這個時間戳的,在數據庫修改數據中也有相似的用法,好比2個編輯同時編輯一篇文章,同時提交,只容許一個用戶提交成功,提示另一個用戶:博客已被其餘人修改,如何實現呢?

數據庫對於樂觀鎖的ABA問題也是一樣的道理,加個版本號或者時間戳解決ABA問題。

博客表:t_blog(id,content,stamp),stamp默認值爲0,每次更新+1

A、B 二個編輯同時對一篇文章進行編輯,stamp都爲0,當點擊提交的時候,將stamp和id做爲條件更新博客內容,執行的sql以下:

update t_blog set content = 更新的內容,stamp = stamp+1 where id = 博客id and stamp = 0;

這條update會返回影響的行數,只有一個會返回1,表示更新成功,另一個提交者返回0,表示須要修改的數據已經不知足條件了,被其餘用戶給修改了。這種修改數據的方式也叫樂觀鎖。

對象的屬性修改原子類介紹

若是須要原子更新某個類裏的某個字段時,須要用到對象的屬性修改原子類。

  • AtomicIntegerFieldUpdater:原子更新整形字段的值
  • AtomicLongFieldUpdater:原子更新長整形字段的值
  • AtomicReferenceFieldUpdater :原子更新應用類型字段的值

要想原子地更新對象的屬性須要兩步:

  1. 第一步,由於對象的屬性修改類型原子類都是抽象類,因此每次使用都必須使用靜態方法 newUpdater()建立一個更新器,而且須要設置想要更新的類和屬性。
  2. 第二步,更新的對象屬性必須使用 public volatile 修飾符。

上面三個類提供的方法幾乎相同,因此咱們這裏以AtomicReferenceFieldUpdater爲例子來介紹。

調用AtomicReferenceFieldUpdater靜態方法newUpdater建立AtomicReferenceFieldUpdater對象

public static <U, W> AtomicReferenceFieldUpdater<U, W> newUpdater(Class<U> tclass, Class<W> vclass, String fieldName)

說明:

三個參數

tclass:須要操做的字段所在的類

vclass:操做字段的類型

fieldName:字段名稱

示例

多線程併發調用一個類的初始化方法,若是未被初始化過,將執行初始化工做,要求只能初始化一次

public class AtomicReferenceFieldUpdaterTest {
    static AtomicReferenceFieldUpdaterTest updaterTest = new AtomicReferenceFieldUpdaterTest();
       //不能操做static修飾的字段,會報Caused by: java.lang.IllegalArgumentException錯誤。compareAndSet操做的是對象實例的偏移值字段,static修飾的字段不屬於對象實例
     //必須被volatile修飾
    private volatile Boolean isInit = Boolean.FALSE;

    private static AtomicReferenceFieldUpdater referenceFieldUpdater = AtomicReferenceFieldUpdater.
            newUpdater(AtomicReferenceFieldUpdaterTest.class, Boolean.class, "isInit");

    public static void init() {
        if (referenceFieldUpdater.compareAndSet(updaterTest, false, true)) {
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ",開始初始化!");
            //模擬休眠3秒
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ",初始化完畢!");
        } else {
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + ",有其餘線程已經執行了初始化!");
        }
    }

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
           new Thread(()->{
               AtomicReferenceFieldUpdaterTest.init();
           }).start();
            
        }
    }
}

輸出:

1599030805588,Thread-0,開始初始化!
1599030805588,Thread-1,有其餘線程已經執行了初始化!
1599030805588,Thread-2,有其餘線程已經執行了初始化!
1599030805589,Thread-3,有其餘線程已經執行了初始化!
1599030805589,Thread-4,有其餘線程已經執行了初始化!
1599030808590,Thread-0,初始化完畢!

說明:

  1. isInit屬性必需要volatille修飾,能夠確保變量的可見性
  2. 能夠看出多線程同時執行init()方法,只有一個線程執行了初始化的操做,其餘線程跳過了。多個線程同時到達updater.compareAndSet,只有一個會成功。

ThreadLocal、InheritableThreadLocal

使用技巧,能夠用static方法包裝ThreadLocal的get/set方法,這樣就能夠直接調用了。也能夠在抽象類中定義ThreadLocal,這樣全部的繼承類也能調用到。

ThreadLocal

線程就至關於一我的同樣,每一個請求至關於一個任務,任務來了,人來處理,處理完畢以後,再處理下一個請求任務。人身上是否是有不少口袋,人剛開始準備處理任務的時候,咱們把任務的編號放在處理者的口袋中,而後處理中一路攜帶者,處理過程當中若是須要用到這個編號,直接從口袋中獲取就能夠了。那麼恰好java中線程設計的時候也考慮到了這些問題,Thread對象中就有不少口袋,用來放東西。Thread類中有這麼一個變量:

ThreadLocal.ThreadLocalMap threadLocals = null;

這個就是用來操做Thread中全部口袋的東西,ThreadLocalMap源碼中有一個數組(有興趣的能夠去看一下源碼),對應處理者身上不少口袋同樣,數組中的每一個元素對應一個口袋。

如何來操做Thread中的這些口袋呢,java爲咱們提供了一個類ThreadLocal,ThreadLocal對象用來操做Thread中的某一個口袋,能夠向這個口袋中放東西、獲取裏面的東西、清除裏面的東西,這個口袋一次性只能放一個東西,重複放東西會將裏面已經存在的東西覆蓋掉。

經常使用的3個方法:

//向Thread中某個口袋中放東西
public void set(T value);
//獲取這個口袋中目前放的東西
public T get();
//清空這個口袋中放的東西
public void remove()

ThreadLocal的官方API解釋爲:

「該類提供了線程局部 (thread-local) 變量。這些變量不一樣於它們的普通對應物,由於訪問某個變量(經過其 get 或 set 方法)的每一個線程都有本身的局部變量,它獨立於變量的初始化副本。ThreadLocal 實例一般是類中的 private static 字段,它們但願將狀態與某一個線程(例如,用戶 ID 或事務 ID)相關聯。」

InheritableThreadLocal

若是一個線程還作併發處理開啓多個線程時,這時候子線程若是也想要父線程保留在口袋裏的東西,就要使用InheritableThreadLocal來代替ThreadLocal。

父線程至關於主管,子線程至關於幹活的小弟,主管讓小弟們幹活的時候,將本身兜裏面的東西複製一份給小弟們使用,主管兜裏面可能有不少牛逼的工具,爲了提高小弟們的工做效率,給小弟們都複製一個,丟到小弟們的兜裏,而後小弟就能夠從本身的兜裏拿去這些東西使用了,也能夠清空本身兜裏面的東西。

Thread對象中有個inheritableThreadLocals變量,代碼以下:

ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

inheritableThreadLocals至關於線程中另一種兜,這種兜有什麼特徵呢,當建立子線程的時候,子線程會將父線程這種類型兜的東西所有複製一份放到本身的inheritableThreadLocals兜中,使用InheritableThreadLocal對象能夠操做線程中的inheritableThreadLocals兜。

InheritableThreadLocal經常使用的方法也有3個:

//向Thread中某個口袋中放東西
public void set(T value);
//獲取這個口袋中目前放的東西
public T get();
//清空這個口袋中放的東西
public void remove()

實例:

@Slf4j
public class ThreadLocalTest {

    //private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    //
    private static InheritableThreadLocal<String> threadLocal = new InheritableThreadLocal<>();
    //自定義包含策略
    private static ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 5, 60,
            TimeUnit.SECONDS, new LinkedBlockingQueue<>(5),
            new DemoThreadFactory("訂單建立組"), new ThreadPoolExecutor.AbortPolicy());

    public static void main(String[] args) {
        //須要插入的數據
        List<String> dataList = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            dataList.add("數據" + i);
        }
        for (int i = 0; i < 5; i++) {
            String traceId = String.valueOf(i);
            executor.execute(() -> {
                threadLocal.set(traceId);
                try {
                    ThreadLocalTest.controller(dataList);
                } finally {
                    threadLocal.remove();
                }

            });

        }
    }

    //模擬controller
    public static void controller(List<String> dataList) {
        log.error("接受請求: " + "traceId:" + threadLocal.get());
        service(dataList);
    }

    //模擬service
    public static void service(List<String> dataList) {
        log.error("執行業務:" + "traceId:" + threadLocal.get());
        //dao(dataList);
        daoMuti(dataList);
    }

    //模擬dao
    public static void dao(List<String> dataList) {
        log.error("執行數據庫操做" + "traceId:" + threadLocal.get());
        //模擬插入數據
        for (String s : dataList) {
            log.error("插入數據" + s + "成功" + "traceId:" + threadLocal.get());
        }
    }
    //模擬dao--多線程
    public static void daoMuti(List<String> dataList) {
        CountDownLatch countDownLatch = new CountDownLatch(dataList.size());

        log.error("執行數據庫操做" + "traceId:" + threadLocal.get());
        String threadName = Thread.currentThread().getName();
        //模擬插入數據
        for (String s : dataList) {
            new Thread(() -> {
                try {
                    //模擬數據庫操做耗時100毫秒
                    TimeUnit.MILLISECONDS.sleep(100);
                    log.error("插入數據" + s + "成功" + threadName + ",traceId:" + threadLocal.get());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    countDownLatch.countDown();
                }
            }).start();
        }
        //等待上面的dataList處理完畢
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

輸出:

17:35:30.465 [From DemoThreadFactory's 訂單建立組-Worker-2] ERROR com.self.current.ThreadLocalTest - 接受請求: traceId:1
17:35:30.465 [From DemoThreadFactory's 訂單建立組-Worker-1] ERROR com.self.current.ThreadLocalTest - 接受請求: traceId:0
17:35:30.465 [From DemoThreadFactory's 訂單建立組-Worker-3] ERROR com.self.current.ThreadLocalTest - 接受請求: traceId:2
17:35:30.471 [From DemoThreadFactory's 訂單建立組-Worker-3] ERROR com.self.current.ThreadLocalTest - 執行業務:traceId:2
17:35:30.471 [From DemoThreadFactory's 訂單建立組-Worker-1] ERROR com.self.current.ThreadLocalTest - 執行業務:traceId:0
17:35:30.471 [From DemoThreadFactory's 訂單建立組-Worker-2] ERROR com.self.current.ThreadLocalTest - 執行業務:traceId:1
17:35:30.471 [From DemoThreadFactory's 訂單建立組-Worker-3] ERROR com.self.current.ThreadLocalTest - 執行數據庫操做traceId:2
17:35:30.471 [From DemoThreadFactory's 訂單建立組-Worker-1] ERROR com.self.current.ThreadLocalTest - 執行數據庫操做traceId:0
17:35:30.471 [From DemoThreadFactory's 訂單建立組-Worker-2] ERROR com.self.current.ThreadLocalTest - 執行數據庫操做traceId:1
17:35:30.574 [Thread-3] ERROR com.self.current.ThreadLocalTest - 插入數據數據2成功From DemoThreadFactory's 訂單建立組-Worker-3,traceId:2
17:35:30.574 [Thread-4] ERROR com.self.current.ThreadLocalTest - 插入數據數據0成功From DemoThreadFactory's 訂單建立組-Worker-2,traceId:1
17:35:30.574 [Thread-1] ERROR com.self.current.ThreadLocalTest - 插入數據數據0成功From DemoThreadFactory's 訂單建立組-Worker-3,traceId:2
17:35:30.574 [Thread-2] ERROR com.self.current.ThreadLocalTest - 插入數據數據1成功From DemoThreadFactory's 訂單建立組-Worker-3,traceId:2
17:35:30.574 [From DemoThreadFactory's 訂單建立組-Worker-3] ERROR com.self.current.ThreadLocalTest - 接受請求: traceId:3
17:35:30.574 [From DemoThreadFactory's 訂單建立組-Worker-3] ERROR com.self.current.ThreadLocalTest - 執行業務:traceId:3
17:35:30.574 [From DemoThreadFactory's 訂單建立組-Worker-3] ERROR com.self.current.ThreadLocalTest - 執行數據庫操做traceId:3
17:35:30.575 [Thread-9] ERROR com.self.current.ThreadLocalTest - 插入數據數據2成功From DemoThreadFactory's 訂單建立組-Worker-1,traceId:0
17:35:30.575 [Thread-8] ERROR com.self.current.ThreadLocalTest - 插入數據數據1成功From DemoThreadFactory's 訂單建立組-Worker-1,traceId:0
17:35:30.575 [Thread-7] ERROR com.self.current.ThreadLocalTest - 插入數據數據0成功From DemoThreadFactory's 訂單建立組-Worker-1,traceId:0
17:35:30.575 [From DemoThreadFactory's 訂單建立組-Worker-1] ERROR com.self.current.ThreadLocalTest - 接受請求: traceId:4
17:35:30.575 [From DemoThreadFactory's 訂單建立組-Worker-1] ERROR com.self.current.ThreadLocalTest - 執行業務:traceId:4
17:35:30.575 [From DemoThreadFactory's 訂單建立組-Worker-1] ERROR com.self.current.ThreadLocalTest - 執行數據庫操做traceId:4
17:35:30.575 [Thread-6] ERROR com.self.current.ThreadLocalTest - 插入數據數據2成功From DemoThreadFactory's 訂單建立組-Worker-2,traceId:1
17:35:30.575 [Thread-5] ERROR com.self.current.ThreadLocalTest - 插入數據數據1成功From DemoThreadFactory's 訂單建立組-Worker-2,traceId:1
17:35:30.682 [Thread-10] ERROR com.self.current.ThreadLocalTest - 插入數據數據0成功From DemoThreadFactory's 訂單建立組-Worker-3,traceId:3
17:35:30.682 [Thread-13] ERROR com.self.current.ThreadLocalTest - 插入數據數據0成功From DemoThreadFactory's 訂單建立組-Worker-1,traceId:4
17:35:30.682 [Thread-14] ERROR com.self.current.ThreadLocalTest - 插入數據數據1成功From DemoThreadFactory's 訂單建立組-Worker-1,traceId:4
17:35:30.682 [Thread-12] ERROR com.self.current.ThreadLocalTest - 插入數據數據2成功From DemoThreadFactory's 訂單建立組-Worker-3,traceId:3
17:35:30.683 [Thread-15] ERROR com.self.current.ThreadLocalTest - 插入數據數據2成功From DemoThreadFactory's 訂單建立組-Worker-1,traceId:4
17:35:30.683 [Thread-11] ERROR com.self.current.ThreadLocalTest - 插入數據數據1成功From DemoThreadFactory's 訂單建立組-Worker-3,traceId:3

JUC中的阻塞隊列

Queue接口

隊列是一種先進先出(FIFO)的數據結構,java中用Queue接口來表示隊列。

Queue接口中定義了6個方法:

public interface Queue<E> extends Collection<E> {
    boolean add(e);
    boolean offer(E e);
    E remove();
    E poll();
    E element();
    E peek();
}

每一個Queue方法都有兩種形式:

(1)若是操做失敗則拋出異常,

(2)若是操做失敗,則返回特殊值(null或false,具體取決於操做),接口的常規結構以下表所示。

操做類型 拋出異常 返回特殊值
插入 add(e) offer(e)
移除 remove() poll()
檢查 element() peek()

Queue從Collection繼承的add方法插入一個元素,除非它違反了隊列的容量限制,在這種狀況下它會拋出IllegalStateException;offer方法與add不一樣之處僅在於它經過返回false來表示插入元素失敗。

remove和poll方法都移除並返回隊列的頭部,確切地移除哪一個元素是由具體的實現來決定的,僅當隊列爲空時,remove和poll方法的行爲纔有所不一樣,在這些狀況下,remove拋出NoSuchElementException,而poll返回null。

element和peek方法返回隊列頭部的元素,但不移除,它們之間的差別與remove和poll的方式徹底相同,若是隊列爲空,則element拋出NoSuchElementException,而peek返回null。

隊列通常不要插入空元素。

BlockingQueue接口

BlockingQueue位於juc中,熟稱阻塞隊列, 阻塞隊列首先它是一個隊列,繼承Queue接口,是隊列就會遵循先進先出(FIFO)的原則,又由於它是阻塞的,故與普通的隊列有兩點區別:

  1. 當一個線程向隊列裏面添加數據時,若是隊列是滿的,那麼將阻塞該線程,暫停添加數據
  2. 當一個線程從隊列裏面取出數據時,若是隊列是空的,那麼將阻塞該線程,暫停取出數據

BlockingQueue相關方法:

操做類型 拋出異常 返回特殊值 一直阻塞 超時退出
插入 add(e) offer(e) put(e) offer(e,timeuout,unit)
移除 remove() poll() take() poll(timeout,unit)
檢查 element() peek() 不支持 不支持

重點,再來解釋一下,加深印象:

  1. 3個可能會有異常的方法,add、remove、element;這3個方法不會阻塞(是說隊列滿或者空的狀況下是否會阻塞);隊列滿的狀況下,add拋出異常;隊列爲空狀況下,remove、element拋出異常
  2. offer、poll、peek 也不會阻塞(是說隊列滿或者空的狀況下是否會阻塞);隊列滿的狀況下,offer返回false;隊列爲空的狀況下,pool、peek返回null
  3. 隊列滿的狀況下,調用put方法會致使當前線程阻塞
  4. 隊列爲空的狀況下,調用take方法會致使當前線程阻塞
  5. offer(e,timeuout,unit),超時以前,插入成功返回true,否者返回false
  6. poll(timeout,unit),超時以前,獲取到頭部元素並將其移除,返回true,否者返回false

BlockingQueue常見的實現類

ArrayBlockingQueue

基於數組的阻塞隊列實現,其內部維護一個定長的數組,用於存儲隊列元素。線程阻塞的實現是經過ReentrantLock來完成的,數據的插入與取出共用同一個鎖,所以ArrayBlockingQueue並不能實現生產、消費同時進行。並且在建立ArrayBlockingQueue時,咱們還能夠控制對象的內部鎖是否採用公平鎖,默認採用非公平鎖。

LinkedBlockingQueue

基於單向鏈表的阻塞隊列實現,在初始化LinkedBlockingQueue的時候能夠指定大小,也能夠不指定,默認相似一個無限大小的容量(Integer.MAX_VALUE),不指隊列容量大小也是會有風險的,一旦數據生產速度大於消費速度,系統內存將有可能被消耗殆盡,所以要謹慎操做。另外LinkedBlockingQueue中用於阻塞生產者、消費者的鎖是兩個(鎖分離),所以生產與消費是能夠同時進行的。

PriorityBlockingQueue

一個支持優先級排序的無界阻塞隊列,進入隊列的元素會按照優先級進行排序

SynchronousQueue

同步阻塞隊列,SynchronousQueue沒有容量,與其餘BlockingQueue不一樣,SynchronousQueue是一個不存儲元素的BlockingQueue,每個put操做必需要等待一個take操做,不然不能繼續添加元素,反之亦然

DelayQueue

DelayQueue是一個支持延時獲取元素的無界阻塞隊列,裏面的元素所有都是「可延期」的元素,列頭的元素是最早「到期」的元素,若是隊列裏面沒有元素到期,是不能從列頭獲取元素的,哪怕有元素也不行,也就是說只有在延遲期到時纔可以從隊列中取元素

LinkedTransferQueue

LinkedTransferQueue是基於鏈表的FIFO無界阻塞隊列,它出如今JDK7中,Doug Lea 大神說LinkedTransferQueue是一個聰明的隊列,它是ConcurrentLinkedQueue、SynchronousQueue(公平模式下)、無界的LinkedBlockingQueues等的超集,LinkedTransferQueue包含了ConcurrentLinkedQueue、SynchronousQueue、LinkedBlockingQueues三種隊列的功能

ArrayBlockingQueue

有界阻塞隊列,內部使用數組存儲元素,有2個經常使用構造方法:

//capacity表示容量大小,默認內部採用非公平鎖
public ArrayBlockingQueue(int capacity)
//capacity:容量大小,fair:內部是不是使用公平鎖
public ArrayBlockingQueue(int capacity, boolean fair)

注意:ArrayBlockingQueue若是隊列容量設置的過小,消費者發送的太快,消費者消費的太慢的狀況下,會致使隊列空間滿,調用put方法會致使發送者線程阻塞,因此注意設置合理的大小,協調好消費者的速度。

LinkedBlockingQueue

內部使用單向鏈表實現的阻塞隊列,3個構造方法:

//默認構造方法,容量大小爲Integer.MAX_VALUE
public LinkedBlockingQueue();
//建立指定容量大小的LinkedBlockingQueue
public LinkedBlockingQueue(int capacity);
//容量爲Integer.MAX_VALUE,並將傳入的集合丟入隊列中
public LinkedBlockingQueue(Collection<? extends E> c);

LinkedBlockingQueue的用法和ArrayBlockingQueue相似,建議使用的時候指定容量,若是不指定容量,插入的太快,移除的太慢,可能會產生OOM。

PriorityBlockingQueue

無界的優先級阻塞隊列,內部使用數組存儲數據,達到容量時,會自動進行擴容,放入的元素會按照優先級進行排序,4個構造方法:

//默認構造方法,默認初始化容量是11
public PriorityBlockingQueue();
//指定隊列的初始化容量
public PriorityBlockingQueue(int initialCapacity);
//指定隊列的初始化容量和放入元素的比較器
public PriorityBlockingQueue(int initialCapacity,Comparator<? super E> comparator);
//傳入集合放入來初始化隊列,傳入的集合能夠實現SortedSet接口或者PriorityQueue接口進行排序,若是沒有實現這2個接口,按正常順序放入隊列
public PriorityBlockingQueue(Collection<? extends E> c);

優先級隊列放入元素的時候,會進行排序,因此咱們須要指定排序規則,有2種方式:

  1. 建立PriorityBlockingQueue指定比較器Comparator
  2. 放入的元素須要實現Comparable接口

上面2種方式必須選一個,若是2個都有,則走第一個規則排序。

示例:

public class PriorityBlockingQueueTest {

   private static PriorityBlockingQueue<Msg> priorityBlockingQueue = new PriorityBlockingQueue<>();

    private static class Msg implements Comparable<Msg>{
       private String msg;
       private int priority;

        public Msg(String msg, int priority) {
            this.msg = msg;
            this.priority = priority;
        }

        @Override
        public String toString() {
            return "Msg{" +
                    "priority=" + priority +
                    ", msg='" + msg + '\'' +
                    '}';
        }

        @Override
        public int compareTo(Msg o) {
            //return this.priority-o.priority;
            return o.priority-this.priority;
            //return Integer.compare(this.priority,o.priority);
        }
    }
    public static void putMsg(Msg msg) throws InterruptedException {
        priorityBlockingQueue.put(msg);
        System.out.println("已推送"+msg);
    }

    static {
        new Thread(() -> {
            while (true) {
                Msg msg;
                try {
                    long starTime = System.currentTimeMillis();
                    //獲取一條推送消息,此方法會進行阻塞,直到返回結果
                    msg = priorityBlockingQueue.take();
                    long endTime = System.currentTimeMillis();
                    //模擬推送耗時
                    TimeUnit.MILLISECONDS.sleep(500);
                    System.out.println(String.format("[%s,%s,take耗時:%s],%s,發送消息:%s", starTime, endTime, (endTime - starTime), Thread.currentThread().getName(), msg));
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 5; i++) {
            PriorityBlockingQueueTest.putMsg(new Msg("消息" + i,i));
        }
    }
}

輸出:

已推送Msg{priority=0, msg='消息0'}
已推送Msg{priority=1, msg='消息1'}
已推送Msg{priority=2, msg='消息2'}
已推送Msg{priority=3, msg='消息3'}
已推送Msg{priority=4, msg='消息4'}
[1599459824306,1599459824307,take耗時:1],Thread-0,發送消息:Msg{priority=0, msg='消息0'}
[1599459824829,1599459824829,take耗時:0],Thread-0,發送消息:Msg{priority=4, msg='消息4'}
[1599459825330,1599459825330,take耗時:0],Thread-0,發送消息:Msg{priority=3, msg='消息3'}
[1599459825831,1599459825831,take耗時:0],Thread-0,發送消息:Msg{priority=2, msg='消息2'}
[1599459826331,1599459826331,take耗時:0],Thread-0,發送消息:Msg{priority=1, msg='消息1'}

SynchronousQueue

同步阻塞隊列,SynchronousQueue沒有容量,與其餘BlockingQueue不一樣,SynchronousQueue是一個不存儲元素的BlockingQueue,每個put操做必需要等待一個take操做,不然不能繼續添加元素,反之亦然。SynchronousQueue 在現實中用的很少,線程池中有用到過,Executors.newCachedThreadPool()實現中用到了這個隊列,當有任務丟入線程池的時候,若是已建立的工做線程都在忙於處理任務,則會新建一個線程來處理丟入隊列的任務。

調用queue.put方法向隊列中丟入一條數據,調用的時候產生了阻塞,從輸出結果中能夠看出,直到take方法被調用時,put方法才從阻塞狀態恢復正常。

DelayQueue

DelayQueue是一個支持延時獲取元素的無界阻塞隊列,裏面的元素所有都是「可延期」的元素,列頭的元素是最早「到期」的元素,若是隊列裏面沒有元素到期,是不能從列頭獲取元素的,哪怕有元素也不行,也就是說只有在延遲期到時纔可以從隊列中取元素。

  1. DelayQueue是一個內部依靠AQS隊列同步器所實現的無界延遲阻塞隊列。
  2. 延遲對象須要覆蓋 getDelay()與compareTo()方法,而且要注意 getDelay()的時間單位的統一,compareTo()根據業務邏輯進行合理的比較邏輯重寫。
  3. DelayQueue中內聚的重入鎖是非公平的。
  4. DelayQueue是實現定時任務的關鍵,ScheduledThreadPoolExecutor中就用到了DelayQueue。

示例:

public class DelayQueueTest {
    //推送信息封裝
    static class Msg implements Delayed {
        //優先級,越小優先級越高
        private int priority;
        //推送的信息
        private String msg;
        //定時發送時間,毫秒格式
        private long sendTimeMs;

        public Msg(int priority, String msg, long sendTimeMs) {
            this.priority = priority;
            this.msg = msg;
            this.sendTimeMs = sendTimeMs;
        }

        @Override
        public String toString() {
            return "Msg{" +
                    "priority=" + priority +
                    ", msg='" + msg + '\'' +
                    ", sendTimeMs=" + sendTimeMs +
                    '}';
        }

        //@Override
        //public long getDelay(TimeUnit unit) {
        //    return unit.convert(this.sendTimeMs - Calendar.getInstance().getTimeInMillis(), TimeUnit.MILLISECONDS);
        //}

        /**
         * 須要實現的接口,得到延遲時間   用過時時間-當前時間
         * @param unit
         * @return
         */
        @Override
        public long getDelay(TimeUnit unit) {
            return unit.convert(this.sendTimeMs - System.currentTimeMillis() , TimeUnit.MILLISECONDS);
        }

        /**
         * 用於延遲隊列內部比較排序   當前時間的延遲時間 - 比較對象的延遲時間
         * @param o
         * @return
         */
        @Override
        public int compareTo(Delayed o) {
            return (int) (this.getDelay(TimeUnit.MILLISECONDS) -o.getDelay(TimeUnit.MILLISECONDS));
        }
    }

    //推送隊列
    static DelayQueue<Msg> pushQueue = new DelayQueue<Msg>();

    static {
        //啓動一個線程作真實推送
        new Thread(() -> {
            while (true) {
                Msg msg;
                try {
                    //獲取一條推送消息,此方法會進行阻塞,直到返回結果
                    msg = pushQueue.take();
                    //此處能夠作真實推送
                    long endTime = System.currentTimeMillis();
                    System.out.println(String.format("定時發送時間:%s,實際發送時間:%s,發送消息:%s", msg.sendTimeMs, endTime, msg));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }

    //推送消息,須要發送推送消息的調用該方法,會將推送信息先加入推送隊列
    public static void pushMsg(int priority, String msg, long sendTimeMs) throws InterruptedException {
        pushQueue.put(new Msg(priority, msg, sendTimeMs));
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 5; i >= 1; i--) {
            String msg = "一塊兒來學java高併發,第" + i + "天";
            DelayQueueTest.pushMsg(i, msg, Calendar.getInstance().getTimeInMillis() + i * 2000);
        }
    }
}

輸出:

定時發送時間:1599462287589,實際發送時間:1599462287590,發送消息:Msg{priority=1, msg='一塊兒來學java高併發,第1天', sendTimeMs=1599462287589}
定時發送時間:1599462289589,實際發送時間:1599462289589,發送消息:Msg{priority=2, msg='一塊兒來學java高併發,第2天', sendTimeMs=1599462289589}
定時發送時間:1599462291589,實際發送時間:1599462291590,發送消息:Msg{priority=3, msg='一塊兒來學java高併發,第3天', sendTimeMs=1599462291589}
定時發送時間:1599462293588,實際發送時間:1599462293589,發送消息:Msg{priority=4, msg='一塊兒來學java高併發,第4天', sendTimeMs=1599462293588}
定時發送時間:1599462295571,實際發送時間:1599462295571,發送消息:Msg{priority=5, msg='一塊兒來學java高併發,第5天', sendTimeMs=1599462295571}

LinkedTransferQueue

LinkedTransferQueue是一個由鏈表結構組成的無界阻塞TransferQueue隊列。相對於其餘阻塞隊列,LinkedTransferQueue多了tryTransfer和transfer方法。

LinkedTransferQueue類繼承自AbstractQueue抽象類,而且實現了TransferQueue接口:

public interface TransferQueue<E> extends BlockingQueue<E> {
    // 若是存在一個消費者已經等待接收它,則當即傳送指定的元素,不然返回false,而且不進入隊列。
    boolean tryTransfer(E e);
    // 若是存在一個消費者已經等待接收它,則當即傳送指定的元素,不然等待直到元素被消費者接收。
    void transfer(E e) throws InterruptedException;
    // 在上述方法的基礎上設置超時時間
    boolean tryTransfer(E e, long timeout, TimeUnit unit)
        throws InterruptedException;
    // 若是至少有一位消費者在等待,則返回true
    boolean hasWaitingConsumer();
    // 獲取全部等待獲取元素的消費線程數量
    int getWaitingConsumerCount();
}

再看一下上面的這些方法,transfer(E e)方法和SynchronousQueue的put方法相似,都須要等待消費者取走元素,否者一直等待。其餘方法和ArrayBlockingQueue、LinkedBlockingQueue中的方法相似。

總結

  1. 重點須要瞭解BlockingQueue中的全部方法,以及他們的區別
  2. 重點掌握ArrayBlockingQueue、LinkedBlockingQueue、PriorityBlockingQueue、DelayQueue的使用場景
  3. 須要處理的任務有優先級的,使用PriorityBlockingQueue
  4. 處理的任務須要延時處理的,使用DelayQueue

疑問:

Q:有界阻塞隊列和無界阻塞隊列的區別?是否是以是否認義隊列大小來做爲區分,沒有定義的就是無界的,有定義的就算是容量(Integer.MAX_VALUE)也是有界的?

JUC中常見的集合

JUC集合框架圖

圖能夠看到,JUC的集合框架也是從Map、List、Set、Queue、Collection等超級接口中繼承而來的。因此,大概能夠知道JUC下的集合包含了一一些基本操做,而且變得線程安全。

Map

ConcurrentHashMap

功能和HashMap基本一致,內部使用紅黑樹實現的。

特性:

  1. 迭代結果和存入順序不一致
  2. key和value都不能爲空
  3. 線程安全的

ConcurrentSkipListMap

內部使用跳錶實現的,放入的元素會進行排序,排序算法支持2種方式來指定:

  1. 經過構造方法傳入一個Comparator
  2. 放入的元素實現Comparable接口

上面2種方式必選一個,若是2種都有,走規則1。

特性:

  1. 迭代結果和存入順序不一致
  2. 放入的元素會排序
  3. key和value都不能爲空
  4. 線程安全的

List

CopyOnWriteArrayList

實現List的接口的,通常咱們使用ArrayList、LinkedList、Vector,其中只有Vector是線程安全的,能夠使用Collections靜態類的synchronizedList方法對ArrayList、LinkedList包裝爲線程安全的List,不過這些方式在保證線程安全的狀況下性能都不高。

CopyOnWriteArrayList是線程安全的List,內部使用數組存儲數據,集合中多線程並行操做通常存在4種狀況:讀讀、讀寫、寫寫、寫讀,這個只有在寫寫操做過程當中會致使其餘線程阻塞,其餘3種狀況均不會阻塞,因此讀取的效率很是高。

能夠看一下這個類的名稱:CopyOnWrite,意思是在寫入操做的時候,進行一次自我複製,換句話說,當這個List須要修改時,並不修改原有內容(這對於保證當前在讀線程的數據一致性很是重要),而是在原有存放數據的數組上產生一個副本,在副本上修改數據,修改完畢以後,用副本替換原來的數組,這樣也保證了寫操做不會影響讀。

特性:

  1. 迭代結果和存入順序一致
  2. 元素不重複
  3. 元素能夠爲空
  4. 線程安全的
  5. 讀讀、讀寫、寫讀3種狀況不會阻塞;寫寫會阻塞
  6. 無界的

Set

ConcurrentSkipListSet

有序的Set,內部基於ConcurrentSkipListMap實現的,放入的元素會進行排序,排序算法支持2種方式來指定:

  1. 經過構造方法傳入一個Comparator
  2. 放入的元素實現Comparable接口

上面2種方式須要實現一個,若是2種都有,走規則1

特性:

  1. 迭代結果和存入順序不一致
  2. 放入的元素會排序
  3. 元素不重複
  4. 元素不能爲空
  5. 線程安全的
  6. 無界的

CopyOnWriteArraySet

內部使用CopyOnWriteArrayList實現的,將全部的操做都會轉發給CopyOnWriteArrayList。

特性:

  1. 迭代結果和存入順序不一致
  2. 元素不重複
  3. 元素能夠爲空
  4. 線程安全的
  5. 讀讀、讀寫、寫讀 不會阻塞;寫寫會阻塞
  6. 無界的

Queue

Queue接口中的方法,咱們再回顧一下:

操做類型 拋出異常 返回特殊值
插入 add(e) offer(e)
移除 remove() poll()
檢查 element() peek()

3種操做,每種操做有2個方法,不一樣點是隊列爲空或者滿載時,調用方法是拋出異常仍是返回特殊值,你們按照表格中的多看幾遍,加深記憶。

ConcurrentLinkedQueue

高效併發隊列,內部使用鏈表實現的。

特性:

  1. 線程安全的
  2. 迭代結果和存入順序一致
  3. 元素能夠重複
  4. 元素不能爲空
  5. 線程安全的
  6. 無界隊列

Deque

先介紹一下Deque接口,雙向隊列(Deque)是Queue的一個子接口,雙向隊列是指該隊列兩端的元素既能入隊(offer)也能出隊(poll),若是將Deque限制爲只能從一端入隊和出隊,則可實現棧的數據結構。對於棧而言,有入棧(push)和出棧(pop),遵循先進後出原則。

一個線性 collection,支持在兩端插入和移除元素。名稱 deque 是「double ended queue(雙端隊列)」的縮寫,一般讀爲「deck」。大多數 Deque 實現對於它們可以包含的元素數沒有固定限制,但此接口既支持有容量限制的雙端隊列,也支持沒有固定大小限制的雙端隊列。

此接口定義在雙端隊列兩端訪問元素的方法。提供插入、移除和檢查元素的方法。每種方法都存在兩種形式:一種形式在操做失敗時拋出異常,另外一種形式返回一個特殊值(null 或 false,具體取決於操做)。插入操做的後一種形式是專爲使用有容量限制的 Deque 實現設計的;在大多數實現中,插入操做不能失敗。

下表總結了上述 12 種方法:

此接口擴展了 Queue接口。在將雙端隊列用做隊列時,將獲得 FIFO(先進先出)行爲。將元素添加到雙端隊列的末尾,從雙端隊列的開頭移除元素。從 Queue 接口繼承的方法徹底等效於 Deque 方法,以下表所示:

Queue 方法 等效 Deque 方法
add(e) addLast(e)
offer(e) offerLast(e)
remove() removeFirst()
poll() pollFirst()
element() getFirst()
peek() peekFirst()

ConcurrentLinkedDeque

實現了Deque接口,內部使用鏈表實現的高效的併發雙端隊列。

特性:

  1. 線程安全的
  2. 迭代結果和存入順序一致
  3. 元素能夠重複
  4. 元素不能爲空
  5. 線程安全的
  6. 無界隊列

BlockingQueue

關於阻塞隊列,上一篇有詳細介紹。

疑問:

Q:跳錶是什麼?

接口性能提高實戰篇

需求:電商app的商品詳情頁,須要給他們提供一個接口獲取商品相關信息:

  1. 商品基本信息(名稱、價格、庫存、會員價格等)
  2. 商品圖片列表
  3. 商品描述信息(描述信息通常是由富文本編輯的大文本信息)

普通接口實現僞代碼以下:

public Map<String,Object> detail(long goodsId){
    //建立一個map
    //step1:查詢商品基本信息,放入map
    map.put("goodsModel",(select * from t_goods where id = #gooldsId#));
    //step2:查詢商品圖片列表,返回一個集合放入map
    map.put("goodsImgsModelList",(select * from t_goods_imgs where goods_id = #gooldsId#));
    //step3:查詢商品描述信息,放入map
    map.put("goodsExtModel",(select * from t_goods_ext where goods_id = #gooldsId#));
    return map;
}

上面這種寫法應該很常見,代碼很簡單,假設上面每一個步驟耗時200ms,此接口總共耗時>=600毫秒

整個過程是按順序執行的,實際上3個查詢之間是沒有任何依賴關係,因此說3個查詢能夠同時執行,那咱們對這3個步驟採用多線程並行執行實現以下:

示例:

public class GetProductDetailTest {

    //自定義包含策略
    private ThreadPoolExecutor executor = new ThreadPoolExecutor(20, 20, 60,
            TimeUnit.SECONDS, new LinkedBlockingQueue<>(5),
            new DemoThreadFactory("訂單建立組"), new ThreadPoolExecutor.AbortPolicy());

    /**
     * 獲取商品基本信息
     *
     * @param goodsId 商品id
     * @return 商品基本信息
     * @throws InterruptedException
     */
    public String goodsDetailModel(long goodsId) throws InterruptedException {
        //模擬耗時,休眠200ms
        TimeUnit.MILLISECONDS.sleep(200);
        return "商品id:" + goodsId + ",商品基本信息....";
    }

    /**
     * 獲取商品圖片列表
     *
     * @param goodsId 商品id
     * @return 商品圖片列表
     * @throws InterruptedException
     */
    public List<String> goodsImgsModelList(long goodsId) throws InterruptedException {
        //模擬耗時,休眠200ms
        TimeUnit.MILLISECONDS.sleep(200);
        return Arrays.asList("圖1", "圖2", "圖3");
    }

    /**
     * 獲取商品描述信息
     *
     * @param goodsId 商品id
     * @return 商品描述信息
     * @throws InterruptedException
     */
    public String goodsExtModel(long goodsId) throws InterruptedException {
        //模擬耗時,休眠200ms
        TimeUnit.MILLISECONDS.sleep(200);
        return "商品id:" + goodsId + ",商品描述信息......";
    }

    public Map<String,Object> getGoodsDetail(long goodsId) throws ExecutionException, InterruptedException {
        Map<String, Object> result = new HashMap<>();
        Future<String> gooldsDetailModelFuture  = executor.submit(() -> goodsDetailModel(goodsId));
        Future<List<String>> goodsImgsModelFuture = executor.submit(() -> goodsImgsModelList(goodsId));
        //異步獲取商品描述信息
        Future<String> goodsExtModelFuture = executor.submit(() -> goodsExtModel(goodsId));
        result.put("gooldsDetailModel", gooldsDetailModelFuture.get());
        result.put("goodsImgsModelList", goodsImgsModelFuture.get());
        result.put("goodsExtModel", goodsExtModelFuture.get());
        return result;
    }


    public static void main(String[] args) throws ExecutionException, InterruptedException {
        GetProductDetailTest detailTest = new GetProductDetailTest();
        long starTime = System.currentTimeMillis();
        Map<String, Object> map = detailTest.getGoodsDetail(1L);
        System.out.println(map);
        System.out.println("耗時(ms):" + (System.currentTimeMillis() - starTime));
    }
}

輸出:

{goodsImgsModelList=[圖1, 圖2, 圖3], gooldsDetailModel=商品id:1,商品基本信息...., goodsExtModel=商品id:1,商品描述信息......}
耗時(ms):255

能夠看出耗時200毫秒左右,性能提高了2倍,假如這個接口中還存在其餘無依賴的操做,性能提高將更加顯著,上面使用了線程池並行去執行3次查詢的任務,最後經過Future獲取異步執行結果。

整個優化過程:

  1. 先列出無依賴的一些操做
  2. 將這些操做改成並行的方式

總結

  1. 對於無依賴的操做盡可能採用並行方式去執行,能夠很好的提高接口的性能

解決微服務日誌的痛點

日誌有什麼用?

  1. 系統出現故障的時候,能夠經過日誌信息快速定位問題,修復bug,恢復業務
  2. 提取有用數據,作數據分析使用

本文主要討論經過日誌來快速定位並解決問題。

日誌存在的痛點

先介紹一下多數公司採用的方式:目前比較流行的是採用springcloud(或者dubbo)作微服務,按照業務拆分爲多個獨立的服務,服務採用集羣的方式部署在不一樣的機器上,當一個請求過來的時候,可能會調用到不少服務進行處理,springcloud通常採用logback(或者log4j)輸出日誌到文件中。當系統出問題的時候,按照系統故障的嚴重程度,嚴重的會回退版本,而後排查bug,輕的,找運維去線上拉日誌,而後排查問題。

這個過程當中存在一些問題:

  1. 日誌文件太大太多,不方便查找
  2. 日誌分散在不一樣的機器上,也不方便查找
  3. 一個請求可能會調用多個服務,完整的日誌難以追蹤(沒有完整的鏈路日誌)
  4. 系統出現了問題,只能等到用戶發現了,本身才知道(沒有報錯預警)

本文要解決上面的幾個痛點,構建咱們的日誌系統,達到如下要求:

  1. 方便追蹤一個請求完整的日誌
  2. 方便快速檢索日誌
  3. 系統出現問題自動報警,通知相關人員

構建日誌系統

方便追蹤一個請求完整的日誌

當一個請求過來的時候,可能會調用多個服務,多個服務內部可能又會產生子線程處理業務,因此這裏面有兩個問題須要解決:

  1. 多個服務之間日誌的追蹤
  2. 服務內部子線程和主線程日誌的追蹤,這個地方舉個例子,好比一個請求內部須要給10000人發送推送,內部開啓10個線程並行處理,處理完畢以後響應操做者,這裏面有父子線程,咱們要可以找到這個裏面全部的日誌

須要追蹤一個請求完整日誌,咱們須要給每一個請求設置一個全局惟一編號,能夠使用UUID或者其餘方式也行。

多個服務之間日誌追蹤的問題:當一個請求過來的時候,在入口處生成一個trace_id,而後放在ThreadLocal中,若是內部設計到多個服務之間相互調用,調用其餘服務的時,將trace_id順便攜帶過去。

父子線程日誌追蹤的問題:能夠採用InheritableThreadLocal來存放trace_id,這樣能夠在線程中獲取到父線程中的trace_id。

因此此處咱們須要使用InheritableThreadLocal來存儲trace_id。

使用了線程池處理請求的,因爲線程池中的線程採用的是複用的方式,因此須要對執行的任務Runable作一些改造 包裝。

public class TraceRunnable implements Runnable {
    private String tranceId;
    private Runnable target;

    public TraceRunnable(Runnable target) {
        this.tranceId = TraceUtil.get();
        this.target = target;
    }

    @Override
    public void run() {
        try {
            TraceUtil.set(this.tranceId);
            MDC.put(TraceUtil.MDC_TRACE_ID, TraceUtil.get());
            this.target.run();
        } finally {
            MDC.remove(TraceUtil.MDC_TRACE_ID);
            TraceUtil.remove();
        }
    }

    public static Runnable trace(Runnable target) {
        return new TraceRunnable(target);
    }
}

須要用線程池執行的任務使用TraceRunnable封裝一下就能夠了。

TraceUtil代碼:

public class TraceUtil {

    public static final String REQUEST_HEADER_TRACE_ID = "com.ms.header.trace.id";
    public static final String MDC_TRACE_ID = "trace_id";

    private static InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();

    /**
     * 獲取traceid
     *
     * @return
     */
    public static String get() {
        String traceId = inheritableThreadLocal.get();
        if (traceId == null) {
            traceId = IDUtil.getId();
            inheritableThreadLocal.set(traceId);
        }
        return traceId;
    }

    public static void set(String trace_id) {
        inheritableThreadLocal.set(trace_id);
    }

    public static void remove() {
        inheritableThreadLocal.remove();
    }

}

日誌輸出中攜帶上trace_id,這樣最終咱們就能夠經過trace_id找到一個請求的完整日誌了。

方便快速檢索日誌

日誌分散在不一樣的機器上,若是要快速檢索,須要將全部服務產生的日誌聚集到一個地方。

關於檢索日誌的,列一下需求:

  1. 咱們將收集日誌發送到消息中間件中(能夠是kafka、rocketmq),消息中間件這塊不介紹,選擇玩的比較溜的就能夠了
  2. 系統產生日誌儘可能不要影響接口的效率
  3. 帶寬有限的狀況下,發送日誌也儘可能不要去影響業務
  4. 日誌儘可能低延次,產生的日誌,儘可能在生成以後1分鐘後能夠檢索到
  5. 檢索日誌功能要可以快速響應

關於上面幾點,咱們須要作的:日誌發送的地方進行改造,引入消息中間件,將日誌異步發送到消息中間件中,查詢的地方採用elasticsearch,日誌系統須要訂閱消息中間件中的日誌,而後丟給elasticsearch建索引,方便快速檢索,我們來一點點的介紹。

日誌發送端的改造

日誌是由業務系統產生的,一個請求過來的時候會產生不少日誌,日誌產生時,咱們儘可能減小日誌輸出對業務耗時的影響,咱們的過程以下:

  1. 業務系統內部引用一個線程池來異步處理日誌,線程池內部能夠使用一個容量稍微大一點的阻塞隊列
  2. 業務系統將日誌丟給線程池進行處理
  3. 線程池中將須要處理的日誌先壓縮一下,而後發送至mq

線程池的使用能夠參考:JAVA線程池,這一篇就夠了

引入mq存儲日誌

業務系統將日誌先發送到mq中,後面由其餘消費者訂閱進行消費。日誌量比較大的,對mq的要求也比較高,能夠選擇kafka,業務量小的,也能夠選取activemq。

使用elasticsearch來檢索日誌

elasticsearch(如下簡稱es)是一個全文檢索工具,具體詳情能夠參考其官網相關文檔。使用它來檢索數據效率很是高。日誌系統中須要咱們開發一個消費端來拉取mq中的消息,將其存儲到es中方便快速檢索,關於這塊有幾點說一下:

  1. 建議按天在es中創建數據庫,日質量很是大的,也能夠按小時創建數據庫。查詢的時候,時間就是必選條件了,這樣能夠快速讓es定位到日誌庫進行檢索,提高檢索效率
  2. 日誌常見的須要收集的信息:trace_id、時間、日誌級別、類、方法、url、調用的接口開始時間、調用接口的結束時間、接口耗時、接口狀態碼、異常信息、日誌信息等等,能夠按照這些在es中創建索引,方便檢索。

日誌監控報警——可自定義配置報警

日誌監控報警是很是重要的,這個必需要有,日誌系統中須要開發監控報警功能,這塊咱們能夠作成經過頁面配置的方式,支持報警規則的配置,如日誌中產生了某些異常、接口響應時間大於多少、接口返回狀態碼404等異常信息的時候可以報警,具體的報警能夠是語音電話、短信通知、釘釘機器人報警等等,這些也作成能夠配置的。

日誌監控模塊從mq中拉取日誌,而後去匹配咱們啓用的一些規則進行報警。

日誌處理結構圖以下:

高併發中常見的限流方式

常見的限流的場景

  1. 秒殺活動,數量有限,訪問量巨大,爲了防止系統宕機,須要作限流處理
  2. 國慶期間,通常的旅遊景點人口太多,採用排隊方式作限流處理
  3. 醫院看病經過發放排隊號的方式來作限流處理。

常見的限流算法

  1. 經過控制最大併發數來進行限流
  2. 使用漏桶算法來進行限流
  3. 使用令牌桶算法來進行限流

經過控制最大併發數來進行限流

以秒殺業務爲例,10個iphone,100萬人搶購,100萬人同時發起請求,最終可以搶到的人也就是前面幾我的,後面的基本上都沒有但願了,那麼咱們能夠經過控制併發數來實現,好比並發數控制在10個,其餘超過併發數的請求所有拒絕,提示:秒殺失敗,請稍後重試。

單機中的JUC中提供了這樣的工具類:Semaphore:若是是集羣,則能夠用redis或者zk代替Semaphore

示例:

public class MaxAccessLimiter {

    private static Semaphore limiter = new Semaphore(5);
    //自定義包含策略
    private static ThreadPoolExecutor executor = new ThreadPoolExecutor(20, 20, 60,
            TimeUnit.SECONDS, new SynchronousQueue(),
            new DemoThreadFactory("訂單建立組"), new ThreadPoolExecutor.AbortPolicy());

    public static void main(String[] args) {
        for (int i = 0; i < 20; i++) {
            executor.submit(() -> {
                boolean flag = false;
                try {
                    flag = limiter.tryAcquire(100, TimeUnit.MICROSECONDS);
                    if (flag) {
                        //休眠2秒,模擬下單操做
                        System.out.println(Thread.currentThread() + ",嘗試下單中。。。。。");
                        TimeUnit.SECONDS.sleep(2);
                    } else {
                        System.out.println(Thread.currentThread() + ",秒殺失敗,請稍微重試!");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    if (flag) {
                        limiter.release();
                    }
                }
            });
        }
           executor.shutdown();
    }
}

輸出:

Thread[From DemoThreadFactory's 訂單建立組-Worker-1,5,main],嘗試下單中。。。。。
Thread[From DemoThreadFactory's 訂單建立組-Worker-2,5,main],嘗試下單中。。。。。
Thread[From DemoThreadFactory's 訂單建立組-Worker-3,5,main],嘗試下單中。。。。。
Thread[From DemoThreadFactory's 訂單建立組-Worker-4,5,main],嘗試下單中。。。。。
Thread[From DemoThreadFactory's 訂單建立組-Worker-5,5,main],嘗試下單中。。。。。
Thread[From DemoThreadFactory's 訂單建立組-Worker-9,5,main],秒殺失敗,請稍微重試!
Thread[From DemoThreadFactory's 訂單建立組-Worker-14,5,main],秒殺失敗,請稍微重試!
Thread[From DemoThreadFactory's 訂單建立組-Worker-16,5,main],秒殺失敗,請稍微重試!
Thread[From DemoThreadFactory's 訂單建立組-Worker-17,5,main],秒殺失敗,請稍微重試!
Thread[From DemoThreadFactory's 訂單建立組-Worker-18,5,main],秒殺失敗,請稍微重試!
Thread[From DemoThreadFactory's 訂單建立組-Worker-20,5,main],秒殺失敗,請稍微重試!
Thread[From DemoThreadFactory's 訂單建立組-Worker-12,5,main],秒殺失敗,請稍微重試!
Thread[From DemoThreadFactory's 訂單建立組-Worker-11,5,main],秒殺失敗,請稍微重試!
Thread[From DemoThreadFactory's 訂單建立組-Worker-7,5,main],秒殺失敗,請稍微重試!
Thread[From DemoThreadFactory's 訂單建立組-Worker-8,5,main],秒殺失敗,請稍微重試!
Thread[From DemoThreadFactory's 訂單建立組-Worker-6,5,main],秒殺失敗,請稍微重試!
Thread[From DemoThreadFactory's 訂單建立組-Worker-10,5,main],秒殺失敗,請稍微重試!
Thread[From DemoThreadFactory's 訂單建立組-Worker-19,5,main],秒殺失敗,請稍微重試!
Thread[From DemoThreadFactory's 訂單建立組-Worker-15,5,main],秒殺失敗,請稍微重試!
Thread[From DemoThreadFactory's 訂單建立組-Worker-13,5,main],秒殺失敗,請稍微重試!

使用漏桶算法來進行限流

國慶期間比較火爆的景點,人流量巨大,通常入口處會有限流的彎道,讓遊客進去進行排隊,排在前面的人,每隔一段時間會放一撥進入景區。排隊人數超過了指定的限制,後面再來的人會被告知今天已經遊客量已經達到峯值,會被拒絕排隊,讓其明天或者之後再來,這種玩法採用漏桶限流的方式。

漏桶算法思路很簡單,水(請求)先進入到漏桶裏,漏桶以必定的速度出水,當水流入速度過大會直接溢出,能夠看出漏桶算法能強行限制數據的傳輸速率。

漏桶算法示意圖:

示例:代碼中BucketLimit.build(10, 60, TimeUnit.MINUTES);建立了一個容量爲10,流水爲60/分鐘的漏桶。

public class BucketLimitTest {

    public static class BucketLimit {
        static AtomicInteger threadNum = new AtomicInteger(1);
        //容量
        private int capcity;
        //流速
        private int flowRate;
        //流速時間單位
        private TimeUnit flowRateUnit;
        private BlockingQueue<Node> queue;
        //漏桶流出的任務時間間隔(納秒)
        private long flowRateNanosTime;

        public BucketLimit(int capcity, int flowRate, TimeUnit flowRateUnit) {
            this.capcity = capcity;
            this.flowRate = flowRate;
            this.flowRateUnit = flowRateUnit;
            this.bucketThreadWork();
        }

        //漏桶線程
        public void bucketThreadWork() {
            this.queue = new ArrayBlockingQueue<Node>(capcity);
            //漏桶流出的任務時間間隔(納秒)
            this.flowRateNanosTime = flowRateUnit.toNanos(1) / flowRate;
            System.out.println(TimeUnit.NANOSECONDS.toSeconds(this.flowRateNanosTime));
            Thread thread = new Thread(this::bucketWork);
            thread.setName("漏桶線程-" + threadNum.getAndIncrement());
            thread.start();
        }

        //漏桶線程開始工做
        public void bucketWork() {
            while (true) {
                Node node = this.queue.poll();
                if (Objects.nonNull(node)) {
                    //喚醒任務線程
                    LockSupport.unpark(node.thread);
                }
                //阻塞當前線程,最長不超過nanos納秒
                //休眠flowRateNanosTime
                LockSupport.parkNanos(this.flowRateNanosTime);
            }
        }

        //返回一個漏桶
        public static BucketLimit build(int capcity, int flowRate, TimeUnit flowRateUnit) {
            if (capcity < 0 || flowRate < 0) {
                throw new IllegalArgumentException("capcity、flowRate必須大於0!");
            }
            return new BucketLimit(capcity, flowRate, flowRateUnit);
        }

        //當前線程加入漏桶,返回false,表示漏桶已滿;true:表示被漏桶限流成功,能夠繼續處理任務
        public boolean acquire() {
            Thread thread = Thread.currentThread();
            Node node = new Node(thread);
            if (this.queue.offer(node)) {
                LockSupport.park();
                return true;
            }
            return false;
        }

        //漏桶中存放的元素
        class Node {
            private Thread thread;

            public Node(Thread thread) {
                this.thread = thread;
            }
        }
    }
    //自定義包含策略
    private static ThreadPoolExecutor executor = new ThreadPoolExecutor(15, 15, 60,
            TimeUnit.SECONDS, new SynchronousQueue(),
            new DemoThreadFactory("訂單建立組"), new ThreadPoolExecutor.AbortPolicy());
    public static void main(String[] args) {
        //容量爲10,流速爲1個/秒,即60/每分鐘
        BucketLimit bucketLimit = BucketLimit.build(10, 60, TimeUnit.MINUTES);
        for (int i = 0; i < 15; i++) {
            executor.submit(() -> {
                boolean acquire = bucketLimit.acquire();
                System.out.println(Thread.currentThread().getName()+ " ," +System.currentTimeMillis() + " " + acquire);
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
    }
}

輸出:

From DemoThreadFactory's 訂單建立組-Worker-11 ,1599545066963 false
From DemoThreadFactory's 訂單建立組-Worker-12 ,1599545066963 false
From DemoThreadFactory's 訂單建立組-Worker-13 ,1599545066963 false
From DemoThreadFactory's 訂單建立組-Worker-14 ,1599545066964 false
From DemoThreadFactory's 訂單建立組-Worker-15 ,1599545066964 false
From DemoThreadFactory's 訂單建立組-Worker-3 ,1599545067961 true
From DemoThreadFactory's 訂單建立組-Worker-1 ,1599545068962 true
From DemoThreadFactory's 訂單建立組-Worker-2 ,1599545069963 true
From DemoThreadFactory's 訂單建立組-Worker-4 ,1599545070964 true
From DemoThreadFactory's 訂單建立組-Worker-5 ,1599545071965 true
From DemoThreadFactory's 訂單建立組-Worker-6 ,1599545072966 true
From DemoThreadFactory's 訂單建立組-Worker-7 ,1599545073966 true
From DemoThreadFactory's 訂單建立組-Worker-8 ,1599545074967 true
From DemoThreadFactory's 訂單建立組-Worker-9 ,1599545075967 true
From DemoThreadFactory's 訂單建立組-Worker-10 ,1599545076968 true

使用令牌桶算法來進行限流

令牌桶算法的原理是系統以恆定的速率產生令牌,而後把令牌放到令牌桶中,令牌桶有一個容量,當令牌桶滿了的時候,再向其中放令牌,那麼多餘的令牌會被丟棄;當想要處理一個請求的時候,須要從令牌桶中取出一個令牌,若是此時令牌桶中沒有令牌,那麼則拒絕該請求。從原理上看,令牌桶算法和漏桶算法是相反的,一個「進水」,一個是「漏水」。這種算法能夠應對突發程度的請求,所以比漏桶算法好。

令牌桶算法示意圖:

限流工具類RateLimiter

Google開源工具包Guava提供了限流工具類RateLimiter,能夠很是方便的控制系統每秒吞吐量.

示例:RateLimiter.create(5)建立QPS爲5的限流對象,後面又調用rateLimiter.setRate(10);將速率設爲10,輸出中分2段,第一段每次輸出相隔200毫秒,第二段每次輸出相隔100毫秒,能夠很是精準的控制系統的QPS。

public class RateLimiterTest {

    public static void main(String[] args) {
        //permitsPerSecond=1 即QPS=1
        RateLimiter rateLimiter = RateLimiter.create(1);
        for (int i = 0; i < 10; i++) {
            //調用acquire會根據QPS計算須要睡眠多久,返回耗時時間
            double acquire = rateLimiter.acquire();
            System.out.println(System.currentTimeMillis()+"耗時"+acquire);
        }
        System.out.println("----------");
        //能夠隨時調整速率,咱們將qps調整爲10
        rateLimiter.setRate(10);
        for (int i = 0; i < 10; i++) {
            //rateLimiter.acquire();
            double acquire = rateLimiter.acquire();
            System.out.println(System.currentTimeMillis()+"耗時"+acquire);
        }
    }
}

輸出:

1599545866820耗時0.0
1599545867820耗時0.998552
1599545868819耗時0.997836
1599545869820耗時0.999819
1599545870820耗時0.998723
1599545871819耗時0.999232
1599545872819耗時0.999328
1599545873819耗時1.000024
1599545874819耗時0.99995
1599545875820耗時0.999597
----------
1599545876819耗時0.998575
1599545876920耗時0.099593
1599545877020耗時0.098779
1599545877119耗時0.098661
1599545877220耗時0.099558
1599545877319耗時0.098965
1599545877419耗時0.099139
1599545877520耗時0.099768
1599545877620耗時0.098729
1599545877720耗時0.0986

JUC中工具類CompletableFuture

CompletableFuture是java8中新增的一個類,算是對Future的一種加強,用起來很方便,也是會常常用到的一個工具類,熟悉一下。

CompletionStage接口

  • CompletionStage表明異步計算過程當中的某一個階段,一個階段完成之後可能會觸發另一個階段
  • 一個階段的計算執行能夠是一個Function,Consumer或者Runnable。好比:stage.thenApply(x -> square(x)).thenAccept(x -> System.out.print(x)).thenRun(() -> System.out.println())
  • 一個階段的執行多是被單個階段的完成觸發,也多是由多個階段一塊兒觸發

CompletableFuture類

  • 在Java8中,CompletableFuture提供了很是強大的Future的擴展功能,能夠幫助咱們簡化異步編程的複雜性,而且提供了函數式編程的能力,能夠經過回調的方式處理計算結果,也提供了轉換和組合 CompletableFuture 的方法。
  • 它可能表明一個明確完成的Future,也有可能表明一個完成階段( CompletionStage ),它支持在計算完成之後觸發一些函數或執行某些動做。
  • 它實現了Future和CompletionStage接口

常見的方法,熟悉一下:

runAsync 和 supplyAsync方法

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)

沒有指定Executor的方法會使用ForkJoinPool.commonPool() 做爲它的線程池執行異步代碼。若是指定線程池,則使用指定的線程池運行。如下全部的方法都類同。

  • runAsync方法不支持返回值。
  • supplyAsync能夠支持返回值。

示例:

public class CompletableFutureTest1 {

    public static void main(String[] args) throws Exception {
        CompletableFutureTest1.runAsync();
        CompletableFutureTest1.supplyAsync();
    }

    //runAsync方法不支持返回值 Runnable
    public static void runAsync() throws Exception {
        CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
            }
            System.out.println("run end ...");
        });

        future.get();
    }

    //supplyAsync能夠支持返回值 Supplier<U>
    public static void supplyAsync() throws Exception {
        CompletableFuture<Long> future = CompletableFuture.supplyAsync(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
            }
            System.out.println("run end ...");
            return System.currentTimeMillis();
        });
  //若是沒有future.get()阻塞等待結果的話,由於CompletableFuture.supplyAsync()方法默認是守護線程形式執行任務,在主線程結束後會跟着退出,
        // 若是傳入的是線程池去執行,這不是守護線程,不會致使退出
        long time = future.get();
        System.out.println("time = "+time);
    }
}

輸出:

run end ...
run end ...
time = 1599556248764

計算結果完成時的回調方法

當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)

能夠看到Action的類型是BiConsumer它能夠處理正常的計算結果,或者異常狀況。

whenComplete 和 whenCompleteAsync 的區別:

  • whenComplete:當前任務的線程繼續執行 whenComplete 的任務。
  • whenCompleteAsync:把 whenCompleteAsync 這個任務繼續提交給線程池來進行執行。

示例:

public class CompletableFutureTest1 {

    public static void main(String[] args) throws Exception {
        CompletableFutureTest1.whenComplete();
        CompletableFutureTest1.whenCompleteBySupply();
    }

    public static void whenComplete() throws Exception {
        CompletableFuture<Void> future = CompletableFuture.runAsync(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
            }
            if (new Random().nextInt() % 2 >= 0) {
                int i = 12 / 0;
                //run end ...
                //執行完成!
                //int i = 12 / 0;
            }
            System.out.println("run end ...");
        });
        //對執行成功或者執行異常作處理的回調方法
        future.whenComplete((avoid, throwable) -> {
            //先判斷是否拋異常分開處理
            if (throwable != null) {
                System.out.println("執行失敗!" + throwable.getMessage());
            } else {
                System.out.println("執行完成!");
            }
        });
        //對執行異常作處理的回調方法
        future.exceptionally(throwable -> {
                    System.out.println("執行失敗!" + throwable.getMessage());
                    return null;
                }
        );
        TimeUnit.SECONDS.sleep(2);
    }

    public static void whenCompleteBySupply() throws Exception {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
            }
            if (new Random().nextInt() % 2 >= 0) {
                //int i = 12 / 0;
                //run end ...
                //執行完成!
                //int i = 12 / 0;
            }
            System.out.println("run end ...");
            return "success";
        });
        //whenComplete在thenAccept以前執行
        future.thenAccept(result -> {
            System.out.println(result);
        });
        //對執行成功或者執行異常作處理的回調方法
        future.whenComplete((avoid, throwable) -> {
            //先判斷是否拋異常分開處理
            if (throwable != null) {
                System.out.println("執行失敗!" + throwable.getMessage());
            } else {
                System.out.println("執行完成!");
            }
        });
        //對執行異常作處理的回調方法
        future.exceptionally(throwable -> {
                    System.out.println("執行失敗!" + throwable.getMessage());
                    return null;
                }
        );
        TimeUnit.SECONDS.sleep(2);
    }
    }

輸出:

執行失敗!java.lang.ArithmeticException: / by zero
執行失敗!java.lang.ArithmeticException: / by zero
run end ...
執行完成!
success

thenApply 方法

當一個線程依賴另外一個線程時,能夠使用 thenApply、thenApplyAsync 方法來把這兩個線程串行化。

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)

Function<? super T,? extends U> 
T:上一個任務返回結果的類型
U:當前任務的返回值類型

示例:

public class CompletableFutureTest2 {

    public static void main(String[] args) throws Exception {
        CompletableFutureTest2.thenApply();
    }
    //多個CompletableFuture能夠串行執行
    //當一個線程依賴另外一個線程時,能夠使用 thenApply 方法來把這兩個線程串行化。
    //多個任務串行執行,第二個任務依賴第一個任務的結果。
    private static void thenApply() throws Exception {
        CompletableFuture<Long> future = CompletableFuture.supplyAsync(() -> {
                    long result = new Random().nextInt(100);
                    System.out.println("result1=" + result);
                    return result;
                }
        ).thenApply((t -> {
            long result = t * 5;
            System.out.println("result2=" + result);
            return result;
        }));
        //方式一:阻塞等待結果
        long result = future.get();
        System.out.println("result2: " + result);
        //方式二:調用成功後接收任務的處理結果,並消費處理,無返回結果
        future.thenAccept((r) -> {
            System.out.println("result2: " + r);
        });
    }
}

輸出:

result1=41
result2=205
result2: 205
result2: 205

handle 方法——能夠處理正常和異常狀況的thenApply 方法

handle 是執行任務完成時對結果的處理。

handle 方法和 thenApply 方法處理方式基本同樣。不一樣的是 handle 是在任務完成後再執行,還能夠處理異常的任務。thenApply 只能夠執行正常的任務,任務出現異常則不執行 thenApply 方法。

public <U> CompletionStage<U> handle(BiFunction<? super T, Throwable, ? extends U> fn);
public <U> CompletionStage<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn);
public <U> CompletionStage<U> handleAsync(BiFunction<? super T, Throwable, ? extends U> fn,Executor executor);

示例:在 handle 中能夠根據任務是否有異常來進行作相應的後續處理操做。而 thenApply 方法,若是上個任務出現錯誤,則不會執行 thenApply 方法。

public class CompletableFutureTest3 {

    public static void main(String[] args) throws Exception {
        CompletableFutureTest3.handle();
    }

    public static void handle() throws Exception {
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(new Supplier<Integer>() {

            @Override
            public Integer get() {
                int i = 10 / 0;
                return new Random().nextInt(10);
            }
        }).handle(
                (param, throwable) -> {
                    int result = -1;
                    if (throwable == null) {
                        result = param * 2;
                    } else {
                        System.out.println(throwable.getMessage());
                    }
                    return result;
                }
                /*new BiFunction<Integer, Throwable, Integer>() {
            @Override
            public Integer apply(Integer param, Throwable throwable) {
                int result = -1;
                if(throwable==null){
                    result = param * 2;
                }else{
                    System.out.println(throwable.getMessage());
                }
                return result;
            }
        }*/);
        System.out.println(future.get());
    }
}

輸出:

java.lang.ArithmeticException: / by zero
-1

thenAccept 消費處理結果——無返回結果

接收任務的處理結果,並消費處理,無返回結果。

public CompletionStage<Void> thenAccept(Consumer<? super T> action);
public CompletionStage<Void> thenAcceptAsync(Consumer<? super T> action);
public CompletionStage<Void> thenAcceptAsync(Consumer<? super T> action,Executor executor);

示例:

public class CompletableFutureTest3 {

    public static void main(String[] args) throws Exception {
        //CompletableFutureTest3.handle();
        CompletableFutureTest3.thenAccept();
    }

    public static void thenAccept() throws Exception {
        CompletableFuture<Void> future = CompletableFuture.supplyAsync(() -> {
                    return new Random().nextInt(10);
                }
        ).thenAccept(integer -> {
            System.out.println(integer);
        });
        future.get();
    }
}
   //輸出:5

thenRun 方法——繼續執行下一個Runnable任務,不獲取上一個任務的處理結果

跟 thenAccept 方法不同的是,不關心任務的處理結果。只要上面的任務執行完成,就開始執行 thenRun 。

public CompletionStage<Void> thenRun(Runnable action);
public CompletionStage<Void> thenRunAsync(Runnable action);
public CompletionStage<Void> thenRunAsync(Runnable action,Executor executor);

示例:

public class CompletableFutureTest3 {

    public static void main(String[] args) throws Exception {
        CompletableFutureTest3.thenRun();
    }

    public static void thenRun() throws Exception{
        CompletableFuture<Void> future = CompletableFuture.supplyAsync(new Supplier<Integer>() {
            @Override
            public Integer get() {
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                return new Random().nextInt(10);
            }
        }).thenRun(() -> {
            System.out.println("thenRun ...");
        });
        future.get();
    }
}
//2秒後輸出:thenRun ...

thenCombine 合併任務

thenCombine 會把 兩個 CompletionStage 的任務都執行完成後,把兩個任務的結果一塊交給 thenCombine 來處理。

public <U,V> CompletionStage<V> thenCombine(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn);
public <U,V> CompletionStage<V> thenCombineAsync(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn);
public <U,V> CompletionStage<V> thenCombineAsync(CompletionStage<? extends U> other,BiFunction<? super T,? super U,? extends V> fn,Executor executor);

示例:

public class CompletableFutureTest3 {

    public static void main(String[] args) throws Exception {
        CompletableFutureTest3.thenCombine();
    }

    private static void thenCombine() throws Exception {
        CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
            return "hello";
        });
        CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
            return "world";
        });
        CompletableFuture<String> result = future1.thenCombine(future2, (result1, result2) -> {
            return result1 + " " + result2;
        });
        System.out.println(result.get());
    }
}
//輸出:hello world

thenAcceptBoth

當兩個CompletionStage都執行完成後,把結果一塊交給thenAcceptBoth來進行消耗。

public <U> CompletionStage<Void> thenAcceptBoth(CompletionStage<? extends U> other,BiConsumer<? super T, ? super U> action);
public <U> CompletionStage<Void> thenAcceptBothAsync(CompletionStage<? extends U> other,BiConsumer<? super T, ? super U> action);
public <U> CompletionStage<Void> thenAcceptBothAsync(CompletionStage<? extends U> other,BiConsumer<? super T, ? super U> action,     Executor executor);

示例:

public class CompletableFutureTest3 {

    public static void main(String[] args) throws Exception {
        CompletableFutureTest3.thenAcceptBoth();
        //等待守護進程執行完
        TimeUnit.SECONDS.sleep(5);
    }

    private static void thenAcceptBoth() throws Exception {
        CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(() -> {
            int t = new Random().nextInt(3);
            try {
                TimeUnit.SECONDS.sleep(t);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("f1=" + t);
            return t;
        });

        CompletableFuture<Integer> f2 = CompletableFuture.supplyAsync(() -> {
            int t = new Random().nextInt(3);
            try {
                TimeUnit.SECONDS.sleep(t);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("f2=" + t);
            return t;
        });
        f1.thenAcceptBoth(f2, (result1, result2) -> {
            System.out.println("f1=" + result1 + ";f2=" + result2 + ";");
        });
    }
 }

輸出:

f1=1
f2=1
f1=1;f2=1;

applyToEither 方法——有返回值消耗

兩個CompletionStage,誰執行返回的結果快,我就用那個CompletionStage的結果進行下一步的轉化操做。

public <U> CompletionStage<U> applyToEither(CompletionStage<? extends T> other,Function<? super T, U> fn);
public <U> CompletionStage<U> applyToEitherAsync(CompletionStage<? extends T> other,Function<? super T, U> fn);
public <U> CompletionStage<U> applyToEitherAsync(CompletionStage<? extends T> other,Function<? super T, U> fn,Executor executor);

示例:

public class CompletableFutureTest3 {
    public static void main(String[] args) throws Exception {
        CompletableFutureTest3.applyToEither();
    }

    private static void applyToEither() throws Exception {
        CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(()->{
            int t = 1;
            try {
                TimeUnit.SECONDS.sleep(t);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("f1="+t);
            return t;
        });
        CompletableFuture<Integer> f2 = CompletableFuture.supplyAsync(()->{
            int t = 2;
            try {
                TimeUnit.SECONDS.sleep(t);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("f2="+t);
            return t;
        });

        CompletableFuture<Integer> result = f1.applyToEither(f2, (r)->{
            System.out.println(r);
            return r * 2;
        });
        System.out.println(result.get());
    }

輸出:

f1=1
1
2

acceptEither 方法——無返回值消耗

兩個CompletionStage,誰執行返回的結果快,我就用那個CompletionStage的結果進行下一步的消耗操做。注意,這時候其實兩個CompletionStage都是會執行完的,只是咱們只獲取其中的一個比較快的結果而已,參考示例的輸出。

public CompletionStage<Void> acceptEither(CompletionStage<? extends T> other,Consumer<? super T> action);
public CompletionStage<Void> acceptEitherAsync(CompletionStage<? extends T> other,Consumer<? super T> action);
public CompletionStage<Void> acceptEitherAsync(CompletionStage<? extends T> other,Consumer<? super T> action,Executor executor);

示例:

public class CompletableFutureTest3 {
    public static void main(String[] args) throws Exception {
        //CompletableFutureTest3.applyToEither();
        CompletableFutureTest3.acceptEither();
        TimeUnit.SECONDS.sleep(5);
    }

    private static void acceptEither() throws Exception {
        CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(() -> {
            int t = new Random().nextInt(3);
            try {
                TimeUnit.SECONDS.sleep(t);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("f1=" + t);
            return t;
        });

        CompletableFuture<Integer> f2 = CompletableFuture.supplyAsync(() -> {
            int t = new Random().nextInt(3);
            try {
                TimeUnit.SECONDS.sleep(t);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("f2=" + t);
            return t;
        });
        f1.acceptEither(f2, (t) -> {
            System.out.println(t);
        });
    }
}

輸出:

f1=1
1
f2=2

runAfterEither 方法

兩個CompletionStage,任何一個完成了都會執行下一步的操做(Runnable),兩個CompletionStage都是會執行完的.

public CompletionStage<Void> runAfterEither(CompletionStage<?> other,Runnable action);
public CompletionStage<Void> runAfterEitherAsync(CompletionStage<?> other,Runnable action);
public CompletionStage<Void> runAfterEitherAsync(CompletionStage<?> other,Runnable action,Executor executor);

示例代碼

public class CompletableFutureTest3 {
    public static void main(String[] args) throws Exception {
        //CompletableFutureTest3.applyToEither();
        //CompletableFutureTest3.acceptEither();
        CompletableFutureTest3.runAfterEither();
        TimeUnit.SECONDS.sleep(5);
    }

    private static void runAfterEither() throws Exception {
        CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(new Supplier<Integer>() {
            @Override
            public Integer get() {
                int t = new Random().nextInt(3);
                try {
                    TimeUnit.SECONDS.sleep(t);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("f1=" + t);
                return t;
            }
        });

        CompletableFuture<Integer> f2 = CompletableFuture.supplyAsync(new Supplier<Integer>() {
            @Override
            public Integer get() {
                int t = new Random().nextInt(3);
                try {
                    TimeUnit.SECONDS.sleep(t);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("f2=" + t);
                return t;
            }
        });
        f1.runAfterEither(f2, ()->{
            System.out.println("上面有一個已經完成了。");
        });
    }
}

輸出:

f1=0
上面有一個已經完成了。
f2=1

runAfterBoth

兩個CompletionStage,都完成了計算纔會執行下一步的操做(Runnable),注意輸出順序,runAfterBoth方法要等兩個CompletionStage都執行完了纔會執行。

public CompletionStage<Void> runAfterBoth(CompletionStage<?> other,Runnable action);
public CompletionStage<Void> runAfterBothAsync(CompletionStage<?> other,Runnable action);
public CompletionStage<Void> runAfterBothAsync(CompletionStage<?> other,Runnable action,Executor executor);

示例代碼

public class CompletableFutureTest3 {
    public static void main(String[] args) throws Exception {
        //CompletableFutureTest3.applyToEither();
        //CompletableFutureTest3.acceptEither();
        //CompletableFutureTest3.runAfterEither();
        CompletableFutureTest3.runAfterBoth();
        TimeUnit.SECONDS.sleep(5);
    }

    private static void runAfterBoth() throws Exception {
        CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(new Supplier<Integer>() {
            @Override
            public Integer get() {
                int t = new Random().nextInt(3);
                try {
                    TimeUnit.SECONDS.sleep(t);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("f1="+t);
                return t;
            }
        });

        CompletableFuture<Integer> f2 = CompletableFuture.supplyAsync(new Supplier<Integer>() {
            @Override
            public Integer get() {
                int t = new Random().nextInt(3);
                try {
                    TimeUnit.SECONDS.sleep(t);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("f2="+t);
                return t;
            }
        });
        f1.runAfterBoth(f2, new Runnable() {

            @Override
            public void run() {
                System.out.println("上面兩個任務都執行完成了。");
            }
        });
    }
}

輸出:

f1=1
f2=2
上面兩個任務都執行完成了。

thenCompose 方法

thenCompose 方法容許你對兩個 CompletionStage 進行流水線操做,第一個操做完成時,將其結果做爲參數傳遞給第二個操做。

public <U> CompletableFuture<U> thenCompose(Function<? super T, ? extends CompletionStage<U>> fn);
public <U> CompletableFuture<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn) ;
public <U> CompletableFuture<U> thenComposeAsync(Function<? super T, ? extends CompletionStage<U>> fn, Executor executor) ;

示例代碼

public class CompletableFutureTest3 {
    public static void main(String[] args) throws Exception {
        CompletableFutureTest3.thenCompose();
        TimeUnit.SECONDS.sleep(3);
    }

    private static void thenCompose() throws Exception {
        CompletableFuture<Integer> f = CompletableFuture.supplyAsync(() -> {
            int t = new Random().nextInt(3);
            System.out.println("t1=" + t);
            return t;
        }).thenCompose((param) -> {
            return CompletableFuture.supplyAsync(() -> {
                int t = param * 2;
                System.out.println("t2=" + t);
                return t;
            });
        });
        System.out.println("thenCompose result : " + f.get());
    }
}

輸出:

t1=1
t2=2
thenCompose result : 2

疑問:

Q:thenAcceptBoth與thenCombine 的區別是什麼?

A:thenAcceptBoth無返回值消耗執行,thenCombine 會有返回值。通常accept都是沒有返回值的,apply是有返回值的。

Q:thenCompose 與thenApply 方法 的區別是什麼?不都是串行執行下一個任務,並把第一個任務做爲參數傳遞給第二個任務麼?

獲取線程執行結果的6種方法

方式1:Thread的join()方法實現

代碼中經過join方式阻塞了當前主線程,當thread線程執行完畢以後,join方法纔會繼續執行。

join的方式,只能阻塞一個線程,若是其餘線程中也須要獲取thread線程的執行結果,join方法無能爲力了。

示例:

public class ThreadJoinTest {
    //用於封裝結果
    static class Result<T> {
        T result;

        public T getResult() {
            return result;
        }

        public void setResult(T result) {
            this.result = result;
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Result<String> result = new Result<>();
        Thread t = new Thread(() -> {
            System.out.println("start thread!");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            result.setResult("success");
            System.out.println("end thread!");
        });
        t.start();
        //讓主線程等待thread線程執行完畢以後再繼續,join方法會讓當前線程阻塞
        t.join();
        System.out.println("main get result="+result.getResult());
    }
}

輸出:

start thread!
end thread!
main get result=success

方式2:CountDownLatch實現

使用CountDownLatch可讓一個或者多個線程等待一批線程完成以後,本身再繼續.

示例:

public class CountDownLatchTest2 {
    static class Result<T>{
        private T result;

        public T getResult() {
            return result;
        }

        public void setResult(T result) {
            this.result = result;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Result<String> result = new Result<>();
        CountDownLatch latch = new CountDownLatch(1);
        Thread t = new Thread(() -> {
            System.out.println("start thread!");
            try {
                TimeUnit.SECONDS.sleep(1);
                result.setResult("success");
                System.out.println("end thread!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                latch.countDown();
            }
        });
        t.start();
        latch.await();
        System.out.println("main get result="+result.getResult());
    }
}

輸出:

start thread!
end thread!
main get result=success

方式3:ExecutorService.submit方法實現——ThreadPoolExecutor

使用ExecutorService.submit方法實現的,此方法返回一個Future,future.get()會讓當前線程阻塞,直到Future關聯的任務執行完畢。

示例:

public class ThreadPoolExecutorTest2 {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //自定義包含策略
        ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 5, 60,
                TimeUnit.SECONDS, new LinkedBlockingQueue<>(5),
                new DemoThreadFactory("訂單建立組"), new ThreadPoolExecutor.AbortPolicy());
        Future<String> future = executor.submit(() -> {
            System.out.println("start thread!");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("end thread!");
            return "success";
        });
        executor.shutdown();
        System.out.println("main get result="+future.get());
    }
}

輸出同上。

方式4:FutureTask方式1——做爲Runnable傳給Thread執行

線程池的submit方法傳入的Callable對象本質上也是包裝成一個FutureTask來執行。

代碼中使用FutureTask實現的,FutureTask實現了Runnable接口,而且內部帶返回值,因此能夠傳遞給Thread直接運行,futureTask.get()會阻塞當前線程,直到FutureTask構造方法傳遞的任務執行完畢,get方法纔會返回。

示例:

public class FutureTaskTest {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //建立一個FutureTask
        FutureTask<String> futureTask = new FutureTask<>(() -> {
            System.out.println("start thread!");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("end thread!");
            return "success";
        });
        //將futureTask傳遞一個線程運行
        new Thread(futureTask).start();
        //futureTask.get()會阻塞當前線程,直到futureTask執行完畢
        String result = futureTask.get();
        System.out.println("main get result=" + result);
    }
}

方式5:FutureTask方式2——構造FutureTask對象及執行內容,直接在Thread裏面跑run方法

當futureTask的run()方法執行完畢以後,futureTask.get()會從阻塞中返回。

示例:

public class FutureTaskTest1 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //建立一個FutureTask
        FutureTask<String> futureTask = new FutureTask<>(() -> {
            System.out.println("start thread!");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("end thread!");
            return "success";
        });
        //將futureTask傳遞一個線程運行
        new Thread(()->futureTask.run()).start();
        //futureTask.get()會阻塞當前線程,直到futureTask執行完畢
        String result = futureTask.get();
        System.out.println("main get result=" + result);
    }
}

方式6:CompletableFuture方式實現

CompletableFuture.supplyAsync能夠用來異步執行一個帶返回值的任務,調用completableFuture.get()

會阻塞當前線程,直到任務執行完畢,get方法纔會返回。

public class CompletableFutureTest4 {

    public static void main(String[] args) throws Exception {
        CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
            System.out.println("start thread!");
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("end thread!");
            return "success";
        });
        // future.get()會阻塞當前線程直到得到結果
        System.out.println("main get result="+future.get());
    }
}

高併發中計數器的四種實現方式

需求:一個jvm中實現一個計數器功能,需保證多線程狀況下數據正確性。

咱們來模擬50個線程,每一個線程對計數器遞增100萬次,最終結果應該是5000萬。

咱們使用4種方式實現,看一下其性能,而後引出爲何須要使用LongAdder、LongAccumulator。

方式一:使用加鎖的方式實現——synchronized或Lock

從示例輸出結果看,ReentrantLock的效率明顯比synchronized差了2-3倍。

示例:

public class SynchronizeCalculator {
    private static long count = 0;
    private static Lock lock = new ReentrantLock();
    public synchronized static void incrment() {
        count++;
    }

    public static void incrmentByLock() {
        lock.lock();
        try {
            count++;
        }finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 5; i++) {
            count = 0;
            averageTest();
        }
    }

    public static void averageTest() throws InterruptedException {
        long t1 = System.currentTimeMillis();
        //自定義包含策略
        ThreadPoolExecutor executor = new ThreadPoolExecutor(50, 50, 60,
                TimeUnit.SECONDS, new LinkedBlockingQueue<>(5),
                new DemoThreadFactory("訂單建立組"), new ThreadPoolExecutor.AbortPolicy());
        CountDownLatch latch = new CountDownLatch(50);
        for (int i = 0; i < 50; i++) {
            executor.execute(() -> {
                try {
                    for (int j = 0; j < 1000000; j++) {
                        incrment();
                        //incrmentByLock();
                    }
                } finally {
                    latch.countDown();
                }

            });
        }
        latch.await();
        long t2 = System.currentTimeMillis();
        System.out.println(String.format("結果:%s,耗時(ms):%s", count, (t2 - t1)));
        executor.shutdown();
    }
}

輸出:

//synchronized
結果:50000000,耗時(ms):490
結果:50000000,耗時(ms):1574
結果:50000000,耗時(ms):399
結果:50000000,耗時(ms):395
結果:50000000,耗時(ms):396
//lock
結果:50000000,耗時(ms):1289
結果:50000000,耗時(ms):1239
結果:50000000,耗時(ms):1224
結果:50000000,耗時(ms):1219
結果:50000000,耗時(ms):1246

方式2:AtomicLong實現

AtomicLong內部採用CAS的方式實現,併發量大的狀況下,CAS失敗率比較高,致使性能比synchronized還低一些。併發量不是太大的狀況下,CAS性能仍是能夠的。

示例:

public class AtomicLongTest {
    private static AtomicLong count = new AtomicLong(0);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 5; i++) {
            count.set(0);
            averageTest();
        }
    }

    public static void averageTest() throws InterruptedException {
        long t1 = System.currentTimeMillis();
        //自定義包含策略
        ThreadPoolExecutor executor = new ThreadPoolExecutor(50, 50, 60,
                TimeUnit.SECONDS, new LinkedBlockingQueue<>(5),
                new DemoThreadFactory("訂單建立組"), new ThreadPoolExecutor.AbortPolicy());
        CountDownLatch latch = new CountDownLatch(50);
        for (int i = 0; i < 50; i++) {
            executor.execute(() -> {
                try {
                    for (int j = 0; j < 1000000; j++) {
                        count.getAndIncrement();
                    }
                } finally {
                    latch.countDown();
                }

            });
        }
        latch.await();
        long t2 = System.currentTimeMillis();
        System.out.println(String.format("結果:%s,耗時(ms):%s", count.get(), (t2 - t1)));
        executor.shutdown();
    }
}

輸出:

結果:50000000,耗時(ms):1018
結果:50000000,耗時(ms):1442
結果:50000000,耗時(ms):1033
結果:50000000,耗時(ms):935
結果:50000000,耗時(ms):1320

方式3:LongAdder實現——至關於鎖分段技術

先介紹一下LongAdder,說到LongAdder,不得不提的就是AtomicLong,AtomicLong是JDK1.5開始出現的,裏面主要使用了一個long類型的value做爲成員變量,而後使用循環的CAS操做去操做value的值,併發量比較大的狀況下,CAS操做失敗的機率較高,內部失敗了會重試,致使耗時可能會增長。

LongAdder是JDK1.8開始出現的,所提供的API基本上能夠替換掉原先的AtomicLong。LongAdder在併發量比較大的狀況下,操做數據的時候,至關於把這個數字分紅了不少份數字,而後交給多我的去管控,每一個管控者負責保證部分數字在多線程狀況下操做的正確性。當多線程訪問的時,經過hash算法映射到具體管控者去操做數據,最後再彙總全部的管控者的數據,獲得最終結果。至關於下降了併發狀況下鎖的粒度,因此效率比較高,看一下下面的圖,方便理解:

示例:

代碼中new LongAdder()建立一個LongAdder對象,內部數字初始值是0,調用increment()方法能夠對LongAdder內部的值原子遞增1。reset()方法能夠重置LongAdder的值,使其歸0。

public class LongAdderTest {
    private static LongAdder count = new LongAdder();

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 5; i++) {
            count.reset();
            averageTest();
        }
    }

    public static void averageTest() throws InterruptedException {
        long t1 = System.currentTimeMillis();
        //自定義包含策略
        ThreadPoolExecutor executor = new ThreadPoolExecutor(50, 50, 60,
                TimeUnit.SECONDS, new LinkedBlockingQueue<>(5),
                new DemoThreadFactory("訂單建立組"), new ThreadPoolExecutor.AbortPolicy());
        CountDownLatch latch = new CountDownLatch(50);
        for (int i = 0; i < 50; i++) {
            executor.execute(() -> {
                try {
                    for (int j = 0; j < 1000000; j++) {
                        count.increment();
                    }
                } finally {
                    latch.countDown();
                }

            });
        }
        latch.await();
        long t2 = System.currentTimeMillis();
        System.out.println(String.format("結果:%s,耗時(ms):%s", count.sum(), (t2 - t1)));
        executor.shutdown();
    }
}

輸出:

結果:50000000,耗時(ms):209
結果:50000000,耗時(ms):133
結果:50000000,耗時(ms):149
結果:50000000,耗時(ms):146
結果:50000000,耗時(ms):148

方式4:LongAccumulator實現

LongAccumulator介紹

LongAccumulator是LongAdder的功能加強版。LongAdder的API只有對數值的加減,而LongAccumulator提供了自定義的函數操做,其構造函數以下:

/**
  * accumulatorFunction:須要執行的二元函數(接收2個long做爲形參,並返回1個long)
  * identity:初始值
 **/
public LongAccumulator(LongBinaryOperator accumulatorFunction, long identity) {
    this.function = accumulatorFunction;
    base = this.identity = identity;
}

示例:

LongAccumulator的效率和LongAdder差很少,不過更靈活一些。

調用new LongAdder()等價於new LongAccumulator((x, y) -> x + y, 0L)。

從上面4個示例的結果來看,LongAdder、LongAccumulator全面超越同步鎖及AtomicLong的方式,建議在使用AtomicLong的地方能夠直接替換爲LongAdder、LongAccumulator,吞吐量更高一些。

public class LongAccumulatorTest {
    private static volatile LongAccumulator count = new LongAccumulator((x, y) -> x + y, 0);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 5; i++) {
            count.reset();
            averageTest();
        }
    }

    public static void averageTest() throws InterruptedException {
        long t1 = System.currentTimeMillis();
        //自定義包含策略
        ThreadPoolExecutor executor = new ThreadPoolExecutor(50, 50, 60,
                TimeUnit.SECONDS, new LinkedBlockingQueue<>(5),
                new DemoThreadFactory("訂單建立組"), new ThreadPoolExecutor.AbortPolicy());
        CountDownLatch latch = new CountDownLatch(50);
        for (int i = 0; i < 50; i++) {
            executor.execute(() -> {
                try {
                    for (int j = 0; j < 1000000; j++) {
                        count.accumulate(1);
                    }
                } finally {
                    latch.countDown();
                }

            });
        }
        latch.await();
        long t2 = System.currentTimeMillis();
        System.out.println(String.format("結果:%s,耗時(ms):%s", count.longValue(), (t2 - t1)));
        executor.shutdown();
    }
}

輸出:

結果:50000000,耗時(ms):152
結果:50000000,耗時(ms):148
結果:50000000,耗時(ms):137
結果:50000000,耗時(ms):137
結果:50000000,耗時(ms):144

疑問:

Q:LongAccumulator.reset方法並不能重置重置LongAccumulator的identity:初始值正確,使其恢復原來的初始值。當初始值爲0是不會發生這個問題,而當咱們設置初始值如1時,就會致使後續的計算操做增長了5份初始值,目前猜想緣由是由於代碼中LongAccumulator在併發量比較大的狀況下,操做數據的時候,至關於把這個數字分紅了不少份數字 ,而初始化的時候也是初始化了多份數據,致使初始值疊加了多份。不知道這是個bug麼?待解惑。

在此記錄下來但願有遇到這種狀況的同窗注意。解決方法即是要麼初始值identity=0不會有這種問題;或者有須要使用reset方法重置的改成從新建立個LongAccumulator處理。

源碼:

public void reset() {
    Cell[] as = cells; Cell a;
    base = identity;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                //對多個cell進行初始值賦值致使後面計算疊加了多份初始值
                a.value = identity;
        }
    }
}

示例:

public class LongAccumulatorTest {
    //設置初始值爲1查看輸出結果
    private static volatile LongAccumulator count = new LongAccumulator((x, y) -> x + y, 1);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 5; i++) {
            count.reset();
            averageTest();
        }
    }

    public static void averageTest() throws InterruptedException {
        long t1 = System.currentTimeMillis();
        //自定義包含策略
        ThreadPoolExecutor executor = new ThreadPoolExecutor(50, 50, 60,
                TimeUnit.SECONDS, new LinkedBlockingQueue<>(5),
                new DemoThreadFactory("訂單建立組"), new ThreadPoolExecutor.AbortPolicy());
        CountDownLatch latch = new CountDownLatch(50);
        for (int i = 0; i < 50; i++) {
            executor.execute(() -> {
                try {
                    for (int j = 0; j < 1000000; j++) {
                        count.accumulate(1);
                    }
                } finally {
                    latch.countDown();
                }

            });
        }
        latch.await();
        long t2 = System.currentTimeMillis();
        System.out.println(String.format("結果:%s,耗時(ms):%s", count.longValue(), (t2 - t1)));
        executor.shutdown();
    }
}

輸出:這時候你會發現只有第一次計算是正確的,只要使用了rest方法重置就會致使這個錯誤。

結果:50000001,耗時(ms):185
結果:50000005,耗時(ms):143
結果:50000005,耗時(ms):139
結果:50000005,耗時(ms):162
結果:50000005,耗時(ms):142

演示公平鎖和非公平鎖

先理解一下什麼是公平鎖、非公平鎖?

公平鎖和非公平鎖體如今別人釋放鎖的一瞬間,若是前面已經有排隊的,新來的是否能夠插隊,若是能夠插隊表示是非公平的,若是不可用插隊,只能排在最後面,是公平的方式。

示例:

@Slf4j
public class ReentrantLockTest2 {

    public static void main(String[] args) throws InterruptedException {
        ReentrantLockTest2.LockTest(false);
        TimeUnit.SECONDS.sleep(4);
        log.error("-------------------------------");
        ReentrantLockTest2.LockTest(true);
    }

    public static void LockTest(boolean fair) throws InterruptedException {
        ReentrantLock lock = new ReentrantLock(fair);
        new Thread(() -> {
            lock.lock();
            try {
                log.error(Thread.currentThread().getName() + " start!");
                TimeUnit.SECONDS.sleep(3);
                new Thread(() -> {
                    //注意線程池要當前線程建立的才能使用,若是傳給新開的線程會獲取不到線程池
                    test("後到組",lock);
                }).start();
                //test(executorAfter,lock);
                log.error(Thread.currentThread().getName() + "end!");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "Hold Lock 4 Test Thread").start();
        test("先到組",lock);
        TimeUnit.SECONDS.sleep(3);
    }

    private static void test(String name,Lock lock){
        //自定義包含策略
        ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 10, 60,
                TimeUnit.SECONDS, new LinkedBlockingQueue<>(5),
                new DemoThreadFactory(name), new ThreadPoolExecutor.AbortPolicy());
        for (int i = 0; i < 10; i++) {
            executor.execute(() -> {
                lock.lock();
                try {
                    log.error("獲取到鎖!");
                } finally {
                    lock.unlock();
                }
            });
        }
        executor.shutdown();
    }
}

輸出:

14:45:23.204 [Hold Lock 4 Test Thread] ERROR com.self.current.ReentrantLockTest2 - Hold Lock 4 Test Thread start!
14:45:26.211 [Hold Lock 4 Test Thread] ERROR com.self.current.ReentrantLockTest2 - Hold Lock 4 Test Threadend!
14:45:26.211 [From DemoThreadFactory's 先到組-Worker-1] ERROR com.self.current.ReentrantLockTest2 - 獲取到鎖!
14:45:26.211 [From DemoThreadFactory's 先到組-Worker-2] ERROR com.self.current.ReentrantLockTest2 - 獲取到鎖!
14:45:26.212 [From DemoThreadFactory's 先到組-Worker-3] ERROR com.self.current.ReentrantLockTest2 - 獲取到鎖!
14:45:26.212 [From DemoThreadFactory's 先到組-Worker-4] ERROR com.self.current.ReentrantLockTest2 - 獲取到鎖!
14:45:26.212 [From DemoThreadFactory's 先到組-Worker-5] ERROR com.self.current.ReentrantLockTest2 - 獲取到鎖!
14:45:26.212 [From DemoThreadFactory's 先到組-Worker-6] ERROR com.self.current.ReentrantLockTest2 - 獲取到鎖!
14:45:26.212 [From DemoThreadFactory's 先到組-Worker-7] ERROR com.self.current.ReentrantLockTest2 - 獲取到鎖!
14:45:26.212 [From DemoThreadFactory's 先到組-Worker-8] ERROR com.self.current.ReentrantLockTest2 - 獲取到鎖!
14:45:26.212 [From DemoThreadFactory's 後到組-Worker-4] ERROR com.self.current.ReentrantLockTest2 - 獲取到鎖!
14:45:26.212 [From DemoThreadFactory's 先到組-Worker-9] ERROR com.self.current.ReentrantLockTest2 - 獲取到鎖!
14:45:26.213 [From DemoThreadFactory's 後到組-Worker-8] ERROR com.self.current.ReentrantLockTest2 - 獲取到鎖!
14:45:26.218 [From DemoThreadFactory's 後到組-Worker-10] ERROR com.self.current.ReentrantLockTest2 - 獲取到鎖!
14:45:26.218 [From DemoThreadFactory's 先到組-Worker-10] ERROR com.self.current.ReentrantLockTest2 - 獲取到鎖!
14:45:26.218 [From DemoThreadFactory's 後到組-Worker-2] ERROR com.self.current.ReentrantLockTest2 - 獲取到鎖!
14:45:26.218 [From DemoThreadFactory's 後到組-Worker-1] ERROR com.self.current.ReentrantLockTest2 - 獲取到鎖!
14:45:26.219 [From DemoThreadFactory's 後到組-Worker-3] ERROR com.self.current.ReentrantLockTest2 - 獲取到鎖!
14:45:26.219 [From DemoThreadFactory's 後到組-Worker-5] ERROR com.self.current.ReentrantLockTest2 - 獲取到鎖!
14:45:26.219 [From DemoThreadFactory's 後到組-Worker-6] ERROR com.self.current.ReentrantLockTest2 - 獲取到鎖!
14:45:26.219 [From DemoThreadFactory's 後到組-Worker-7] ERROR com.self.current.ReentrantLockTest2 - 獲取到鎖!
14:45:26.219 [From DemoThreadFactory's 後到組-Worker-9] ERROR com.self.current.ReentrantLockTest2 - 獲取到鎖!
14:45:30.205 [main] ERROR com.self.current.ReentrantLockTest2 - -------------------------------
14:45:30.205 [Hold Lock 4 Test Thread] ERROR com.self.current.ReentrantLockTest2 - Hold Lock 4 Test Thread start!
14:45:33.206 [Hold Lock 4 Test Thread] ERROR com.self.current.ReentrantLockTest2 - Hold Lock 4 Test Threadend!
14:45:33.206 [From DemoThreadFactory's 先到組-Worker-1] ERROR com.self.current.ReentrantLockTest2 - 獲取到鎖!
14:45:33.206 [From DemoThreadFactory's 先到組-Worker-2] ERROR com.self.current.ReentrantLockTest2 - 獲取到鎖!
14:45:33.209 [From DemoThreadFactory's 先到組-Worker-3] ERROR com.self.current.ReentrantLockTest2 - 獲取到鎖!
14:45:33.209 [From DemoThreadFactory's 先到組-Worker-4] ERROR com.self.current.ReentrantLockTest2 - 獲取到鎖!
14:45:33.209 [From DemoThreadFactory's 先到組-Worker-5] ERROR com.self.current.ReentrantLockTest2 - 獲取到鎖!
14:45:33.209 [From DemoThreadFactory's 先到組-Worker-6] ERROR com.self.current.ReentrantLockTest2 - 獲取到鎖!
14:45:33.210 [From DemoThreadFactory's 先到組-Worker-7] ERROR com.self.current.ReentrantLockTest2 - 獲取到鎖!
14:45:33.210 [From DemoThreadFactory's 先到組-Worker-8] ERROR com.self.current.ReentrantLockTest2 - 獲取到鎖!
14:45:33.210 [From DemoThreadFactory's 先到組-Worker-9] ERROR com.self.current.ReentrantLockTest2 - 獲取到鎖!
14:45:33.210 [From DemoThreadFactory's 先到組-Worker-10] ERROR com.self.current.ReentrantLockTest2 - 獲取到鎖!
14:45:33.210 [From DemoThreadFactory's 後到組-Worker-2] ERROR com.self.current.ReentrantLockTest2 - 獲取到鎖!
14:45:33.210 [From DemoThreadFactory's 後到組-Worker-1] ERROR com.self.current.ReentrantLockTest2 - 獲取到鎖!
14:45:33.211 [From DemoThreadFactory's 後到組-Worker-6] ERROR com.self.current.ReentrantLockTest2 - 獲取到鎖!
14:45:33.211 [From DemoThreadFactory's 後到組-Worker-7] ERROR com.self.current.ReentrantLockTest2 - 獲取到鎖!
14:45:33.211 [From DemoThreadFactory's 後到組-Worker-5] ERROR com.self.current.ReentrantLockTest2 - 獲取到鎖!
14:45:33.211 [From DemoThreadFactory's 後到組-Worker-4] ERROR com.self.current.ReentrantLockTest2 - 獲取到鎖!
14:45:33.211 [From DemoThreadFactory's 後到組-Worker-3] ERROR com.self.current.ReentrantLockTest2 - 獲取到鎖!
14:45:33.211 [From DemoThreadFactory's 後到組-Worker-9] ERROR com.self.current.ReentrantLockTest2 - 獲取到鎖!
14:45:33.212 [From DemoThreadFactory's 後到組-Worker-10] ERROR com.self.current.ReentrantLockTest2 - 獲取到鎖!
14:45:33.212 [From DemoThreadFactory's 後到組-Worker-8] ERROR com.self.current.ReentrantLockTest2 - 獲取到鎖!

google提供的一些好用的併發工具類

關於併發方面的,juc已幫咱們提供了不少好用的工具,而谷歌在此基礎上作了擴展,使併發編程更容易,這些工具放在guava.jar包中。

guava maven配置

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>27.0-jre</version>
</dependency>

guava中經常使用幾個類

MoreExecutors:提供了一些靜態方法,是對juc中的Executors類的一個擴展。

Futures:也提供了不少靜態方法,是對juc中Future的一個擴展。

案例1:異步執行任務完畢以後回調——至關於CompletableFuture的whenComplete方法

ListeningExecutorService接口繼承於juc中的ExecutorService接口,對ExecutorService作了一些擴展,看其名字中帶有Listening,說明這個接口自帶監聽的功能,能夠監聽異步執行任務的結果。經過MoreExecutors.listeningDecorator建立一個ListeningExecutorService對象,需傳遞一個ExecutorService參數,傳遞的ExecutorService負責異步執行任務。

ListeningExecutorService的submit方法用來異步執行一個任務,返回ListenableFuture,ListenableFuture接口繼承於juc中的Future接口,對Future作了擴展,使其帶有監聽的功能。調用submit.addListener能夠在執行的任務上添加監聽器,當任務執行完畢以後會回調這個監聽器中的方法。

ListenableFuture的get方法會阻塞當前線程直到任務執行完畢。

另外一種回調方式是經過調用Futures的靜態方法addCallback在異步執行的任務中添加回調,回調的對象是一個FutureCallback,此對象有2個方法,任務執行成功調用onSuccess,執行失敗調用onFailure。

失敗的狀況能夠將代碼中int i = 10 / 0;註釋去掉,執行一下能夠看看效果。

示例:

@Slf4j
public class GuavaTest {

    //至關於CompletableFuture的whenComplete方法
    public static void main1(String[] args) throws ExecutionException, InterruptedException {
        //建立一個線程池
        ExecutorService delegate = Executors.newFixedThreadPool(5);
        try {
            ListeningExecutorService executorService = MoreExecutors.listeningDecorator(delegate);
            //異步執行一個任務
            ListenableFuture<Integer> submit = executorService.submit(() -> {
                log.error("{}", System.currentTimeMillis());
                //休眠2秒,默認耗時
                TimeUnit.SECONDS.sleep(2);
                log.error("{}", System.currentTimeMillis());
                return 10;
            });
            //當任務執行完畢以後回調對應的方法
            submit.addListener(() -> {
                log.error("任務執行完畢了,我被回調了");
            }, MoreExecutors.directExecutor());
            log.error("{}", submit.get());
        } finally {
            delegate.shutdown();
        }
    }

    //至關於CompletableFuture的whenComplete方法
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 60,
                TimeUnit.SECONDS, new LinkedBlockingQueue<>(10),
                new DemoThreadFactory("訂單建立組"), new ThreadPoolExecutor.AbortPolicy());


        ListeningExecutorService service = MoreExecutors.listeningDecorator(executor);
        try {
            ListenableFuture<Integer> future = service.submit(() -> {
                log.error("{}", System.currentTimeMillis());
                //休眠2秒,默認耗時
                TimeUnit.SECONDS.sleep(2);
                //int i = 10 / 0;
                log.error("{}", System.currentTimeMillis());
                return 10;
            });
            Futures.addCallback(future, new FutureCallback<Integer>() {
                @Override
                public void onSuccess(Integer integer) {
                    log.error("執行成功:{}", integer);
                }

                @Override
                public void onFailure(Throwable throwable) {
                    log.error("執行失敗:{}", throwable.getMessage());
                }
            });
            log.error("{}", future.get());
        } finally {
            service.shutdown();
        }

    }
}

輸出:

15:32:54.480 [From DemoThreadFactory's 訂單建立組-Worker-1] ERROR com.self.current.GuavaTest - 1599809574477
15:32:56.487 [From DemoThreadFactory's 訂單建立組-Worker-1] ERROR com.self.current.GuavaTest - 1599809576487
15:32:56.488 [main] ERROR com.self.current.GuavaTest - 10
15:32:56.488 [From DemoThreadFactory's 訂單建立組-Worker-1] ERROR com.self.current.GuavaTest - 執行成功:10

示例2:獲取一批異步任務的執行結果——Futures.allAsList(futureList).get()

結果中按順序輸出了6個異步任務的結果,此處用到了Futures.allAsList方法,看一下此方法的聲明:

public static <V> ListenableFuture<List<V>> allAsList(
      Iterable<? extends ListenableFuture<? extends V>> futures)

傳遞一批ListenableFuture,返回一個ListenableFuture<List >,內部將一批結果轉換爲了一個ListenableFuture對象。

示例:

public static void main(String[] args) throws ExecutionException, InterruptedException {
    log.error("star");
    ExecutorService delegate = Executors.newFixedThreadPool(5);
    try {
        ListeningExecutorService executorService = MoreExecutors.listeningDecorator(delegate);
        List<ListenableFuture<Integer>> futureList = new ArrayList<>();
        for (int i = 5; i >= 0; i--) {
            int j = i;
            futureList.add(executorService.submit(() -> {
                TimeUnit.SECONDS.sleep(j);
                return j;
            }));
        }
        //把多個ListenableFuture轉換爲一個ListenableFuture
        //ListenableFuture<List<Integer>> listListenableFuture = Futures.allAsList(futureList);
        //獲取一批任務的執行結果
        List<Integer> resultList = Futures.allAsList(futureList).get();
        //輸出
        resultList.forEach(item -> {
            log.error("{}", item);
        });
    } finally {
        delegate.shutdown();
    }
}

輸出:

15:45:51.160 [main] ERROR com.self.current.GuavaTest - star
15:45:56.185 [main] ERROR com.self.current.GuavaTest - 5
15:45:56.185 [main] ERROR com.self.current.GuavaTest - 4
15:45:56.185 [main] ERROR com.self.current.GuavaTest - 3
15:45:56.185 [main] ERROR com.self.current.GuavaTest - 2
15:45:56.185 [main] ERROR com.self.current.GuavaTest - 1
15:45:56.185 [main] ERROR com.self.current.GuavaTest - 0

示例3:一批任務異步執行完畢以後回調——包裝futureList傳遞給Futures.addCallback 方法

異步執行一批任務,最後計算其和,代碼中異步執行了一批任務,全部任務完成以後,回調了上面的onSuccess方法,內部對全部的結果進行sum操做。

示例:

public static void main(String[] args) throws ExecutionException, InterruptedException {
    log.error("start");
    ExecutorService delegate = Executors.newFixedThreadPool(5);
    try {
        ListeningExecutorService executorService = MoreExecutors.listeningDecorator(delegate);
        List<ListenableFuture<Integer>> futureList = new ArrayList<>();
        for (int i = 5; i >= 0; i--) {
            int j = i;
            futureList.add(executorService.submit(() -> {
                TimeUnit.SECONDS.sleep(j);
                return j;
            }));
        }
        //把多個ListenableFuture轉換爲一個ListenableFuture
        ListenableFuture<List<Integer>> listListenableFuture = Futures.allAsList(futureList);
        Futures.addCallback(listListenableFuture, new FutureCallback<List<Integer>>() {
            @Override
            public void onSuccess(List<Integer> result) {
              log.error("result中全部結果之和:"+result.stream().reduce(Integer::sum).get());
            }

            @Override
            public void onFailure(Throwable throwable) {
                log.error("執行任務發生異常:" + throwable.getMessage(), throwable);
            }
        });
    } finally {
        delegate.shutdown();
    }
}

輸出:

15:57:00.539 [main] ERROR com.self.current.GuavaTest - start
15:57:05.560 [pool-2-thread-1] ERROR com.self.current.GuavaTest - result中全部結果之和:15

其餘疑問:

Q:運行下面這個例子結束不了,debug卻是能夠,這是爲何呢?Thread[Monitor Ctrl-Break,5,main]是哪來的?

public class VolatileTest1 {

    public static volatile int num = 0;

    public  static void add(){
        num++;
    }

    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[10];
        for (Thread thread : threads) {
            thread = new Thread(()->{
                for (int i = 0; i < 1000; i++) {
                    VolatileTest1.add();
                }
            });
            thread.start();
            thread.join();
        }
        //2
        //java.lang.ThreadGroup[name=main,maxpri=10]
        //    Thread[main,5,main]
        //    Thread[Monitor Ctrl-Break,5,main]
        //結束不了,debug卻是能夠,這是爲何呢?Thread[Monitor Ctrl-Break,5,main]是哪來的?
        while (Thread.activeCount() >1){
            Thread.yield();
            System.out.println(Thread.activeCount());
            ThreadGroup parent = Thread.currentThread().getThreadGroup();
            parent.list();
        }
        System.out.println("num="+num);
    }
}
相關文章
相關標籤/搜索