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

卡斯特梅的雨傘發表於2020-09-11

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

參考

java高併發系列

liaoxuefeng Java教程 CompletableFuture

AQS原理沒講,需要找資料補充。

JUC中常見的集合原來沒講,比如ConcurrentHashMap最常用的,後面的都很泛,沒有深入,虎頭蛇尾。

阻塞佇列講得不夠深入。

併發概念詞

同步(Synchronous)和非同步(Asynchronous)

同步和非同步通常來形容一次方法呼叫,同步方法呼叫一旦開始,呼叫者必須等到方法呼叫返回後,才能繼續後續的行為。非同步方法呼叫更像一個訊息傳遞,一旦開始,方法呼叫就會立即返回,呼叫者就可以繼續後續的操作。

舉例:

拿泡泡麵來說,我們把整個泡泡麵的過程分3個步驟:

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

如果我們泡泡麵的時候是按照這三個步驟,等開水開了再加調味加蛋,最後倒開水泡麵,這時候就是同步步驟;而如果我們燒開水的同時把泡麵加調味加蛋準備好,就可以省去燒水的同步等待時間,這就是非同步。

併發(Concurrency)和並行(Parallelism)

併發和並行是兩個非常容易被混淆的概念。他們都可以表示兩個或者多個任務一起執行,但是側重點有所不同。併發偏重於多個任務交替執行,而多個任務之間有可能還是序列的(等待阻塞等),而並行是真正意義上的“同時執行” 。

舉例:

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

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

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

如果系統內只有一個CPU,而使用多程式或者多執行緒任務,那麼真實環境中這些任務不可能是真實並行的,畢竟一個CPU一次只能執行一條指令,在這種情況下多程式或者多執行緒就是併發的,而不是並行的(作業系統會不停地切換多工) 。

臨界區

臨界區用來表示一種公共資源或者說共享資料,可以被多個執行緒使用,但是每一次只能有一個執行緒使用它,一旦臨界區資源被佔用,其他執行緒要想使用這個資源就必須等待。

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

阻塞和非阻塞通常用來形容很多執行緒間的相互影響。比如一個執行緒佔用了臨界區資源,那麼其他所有需要這個資源的執行緒就必須在這個臨界區中等待。等待會導致執行緒掛起,這種情況就是阻塞。 非阻塞的意思與之相反,它強調沒有一個執行緒可以妨礙其他執行緒執行,所有的執行緒都會嘗試不斷向前執行。

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

死鎖、飢餓和活鎖都屬於多執行緒的活躍性問題 。

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

飢餓是指某一個或者多個執行緒因為種種原因無法獲得所要的資源,導致一直無法執行。一種比如它的優先順序可能太低,而高優先順序的執行緒不斷搶佔它需要的資源,導致低優先順序執行緒無法工作。另一種如某一個執行緒一直佔著關鍵資源不放(例子:單執行緒池裡submit一個執行緒任務,而該執行緒又往該單執行緒池裡submit一個新的任務並等待結果返回,因為執行緒池是單執行緒池,所以便一種套娃著),導致其他需要這個資源的執行緒無法正常執行,這種情況也是飢餓的一種。與死鎖相比,飢餓還是有可能在未來一段時間內解決的(比如,高優先順序的執行緒已經完成任務,不再瘋狂執行)。

活鎖:當兩個執行緒都秉承著“謙讓”的原則(導致死迴圈),主動將資源釋放給他人使用,那麼就會導致資源不斷地在兩個執行緒間跳動,而沒有一個執行緒可以同時拿到所有資源正常執行。這種情況就是活鎖。

擴充套件

通過jstack檢視到死鎖資訊

1、使用jps找到執行程式碼的程式ID,啟動類名為DeadLockTest(main函式所在類)的程式ID為11084 
jps
2、通過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需要等待T2、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種方式

1、通過一個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");
        }
    }
}

2、通過執行緒自帶的中斷標誌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秒(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):喚醒處於阻塞狀態的指定執行緒

1、LockSupport呼叫park、unpark方法執行喚醒等待無需加鎖。

2、LockSupport中,喚醒的方法不管是在等待之前還是在等待之後呼叫,執行緒都能夠被喚醒。 喚醒方法在等待方法之前執行,執行緒也能夠被喚醒,這點是另外兩種方法無法做到的。而Object和Condition中的喚醒必須在等待之後呼叫,執行緒才能被喚醒。

3、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();會讓主執行緒等待,t1、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);
    }
}

相關文章