­

關於Java高並發編程你需要知道的「升段攻略」

關於Java高並發編程你需要知道的「升段攻略」

基礎

  1. Thread對象調用start()方法包含的步驟

    1. 通過jvm告訴作業系統創建Thread
    2. 作業系統開闢記憶體並使用Windows SDK中的createThread()函數創建Thread執行緒對象
    3. 作業系統對Thread對象進行調度,以確定執行時機
    4. Thread在作業系統中被成功執行
  2. 執行start的順序不代表執行run的順序

  3. 執行方法run和start有區別

    1. xxx.run():立即執行run()方法,不啟動新的執行緒
    2. xxx.start():執行run()方法時機不確定,啟動新的執行緒
  4. 執行緒停止:

    1. public static boolean interrupted():測試當前執行緒是否已經是中斷狀態,執行後具有清除狀態標誌值的功能

    2. public boolean this.isInterrupted():測試當前執行緒是否已經是中斷狀態,不清除狀態標誌

    3. 停止執行緒-異常

    4. 在sleep下停止執行緒,當sleep和interrupt同時出現時,會報錯

    5. stop終止執行緒

    6. return停止執行緒

    7. 暫停執行緒

      ​ suspend 和 resume獨佔

  5. synchronized原理:

    1. 方法:使用ACC_SYNCHRONIZED標記
    2. 程式碼塊:使用monitorentermonitorexit指令
  6. 靜態同步synchronized方法與synchronized(class)程式碼塊,同步synchronized方法與synchronized(this)程式碼塊

  7. volatile關鍵字特性:

    1. 可見性:一個執行緒修改共享變數的值,其他執行緒立馬就知道
    2. 原子性:double和long數據類型具有原子性,針對volatile聲明變數的自增不具有原子性
    3. 禁止程式碼重排序
  8. 利用volatile關鍵字解決多執行緒出現的死循環(可見性),本質是因為執行緒的私有堆棧和公有堆棧不同步

  9. synchronized程式碼塊具有增加可見性的作用

  10. 自增/自減操作的步驟

    1. 從記憶體中取值
    2. 計算值
    3. 將值寫入記憶體
  11. 使用Atomic原子類進行i++操作實現原子性

  12. sychronized程式碼塊和volatile關鍵字都禁止程式碼重排序:位於程式碼塊/關鍵字兩側的不能互相重排序,只能各自在前面或者後面重排序

  13. wait/notify機制

    1. 使用前都必須獲得鎖,即必須位於synchronized修飾的方法內或者程式碼塊內,且必須是同一把鎖
    2. 使用wait後會釋放鎖,當調用notify後,執行緒再次獲得鎖並執行
    3. wait執行後會立即釋放鎖,而notify執行後不會立即讓出鎖,而是等到執行notify方法的執行緒將程式執行完以後才讓出鎖
    4. wait使執行緒暫停運行,notify使執行緒繼續運行
    5. 處於wait狀態下的執行緒會一直等待
    6. 在調用notify時若沒有處於wait狀態的執行緒,命令會被忽略
    7. 當調用了多個wait方法,同時需要多個notify方法時,所有等待的wait狀態執行緒獲取鎖的順序是執行wait方法的順序,即先調用先獲得鎖
    8. notifyAll方法可以一次通知所有的處理wait狀態的執行緒,但是這些執行緒獲得鎖的順序是先調用的最後獲得,後調用的先獲得,通常還有其他的順序,取決於jvm的具體實現
  14. wait立即釋放鎖,notify不立即釋放鎖,sleep不會釋放鎖

  15. wait與interrupt方法同時出現會出現異常

  16. wait(long)表示等待一段時間後,再次獲取鎖,不需要notify方法通知,若沒有獲取鎖則會一直等待,直到獲取鎖為止

  17. if+wait建議替換成while+wait

  18. 執行緒間通過管道通訊(字元流/位元組流):PipInputStream/PipOutputStream、PipWriter/PipReader

  19. join方法是所屬的執行緒對象x正常執行run()方法中的任務,而使當前執行緒進行無期限的阻塞,等待執行緒x銷毀後再繼續執行執行緒z後面的程式碼,具有串聯執行的效果

  20. join方法具有使執行緒排隊運行的效果,有類似同步的運行效果,但是join方法與synchronized的區別是join方法內部使用wait方法進行等待(會釋放鎖),而synchronized關鍵字使用鎖作為同步

  21. join與interrupt同時出現會出現異常

  22. join(long)執行後會調用內部的wait(long)會釋放鎖,而Thread.sleep(long)則不會釋放鎖

  23. ThreadLocal

    1. 將數據放入到當前執行緒對象中的Map中,這個Map是Thread類的實例變數。類ThreadLocal自己不管理、不存儲任何數據,它只是數據和Map之間的橋樑,用於將數據放入Map中,執行流程如下:數據–>ThreadLocal–>currentThread() –>Map

    2. 執行後每個執行緒中的Map存有自己的數據,Map中的key存儲的是ThreadLocal對象,value就是存儲的值。每個Thread中的Map值只對當前執行緒可見,其他執行緒不可以訪問。當前執行緒銷毀,Map隨之銷毀,Map中的數據如果沒有被引用、沒有被使用,則隨時GC回收

    3. 執行緒、Map、數據之間的關係可以做如下類比:

      人(Thread)隨身帶有兜子(Map),兜子(Map)裡面有東西(Value),這樣,Thread隨身也有數據,隨時可以訪問自己的數據

    4. 可以通過繼承類,並重寫initialValue()來解決get()返回null的問題

    5. Thread本身不能實現子執行緒使用父執行緒的值,但是使用InheritableLocalThread則可以訪問,不過,不能實現同步更新值,也就是說子執行緒更新了值,父執行緒中還是舊值,父執行緒更新了值,子執行緒中還是舊值

    6. 通過重寫InheritableLocalThread的childValue可以實現子執行緒對父執行緒繼承的值進行加工修改

  24. Java多執行緒可以使用synchronized關鍵字來實現執行緒同步,不過jdk1.5新增加的ReentrantLock類可能達到同樣的效果,並且在擴展功能上更加強大,如具有嗅探鎖定、多路分支通知等功能

  25. 關鍵字synchronized與wait()、notify()/notifyAll()方法相結合可以實現wait/notify模型,ReentrantLock類也可以實現同樣的功能,但需要藉助於Condition對象。Condition類是jdk5的技術,具有更好的靈活性,例如,可以實現多路通知功能,也就是在一個Lock對象中可以創建多個Condition實例,執行緒對象註冊在指定的Condition中,從而可以有選擇性的進行執行緒通知,在調度執行緒上更加靈活;在使用notify()/notifyAll()方法進行通知時,被通知的執行緒有jvm進行選擇,而方法notifyAll()會通知所有waiting執行緒,沒有選擇權,會出現相當大的效率問題,但使用ReentrantLock結合Condition類可以實現「選擇性通知」,這個功能是Condition默認提供的

  26. Condition類

    1. Condition對象的創建是使用ReentrantLoack的newCondition方法創建的
    2. Condition對象的作用是控制並處理執行緒的狀態,它可以使執行緒呈wait狀態,也可以讓執行緒繼續運行
    3. 通過Condition中的await/signal可以實現wait/notify一樣的效果
      1. Object中的wait方法相當於Condition類中的await方法
      2. Object中的wait(long timeout)方法相當於Condition類中的await(long time, TimeUnit unit)方法
      3. Object中的notify方法相當於Condition類中的signal1方法
      4. Object類中的notifyAll方法相當於Condition類中的signalAll方法
    4. await方法暫停執行緒的原理
      1. 並發包內部執行了unsafe類中的public native void park(boolean isAbsolute, long time)方法,讓當前執行緒呈暫停狀態,方法參數isAbsolute代表是否為絕對時間
    5. 公平鎖:採用先到先得的鎖,每次獲取之前都會檢查隊列裡面有沒有排隊等待的執行緒,沒有才嘗試獲取鎖,如果有就將當前執行緒追加到隊列中
    6. 非公平鎖:採用「有機會插隊」的策略,一個執行緒獲取之前要先嘗試獲取鎖而不是在隊列中等待,如果獲取成功,則說明執行緒雖然是啟動的,但先獲得了鎖,如果沒有獲取成功,就將自身添加到隊列中進行等待
    7. 執行緒執行lock方法後內部執行了unsafe.park(false, 0L)程式碼;執行緒執行unlock方法後內部執行unsafe.unpark(bThread)方法
    8. public int getHoldCount()查詢「當前執行緒」保持鎖定的個數,即調用lock方法的次數
    9. public final int getQueueLength()返回正等待獲取此鎖的執行緒估計數,例如,這裡有5個執行緒,其中1個執行緒長時間佔有鎖,那麼調用getQueueLength()方法後,其返回值是4,說明有4個執行緒同時在等待鎖的釋放
    10. public int getWaitQueueLength()返回等待與此鎖有關的給定條件Condition的執行緒統計數。例如,有5個執行緒,每個執行緒都執行了同一個Condition對象的await()方法,則調用getWaitQueueLength()方法時,其返回的值是5
    11. public final boolean hasQueueThread(Thread thread)查詢指定的執行緒是否正在等待獲取此鎖,也就是判斷參數中的執行緒是否在等待隊列中
    12. public final boolean hasQueueThreads()查詢是否有執行緒正在等待獲取此鎖,也就是等待隊列中是否有等待的執行緒
    13. public boolean hasWaiters(Condition condition)查詢是否有執行緒正在等待與此鎖有關的condition條件,也就是是否有執行緒執行了condition對象中的await方法而呈等待狀態。而public int getWaitQueueLength(Condition condition)方法是返回有多少個執行緒執行了condition對象中的await方法而呈等待狀態
    14. public final boolean isFair()判斷是不是公平鎖
    15. public boolean isHeldByCurrentThread()是查詢當前執行緒是否保持此鎖
    16. public boolean isLocked()查詢此鎖是否由任意執行緒保持,並沒有釋放
    17. public void lockInterruptibly()當某個執行緒嘗試獲取鎖並且阻塞在lockInterruptibly()方法時,該執行緒可以被中斷
    18. public boolean tryLock()嗅探拿鎖,如果當前執行緒發現鎖被其他執行緒持有,則返回false,程式繼續執行後面的程式碼,而不是呈阻塞等待鎖的狀態
    19. public boolean tryLock(long timeout, TimeUnit unit)嗅探拿鎖,如果當前執行緒發現鎖被其他執行緒持有了,則返回false,程式繼續執行後面的程式碼,而不是呈阻塞等待狀態。如果當前執行緒在指定的timeout內持有了鎖,則返回值是true,超過時間則返回false。參數timeout代表當前執行緒搶鎖的時間
    20. public boolean await(long time, TimeUnit unit)public final native void wait(long timeout)方法一樣,都具有自動喚醒執行緒的功能
    21. public long awaitNanos(long nanoTimeout)public final native void wait(long timeout)方法一樣,都具有自動喚醒執行緒的功能,時間單位是納秒(ns)。
    22. public boolean awaitUntil(Date deadline)在指定的Date結束等待
    23. public void awaitUninterruptibly()在實現執行緒在等待的過程中,不允許被中斷
  27. ReentrantReadWriteLock類

    1. ReentrantLock類具有完全互斥排他的效果,同一時間只有一個執行緒在執行ReentrantLock.lock()方法後面的任務,這樣做雖然保證了同時寫實例變數的執行緒安全性,但效率非常低下,所以jdk提供了一種讀寫鎖——ReentrantReadWriteLock類,使用它可以在進行讀操作時不需要同步執行,提升運行速度,加快運行效率
    2. 讀寫鎖有兩個鎖:一個是讀操作相關的鎖,也稱共享鎖;另一個是寫操作相關的鎖,也稱排他鎖
    3. 讀寫互斥,寫讀互斥,寫寫互斥,讀讀非同步
  28. Timer定時器

    1. 定時/計劃任務功能在Java中主要使用Timer對象實現,它在內部使用多執行緒的方式進行處理,所以它和執行緒技術有很大的關聯
    2. 在指定的日期執行任務:schedule(TimerTask task, Date firstTime, long period)
    3. 按指定周期執行任務:schedule(TimerTask task, long delay, long period)
    4. 注意:在創建Timer對象時會創建一個守護執行緒,需要手動關閉,有兩種方法
      1. new Timer().concel():將任務隊列中的任務全部清空
      2. new TimerTask().concel():僅將自身從任務隊列中清空
    5. scheduleAtFixedRate()具有追趕性,也就是若任務的啟動時間早於當前時間,那麼它會將任務在這個時間差內按照任務執行的規律執行一次,相當於「彌補」流逝的時間
    6. 但timer隊列中有多個任務時,這些任務執行順序的演算法是每次將最後一個任務放入隊列頭,再執行隊列頭TimerTask任務的run方法
  29. 單例模式與多執行緒

    1. 餓漢式
    2. 懶漢式
      1. 使用DCL(雙重檢查鎖)來實現(volatile+synchronized)

進階

並發編程的進階問題

  1. 並發執行程式一定比串列執行程式快嗎?

    答:不是的。因為執行緒有創建和上下文切換的開銷,所以說,在某種情況下線並發執行程式會比串列執行程式慢

  2. 上下文切換:cpu通過時間片分配演算法來循環執行任務,當前任務執行一個時間片後會切換到下一個任務。但是,在切換前會保存是一個任務的狀態,以便於下次切換回這個任務時,可以再載入這個任務的狀態,故任務從保存到再載入的過程就是一次上下文切換。這就像我們同時讀兩本書,當我們在第一本英文的技術書時,發現某個單詞不認識,於是便打開英文字典,但是放下英文技術書之前,大腦必須記住這本書讀到了多少頁的多少行,等查完單詞後,能夠繼續讀這本書。這樣的切換是會影響讀書效率的,同樣上下文切換也會影響多執行緒的執行速度

  3. 減少上下文切換:

    1. 無鎖並發編程。多執行緒競爭鎖時,會引起上下文切換,所以多執行緒處理數據時,可以用一些辦法來避免使用鎖,如將數據的id按照Hash演算法取模分段,不同的執行緒處理不同段的數據
    2. CAS演算法。Java的Atomic包使用CAS演算法來更新數據,而不需要加鎖
    3. 使用最少執行緒。避免創建不需要的執行緒,比如任務少,但是創建了很多執行緒來處理,這樣會造成大量執行緒都處於等待狀態
    4. 協程:在單執行緒里實現多任務的調度,並在單執行緒里維持多個任務間的切換

Java並發機制的底層實現原理

關於volatile

  1. Java程式碼在編譯後會變成Java位元組碼,位元組碼被類載入器載入到jvm里,jvm執行位元組碼,最終需要轉化為彙編指令在CPU上執行,Java中所有的並發機制依賴於jvm的實現和CPU的命令
  2. volatile是輕量級的synchronized,它在多處理器開發中保證了共享變數的「可見性」。可見性的意思是當一個執行緒修改一個共享變數時另外一個執行緒能讀到這個修改的值。如果volatile變數修飾符使用恰當的話,它比synchronized的使用和執行成本更低,因為它不會引起執行緒上下文的切換和調度
  3. volatile的定義:Java程式語言允許執行緒訪問共享變數,為了確保共享變數能被準確和一致地更新,執行緒應該確保排他鎖單獨獲得這個變數
  4. 相關CPU的術語定義
    1. 記憶體屏障(memory barries):是一組處理器指令,用於實現對記憶體操作的順序限制
    2. 緩衝行(cache line):快取中可以分配的最小儲存單元。處理器填寫快取線時會載入整個快取線,需要使用多個主記憶體讀周期
    3. 原子操作(atomic operations):不可中斷的一個或一系列操作
    4. 快取行填充(cache line fill):當處理器識別到從記憶體中讀取操作數是可快取的,處理器讀取整個快取行到適當的快取
    5. 快取命中(cache hit):如果進行高速快取填充操作的記憶體位置仍然是下次處理器訪問的地址時,處理器從快取中讀取數據,而不是從記憶體讀取
    6. 寫命中(write hit):當處理器將操作數寫回到一個記憶體快取的區域時,它首先會檢查這個快取的記憶體地址是否在快取行中,如果存在一個有效的快取行,則處理器將這個操作數寫回到快取,而不是寫回到記憶體
    7. 寫缺失(write misses the cache):一個有效的快取行被寫入到不存在的記憶體區域
  5. volatile底層通過使用lock前綴的指令來保證可見性,lock前綴的指令在多核處理器下會引發兩件事情:
    1. 將當前處理器快取行的數據寫回到系統記憶體
    2. 這個寫回記憶體操作會使其他CPU里快取了該記憶體地址的數據無效

原子操作

原子操作(atomic operation):不可中斷的一個或一系列操作

  1. 相關的CPU術語
    1. 快取行(cache line):快取的最小操作單位
    2. 比較並交換(compare and swap):CAS操作需要輸入兩個數值,一個舊值和新值,在操作期間先比較舊值有沒有發生變化,若沒有發生變化才交換成新值,發生了變化則不交換
    3. CPU流水線(CPU pipeline):CPU流水線的工作方式就像工業生產上的裝配流水線,在CPU中由56個不同功能的電路單元組成一條指令處理流水線,然後將x86指令分成56步後再由這些電路單元分別執行,這樣就能實現在一個CPU時鐘周期完成一條指令,因此提高CPU的運算速度
    4. 記憶體順序衝突(memory order violation):記憶體順序衝突一般是由假共享引起的,假共享是指多個CPU同時修改同一個快取行的不同部分而引起其中一個CPU的操作無效,當出現這個記憶體順序衝突時,CPU必須清空流水線
  2. 處理器提供匯流排鎖定和快取鎖定兩個機制來保證複雜記憶體操作的原子性
    1. 匯流排鎖就是使用處理器提供的一個LOCK#訊號,當一個處理器在匯流排上輸出此訊號時,其他處理器的請求被阻塞住,該處理器獨佔記憶體。例如:當多個執行緒執行i++操作時,多個處理器同時從各自的快取中讀取變數i,分別進行加1操作,然後分別寫入系統記憶體中。那麼,想要保證讀改寫共享變數的操作是原子的,就必須保證CPU1讀改寫共享變數的時候,CPU2不能操作快取了該共享變數記憶體地址的快取
    2. 頻繁使用的記憶體會快取在處理器的L1、L2和L3高速快取里,那麼原子操作就可以直接在處理器內部快取中進行,並不需要聲明匯流排鎖。快取鎖定是指記憶體區域如果被快取在處理器的快取行中,並且在LOCK期間被鎖定,那麼但它執行鎖操作回寫到記憶體時,處理器不在匯流排上聲言LOCK#訊號,而是修改內部的記憶體地址,並允許它的快取一致機制來保證操作的原子性,因為快取一致性機制會阻止同時修改由兩個以上處理器快取的記憶體區域數據,當其他處理器回寫已被鎖定的快取行的數據時,會使快取行無效。當CPU1修改快取行中的i時使用了快取鎖定,那麼CPU2不能同時快取i的快取行
    3. 兩種情況不會使用快取鎖定
      1. 當操作的數據不能被快取在處理器內部,或操作的數據跨多個快取行時,則處理器會調用匯流排鎖定
      2. 有些處理器不支援快取鎖定
  3. Java實現原子操作的方式:鎖和循環CAS
    1. jvm中的CAS操作正是利用了處理器提供的CMPXCHG指令實現的。自旋CAS實現的基本思路就是循環進行CAS操作直接成功為止
    2. 鎖機制保證了只有獲得鎖的執行緒才能夠操作鎖定的記憶體區域。jvm內部實現了很多種鎖,有偏向鎖、輕量級鎖和互斥鎖。除了偏向鎖,jvm實現鎖的方式都用了循環CAS,即當一個執行緒想進入同步塊的時候使用循環CAS的方式來獲取鎖,當它退出同步塊的時候使用循環CAS釋放鎖

Java記憶體模型(Java Memory Module,JMM)

Java記憶體模型基礎
  1. 在並發編程中,需要處理兩個問題:執行緒之間如何通訊及執行緒之間如何同步。通訊是指執行緒之間以何種機制來交換資訊。在命令式編程中,執行緒之間的通訊機制有兩種:共享記憶體和資訊傳遞
  2. 在共享記憶體的並發模型里,執行緒之間共享程式的公共狀態,通過寫—讀記憶體中的公共狀態進行隱式通訊。在消息傳遞的並發模型里,執行緒之間沒有公共狀態,執行緒之間必須通過發送消息來顯示進行通訊
  3. 同步是指程式中用於控制不同執行緒間操作發生相對順序的機制。在消息傳遞的並發模型里,由於消息的發送必須在消息的接受之前,因此同步是隱式進行的
Java記憶體結構的抽象結構
  1. 在Java中,所有實例域、靜態域和數組元素都存儲在堆記憶體中,堆記憶體在執行緒之間共享。局部變數(Local Variables),方法定義參數(Java語言規範稱之為Formal Method Parameters)和異常處理器參數(ExceptionHandler Parameters)不會在執行緒之間共享,它們不會有記憶體可見性問題,也不受記憶體模型的影響

  2. Java執行緒之間的通訊由Java記憶體模型控制,JMM決定一個執行緒對共享變數的寫入何時對另一個執行緒可見。從抽象的角度來看,JMM定義了執行緒和主記憶體之間的抽象關係:執行緒之間的共享變數存儲在主記憶體(Main Memory)中,每個執行緒都有一個私有的本地記憶體(Local Memory),本地記憶體中存儲了該執行緒以讀/寫共享變數的副本。本地記憶體是JMM的一個抽象概念,並不真實存在。它涵蓋了快取、寫緩衝區、暫存器以及其他的硬體和編譯器優化

  1. 如果執行緒A與執行緒B之間要通訊的話,必須要經歷下面2個步驟。

    1. 執行緒A把本地記憶體A中更新過的共享變數刷新到主記憶體中去。
    2. 執行緒B到主記憶體中去讀取執行緒A之前已更新過的共享變數

本地記憶體A和本地記憶體B由主記憶體中共享變數x的副本。假設初始時,這3個記憶體中的x值都為0。執行緒A在執行時,把更新後的x值(假設值為1)臨時存放在自己的本地記憶體A中。當執行緒A和執行緒B需要通訊時,執行緒A首先會把自己本地記憶體中修改後的x值刷新到主記憶體中,此時主記憶體中的x值變為了1。隨後,執行緒B到主記憶體中去讀取執行緒A更新後的x值,此時執行緒B的本地記憶體的x值也變為了1

從整體來看,這兩個步驟實質上是執行緒A在向執行緒B發送消息,而且這個通訊過程必須要經過主記憶體。JMM通過控制主記憶體與每個執行緒的本地記憶體之間的交互,來為Java程式設計師提供記憶體可見性保證

從源程式碼到指令序列的重排序

在執行程式時,為了提高性能,編譯器和處理器常常會對指令做重排序。重排序分3種類型

  1. 編譯器優化的重排序。編譯器在不改變單執行緒程式語義的前提下,可以重新安排語句的執行順序
  2. 指令級並行的重排序。現代處理器採用了指令級並行技術(Instruction-Level Parallelism,ILP)來將多條指令重疊執行。如果不存在數據依賴性,處理器可以改變語句對應機器指令的執行順序
  3. 記憶體系統的重排序。由於處理器使用快取和讀/寫緩衝區,這使得載入和存儲操作看上去可能是在亂序執行

上述的1屬於編譯器重排序,2和3屬於處理器重排序。這些重排序可能會導致多執行緒程式出現記憶體可見性問題。對於編譯器,JMM的編譯器重排序規則會禁止特定類型的編譯器重排序(不是所有的編譯器重排序都要禁止)。對於處理器重排序,JMM的處理器重排序規則會要求Java編譯器在生成指令序列時,插入特定類型的記憶體屏障(Memory Barriers,Intel稱之為Memory Fence)指令,通過記憶體屏障指令來禁止特定類型的處理器重排序

JMM屬於語言級的記憶體模型,它確保在不同的編譯器和不同的處理器平台之上,通過禁 止特定類型的編譯器重排序和處理器重排序,為程式設計師提供一致的記憶體可見性保證

並發編程模型分類
  1. 現代的處理器使用寫緩衝區臨時保存向記憶體寫入的數據。寫緩衝區可以保證指令流水線持續運行,它可以避免由於處理器停頓下來等待向記憶體寫入數據而產生的延遲。同時,通過以批處理的方式刷新寫緩衝區,以及合併寫緩衝區中對同一記憶體地址的多次寫,減少對記憶體匯流排的佔用。雖然寫緩衝區有這麼多好處,但每個處理器上的寫緩衝區,僅僅對它所在的處理器可見。這個特性會對記憶體操作的執行順序產生重要的影響:處理器對記憶體的讀/寫操作的執行順序,不一定與記憶體實際發生的讀/寫操作順序一致!

  2. 為了保證記憶體可見性,Java編譯器在生成指令序列的適當位置會插入記憶體屏障指令來禁止特定類型的處理器重排序。JMM把記憶體屏障指令分為4類 :

  1. StoreLoad Barriers是一個「全能型」的屏障,它同時具有其他3個屏障的效果。現代的多處理器大多支援該屏障(其他類型的屏障不一定被所有處理器支援)。執行該屏障開銷會很昂貴,因為當前處理器通常要把寫緩衝區中的數據全部刷新到記憶體中(Buffer Fully Flush)。
happens-before
  1. 在JMM中,如果一個操作執行的結果需要對另一個操作可見,那麼這兩個操作之間必須要存在happens-before關係。這裡提到的兩個操作既可以是在一個執行緒之內,也可以是在不同執行緒之間
  2. happens-before原則如下:
    1. 程式順序規則:一個執行緒中的每個操作,happens-before於該執行緒中的任意後續操作
    2. 監視器鎖規則:對一個鎖的解鎖,happens-before於隨後對這個鎖的加鎖
    3. volatile變數規則:對一個volatile域的寫,happens-before於任意後續對這個volatile域的讀
    4. 傳遞性:如果A happens-before B,且B happens-before C,那麼A happens-before C。
重排序
  1. 重排序是指編譯器和處理器為了優化程式性能而對指令序列進行重新排序的一種手段

  2. 如果兩個操作訪問同一個變數,且這兩個操作中有一個為寫操作,此時這兩個操作之間就存在數據依賴性。存在數據依賴性的操作不能重排序

  3. as-if-serial語義

    1. as-if-serial語義的意思是:不管怎麼重排序(編譯器和處理器為了提高並行度),(單執行緒)程式的執行結果不能被改變。編譯器、runtime和處理器都必須遵守as-if-serial語義。
    2. 為了遵守as-if-serial語義,編譯器和處理器不會對存在數據依賴關係的操作做重排序,因為這種重排序會改變執行結果。但是,如果操作之間不存在數據依賴關係,這些操作就可能被編譯器和處理器重排序
    3. as-if-serial語義把單執行緒程式保護了起來,遵守as-if-serial語義的編譯器、runtime和處理器共同為編寫單執行緒程式的程式設計師創建了一個幻覺:單執行緒程式是按程式的順序來執行的。asif-serial語義使單執行緒程式設計師無需擔心重排序會干擾他們,也無需擔心記憶體可見性問題
    4. 在電腦中,軟體技術和硬體技術有一個共同的目標:在不改變程式執行結果的前提下,儘可能提高並行度
    5. 在單執行緒程式中,對存在控制依賴的操作重排序,不會改變執行結果(這也是as-if-serial語義允許對存在控制依賴的操作做重排序的原因);但在多執行緒程式中,對存在控制依賴的操作重排序,可能會改變程式的執行結果
  4. 順序一致性

    1. 順序一致性記憶體模型是一個理論參考模型,在設計的時候,處理器的記憶體模型和程式語言的記憶體模型都會以順序一致性記憶體模型作為參照

    2. 如果程式是正確同步的,程式的執行將具有順序一致性(Sequentially Consistent)——即程式的執行結果與該程式在順序一致性記憶體模型中的執行結果相同

    3. 順序一致性記憶體模型有兩大特性 :

      1. 一個執行緒中的所有操作必須按照程式的順序來執行

      2. (不管程式是否同步)所有執行緒都只能看到一個單一的操作執行順序。在順序一致性記憶體模型中,每個操作都必須原子執行且立刻對所有執行緒可見

      3. 在概念上,順序一致性模型有一個單一的全局記憶體,這個記憶體通過一個左右擺動的開關可以連接到任意一個執行緒,同時每一個執行緒必須按照程式的順序來執行記憶體讀/寫操作。從上
        面的示意圖可以看出,在任意時間點最多只能有一個執行緒可以連接到記憶體。當多個執行緒並發執行時,圖中的開關裝置能把所有執行緒的所有記憶體讀/寫操作串列化(即在順序一致性模型中,所有操作之間具有全序關係)

      4. 對於未同步或未正確同步的多執行緒程式,JMM只提供最小安全性:執行緒執行時讀取到的值,要麼是之前某個執行緒寫入的值,要麼是默認值(0,Null,False),JMM保證執行緒讀操作讀取到的值不會無中生有(Out Of Thin Air)的冒出來。為了實現最小安全性,JVM在堆上分配對象時,首先會對記憶體空間進行清零,然後才會在上面分配對象(JVM內部會同步這兩個操作)。因此,在已清零的記憶體空間(Pre-zeroed Memory)分配對象時,域的默認初始化已經完成了

      5. 在電腦中,數據通過匯流排在處理器和記憶體之間傳遞。每次處理器和記憶體之間的數據傳遞都是通過一系列步驟來完成的,這一系列步驟稱之為匯流排事務(Bus Transaction)。匯流排事務包括讀事務(Read Transaction)和寫事務(Write
        Transaction)。讀事務從記憶體傳送數據到處理器,寫事務從處理器傳送數據到記憶體,每個事務會讀/寫記憶體中一個或多個物理上連續的字。這裡的關鍵是,匯流排會同步試圖並發使用匯流排的事務。在一個處理器執行匯流排事務期間,匯流排會禁止其他的處理器和I/O設備執行記憶體的讀/寫

volatile的記憶體語義
  1. 鎖的happens-before規則保證釋放鎖和獲取鎖的兩個執行緒之間的記憶體可見性,這意味著對一個volatile變數的讀,總是能看到(任意執行緒)對這個volatile變數最後的寫入
  2. volatile變數自身具有下列特性:
    1. 可見性:對一個volatile變數的讀,總是能看到(任意執行緒)對這個volatile變數最後的寫入
    2. 原子性:對任意單個volatile變數的讀/寫具有原子性,但類似於volatile++這種複合操作不具有原子性
  3. 從記憶體語義的角度來說,volatile的寫-讀與鎖的釋放-獲取有相同的記憶體效果:volatile寫和鎖的釋放有相同的記憶體語義;volatile讀與鎖的獲取有相同的記憶體語義
  4. volatile寫的記憶體語義:當寫一個volatile變數時,JMM會把該執行緒對應的本地記憶體中的共享變數值刷新到主記憶體
  5. volatile讀的記憶體語義:當讀一個volatile變數時,JMM會把該執行緒對應的本地記憶體置為無效。執行緒接下來將從主記憶體中讀取共享變數
  6. 如果我們把volatile寫和volatile讀兩個步驟綜合起來看的話,在讀執行緒B讀一個volatile變數後,寫執行緒A在寫這個volatile變數之前所有可見的共享變數的值都將立即變得對讀執行緒B可見
  7. 對volatile寫和volatile讀的記憶體語義做個總結:
    1. 執行緒A寫一個volatile變數,實質上是執行緒A向接下來將要讀這個volatile變數的某個執行緒發出了(其對共享變數所做修改的)消息。
    2. 執行緒B讀一個volatile變數,實質上是執行緒B接收了之前某個執行緒發出的(在寫這個volatile變數之前對共享變數所做修改的)消息。
    3. 執行緒A寫一個volatile變數,隨後執行緒B讀這個volatile變數,這個過程實質上是執行緒A通過主記憶體向執行緒B發送消息
  8. 為了實現volatile記憶體語義,JMM會分別限制這兩種類型的重排序類型
    1. 由表得出:

      1. 當第二個操作是volatile寫時,不管第一個操作是什麼,都不能重排序。這個規則確保volatile寫之前的操作不會被編譯器重排序到volatile寫之後。
      2. 當第一個操作是volatile讀時,不管第二個操作是什麼,都不能重排序。這個規則確保volatile讀之後的操作不會被編譯器重排序到volatile讀之前。
      3. 當第一個操作是volatile寫,第二個操作是volatile讀時,不能重排序
  9. 為了實現volatile的記憶體語義,編譯器在生成位元組碼時,會在指令序列中插入記憶體屏障來禁止特定類型的處理器重排序。對於編譯器來說,發現一個最優布置來最小化插入屏障的總數幾乎不可能。為此,JMM採取保守策略。下面是基於保守策略的JMM記憶體屏障插入策略:
    1. 在每個volatile寫操作的前面插入一個StoreStore屏障
    2. 在每個volatile寫操作的後面插入一個StoreLoad屏障
    3. 在每個volatile讀操作的後面插入一個LoadLoad屏障
    4. 在每個volatile讀操作的後面插入一個LoadStore屏障
鎖的記憶體語義
  1. 鎖是Java並發編程中最重要的同步機制。鎖除了讓臨界區互斥執行外,還可以讓釋放鎖的執行緒向獲取同一個鎖的執行緒發送消息

  2. 鎖的釋放和獲取的記憶體語義

    1. 當執行緒釋放鎖時,JMM會把該執行緒對應的本地記憶體中的共享變數刷新到主記憶體中
    2. 當執行緒獲取鎖時,JMM會把該執行緒對應的本地記憶體置為無效
  3. 對鎖釋放和鎖獲取的記憶體語義做個總結:

    1. 執行緒A釋放一個鎖,實質上是執行緒A向接下來將要獲取這個鎖的某個執行緒發出了(執行緒A對共享變數所做修改的)消息
    2. 執行緒B獲取一個鎖,實質上是執行緒B接收了之前某個執行緒發出的(在釋放這個鎖之前對共享變數所做修改的)消息
    3. 執行緒A釋放鎖,隨後執行緒B獲取這個鎖,這個過程實質上是執行緒A通過主記憶體向執行緒B發送消息
  4. 鎖記憶體語義的實現

    1. 利用volatile變數的寫-讀所具有的記憶體語義
    2. 利用CAS所附帶的volatile讀和volatile寫的記憶體語義
final域的記憶體語義
  1. 對於final域,編譯器和處理器要遵守兩個重排序規則:

    1. 在構造函數內對一個final域的寫入,與隨後把這個被構造對象的引用賦值給一個引用變數,這兩個操作之間不能重排序
    2. 初次讀一個包含final域的對象的引用,與隨後初次讀這個final域,這兩個操作之間不能重排序
  2. 寫final域的重排序規則

    1. 寫final域的重排序規則禁止把final域的寫重排序到構造函數之外
      1. JMM禁止編譯器把final域的寫重排序到構造函數之外
      2. 編譯器會在final域的寫之後,構造函數return之前,插入一個StoreStore屏障。這個屏障禁止處理器把final域的寫重排序到構造函數之外
      3. 簡單的來說就是構造函數可以分批次初始化,final域要最先初始化
    2. 寫final域的重排序規則可以確保:在對象引用為任意執行緒可見之前,對象的final域已經被正確初始化過了,而普通域不具有這個保障
  3. 讀final域的重排序規則

    1. 讀final域的重排序規則是,在一個執行緒中,初次讀對象引用與初次讀該對象包含的final域,JMM禁止處理器重排序這兩個操作(注意,這個規則僅僅針對處理器)。編譯器會在讀final域操作的前面插入一個LoadLoad屏障
    2. 初次讀對象引用與初次讀該對象包含的final域,這兩個操作之間存在間接依賴關係。由於編譯器遵守間接依賴關係,因此編譯器不會重排序這兩個操作。大多數處理器也會遵守間接依賴,也不會重排序這兩個操作。但有少數處理器允許對存在間接依賴關係的操作做重排序(比如alpha處理器),這個規則就是專門用來針對這種處理器的
    3. 讀final域的重排序規則可以確保:在讀一個對象的final域之前,一定會先讀包含這個final域的對象的引用
  4. 當final域為引用類型,寫final域的重排序規則對編譯器和處理器增加了如下約束:在構造函數內對一個final引用的對象的成員域的寫入,與隨後在構造函數外把這個被構造對象的引用賦值給一個引用變數,這兩個操作之間不能重排序

  5. happens-before

    1. 首先,讓我們來看JMM的設計意圖。從JMM設計者的角度,在設計JMM時,需要考慮兩個關鍵因素:
      1. 程式設計師對記憶體模型的使用。程式設計師希望記憶體模型易於理解、易於編程。程式設計師希望基於一個強記憶體模型來編寫程式碼
      2. 編譯器和處理器對記憶體模型的實現。編譯器和處理器希望記憶體模型對它們的束縛越少越好,這樣它們就可以做儘可能多的優化來提高性能。編譯器和處理器希望實現一個弱記憶體模型
    2. 由於這兩個因素互相矛盾,所以JSR-133專家組在設計JMM時的核心目標就是找到一個好的平衡點:一方面,要為程式設計師提供足夠強的記憶體可見性保證;另一方面,對編譯器和處理。因此,JMM把happens-before要求禁止的重排序分為了下面兩類。 器的限制要儘可能地放鬆:
      1. 會改變程式執行結果的重排序
      2. 不會改變程式執行結果的重排序
    3. JMM對這兩種不同性質的重排序,採取了不同的策略:
      1. 對於會改變程式執行結果的重排序,JMM要求編譯器和處理器必須禁止這種重排序
      2. 對於不會改變程式執行結果的重排序,JMM對編譯器和處理器不做要求(JMM允許這種重排序)
    4. 只要不改變程式的執行結果(指的是單執行緒程式和正確同步的多執行緒程式),編譯器和處理器怎麼優化都行。例如,如果編譯器經過細緻的分析後,認定一個鎖只會被單個執行緒訪問,那麼這個鎖可以被消除。再如,如果編譯器經過細緻的分析後,認定一個volatile變數只會被單個執行緒訪問,那麼編譯器可以把這個volatile變數當作一個普通變數來對待。這些優化既不會改變程式的執行結果,又能提高程式的執行效率
    5. happens-before關係本質上和as-if-serial語義是一回事,都是為了在不改變程式執行結果的前提下,儘可能地提高程式執行的並行度 :
      1. as-if-serial語義保證單執行緒內程式的執行結果不被改變,happens-before關係保證正確同步的多執行緒程式的執行結果不被改變
      2. as-if-serial語義給編寫單執行緒程式的程式設計師創造了一個幻境:單執行緒程式是按程式的順序來執行的。happens-before關係給編寫正確同步的多執行緒程式的程式設計師創造了一個幻境:正確同步的多執行緒程式是按happens-before指定的順序來執行的
  6. 雙重檢查鎖定與延遲初始化

    1. 在早期的JVM中,synchronized(甚至是無競爭的synchronized)存在巨大的性能開銷。因此,人們想出了一個「聰明」的技巧:雙重檢查鎖定(Double-Checked Locking)。人們想通過雙重檢查鎖定來降低同步的開銷。下面是使用雙重檢查鎖定來實現延遲初始化的示例程式碼

      在執行緒執行到第4行,程式碼讀取到instance不為null時,instance引用的對象有可能還沒有完成初始化

    2. 前面的雙重檢查鎖定示例程式碼的第7行(instance=new Singleton();)創建了一個對象。這一行程式碼可以分解為如下的3行偽程式碼。

      上面3行偽程式碼中的2和3之間,可能會被重排序

      DoubleCheckedLocking示例程式碼的第7行(instance=new Singleton();)如果發生重排序,另一個並發執行的執行緒B就有可能在第4行判斷instance不為null。執行緒B接下來將訪問instance所引用的對象,但此時這個對象可能還沒有被A執行緒初始化

    3. 在知曉了問題發生的根源之後,我們可以想出兩個辦法來實現執行緒安全的延遲初始化:

      1. 不允許2和3重排序 (volatile)
      2. 允許2和3重排序,但不允許其他執行緒「看到」這個重排序(靜態內部類)
    4. 基於volatile解決辦法

      程式碼中的2和3之間的重排序,在多執行緒環境中將會被禁止

    5. 基於類初始化的解決方案

      1. JVM在類的初始化階段(即在Class被載入後,且被執行緒使用之前),會執行類的初始化。在執行類的初始化期間,JVM會去獲取一個鎖。這個鎖可以同步多個執行緒對同一個類的初始化

      2. 這個方案的實質是:允許上面中的3行偽程式碼中的2和3重排序,但不允許非構造執行緒(這裡指執行緒B)「看到」這個重排序。

      3. 初始化一個類,包括執行這個類的靜態初始化和初始化在這個類中聲明的靜態欄位。根據Java語言規範,在首次發生下列任意一種情況時,一個類或介面類型T將被立即初始:

        1. T是一個類,而且一個T類型的實例被創建
        2. T是一個類,且T中聲明的一個靜態方法被調用
        3. T中聲明的一個靜態欄位被賦值
        4. T中聲明的一個靜態欄位被使用,而且這個欄位不是一個常量欄位
        5. T是一個頂級類,而且一個斷言語句嵌套在T內部被執行
      4. Java語言規範規定,對於每一個類或介面C,都有一個唯一的初始化鎖LC與之對應。從C到LC的映射,由JVM的具體實現去自由實現。JVM在類初始化期間會獲取這個初始化鎖,並且每個執行緒至少獲取一次鎖來確保這個類已經被初始化過了

    6. 通過對比基於volatile的雙重檢查鎖定的方案和基於類初始化的方案,我們會發現基於類初始化的方案的實現程式碼更簡潔。但基於volatile的雙重檢查鎖定的方案有一個額外的優勢:除了可以對靜態欄位實現延遲初始化外,還可以對實例欄位實現延遲初始化

Java中的鎖

Lock介面
  1. 鎖是用來控制多個執行緒訪問共享資源的方式,一般來說,一個鎖能夠防止多個執行緒同時訪問共享資源(但是有些鎖可以允許多個執行緒並發的訪問共享資源,比如讀寫鎖)。在Lock介面出現之前,Java程式是靠synchronized關鍵字實現鎖功能的,而Java SE 5之後,並發包中新增了Lock介面(以及相關實現類)用來實現鎖功能,它提供了與synchronized關鍵字類似的同步功
    能,只是在使用時需要顯式地獲取和釋放鎖。雖然它缺少了(通過synchronized塊或者方法所提供的)隱式獲取釋放鎖的便捷性,但是卻擁有了鎖獲取與釋放的可操作性、可中斷的獲取鎖以及超時獲取鎖等多種synchronized關鍵字所不具備的同步特性
  2. 使用synchronized關鍵字將會隱式地獲取鎖,但是它將鎖的獲取和釋放固化了,也就是先獲取再釋放。當然,這種方式簡化了同步的管理,可是擴展性沒有顯示的鎖獲取和釋放來的好。例如,針對一個場景,手把手進行鎖獲取和釋放,先獲得鎖A,然後再獲取鎖B,當鎖B獲得後,釋放鎖A同時獲取鎖C,當鎖C獲得後,再釋放B同時獲取鎖D,以此類推。這種場景下,synchronized關鍵字就不那麼容易實現了,而使用Lock卻容易許多
隊列同步器
  1. 隊列同步器AbstractQueuedSynchronizer(以下簡稱同步器),是用來構建鎖或者其他同步組件的基礎框架,它使用了一個int成員變數表示同步狀態,通過內置的FIFO隊列來完成資源獲取執行緒的排隊工作

  2. 同步器是實現鎖(也可以是任意同步組件)的關鍵,在鎖的實現中聚合約步器,利用同步器實現鎖的語義。可以這樣理解二者之間的關係:鎖是面向使用者的,它定義了使用者與鎖交互的介面(比如可以允許兩個執行緒並行訪問),隱藏了實現細節;同步器面向的是鎖的實現者,它簡化了鎖的實現方式,屏蔽了同步狀態管理、執行緒的排隊、等待與喚醒等底層操作。鎖和同步器很好地隔離了使用者和實現者所需關注的領域

  3. 同步器的設計是基於模板方法模式的,也就是說,使用者需要繼承同步器並重寫指定的方法,隨後將同步器組合在自定義同步組件的實現中,並調用同步器提供的模板方法,而這些模板方法將會調用使用者重寫的方法

  4. 可重寫的方法

  1. 模板方法

  2. 同步器提供的模板方法基本上分為3類:獨佔式獲取與釋放同步狀態、共享式獲取與釋放同步狀態和查詢同步隊列中的等待執行緒情況。自定義同步組件將使用同步器提供的模板方法來實現自己的同步語義

  3. 自定義一個鎖:繼承實現Lock介面,使用靜態內部類繼承AbstractQueueSynchronizer,定義模板方法,重寫相關的方法,使用模板方法模式以及代理模式,案例如下:

    class Mutex implements Lock {
    // 靜態內部類,自定義同步器
    private static class Sync extends AbstractQueuedSynchronizer {
    // 是否處於佔用狀態
    protected boolean isHeldExclusively() {
    return getState() == 1;
    } /
    / 當狀態為0的時候獲取鎖
    public boolean tryAcquire(int acquires) {
    if (compareAndSetState(0, 1)) {
    setExclusiveOwnerThread(Thread.currentThread());
    return true;
    } r
    eturn false;
    } /
    / 釋放鎖,將狀態設置為0
    protected boolean tryRelease(int releases) {
    if (getState() == 0) throw new
    IllegalMonitorStateException();
    setExclusiveOwnerThread(null);
    setState(0);
    return true;
    } /
    / 返回一個Condition,每個condition都包含了一個condition隊列
    Condition newCondition() { return new ConditionObject(); }
    } /
    / 僅需要將操作代理到Sync上即可
    private final Sync sync = new Sync();
    public void lock() { sync.acquire(1); }
    public boolean tryLock() { return sync.tryAcquire(1); }
    public void unlock() { sync.release(1); }
    public Condition newCondition() { return sync.newCondition(); }
    public boolean isLocked() { return sync.isHeldExclusively(); }
    public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); }
    public void lockInterruptibly() throws InterruptedException {
    sync.acquireInterruptibly(1);
    } p
    ublic boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
    return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }
    }
    
  4. 上述示例中,獨佔鎖Mutex是一個自定義同步組件,它在同一時刻只允許一個執行緒佔有鎖。Mutex中定義了一個靜態內部類,該內部類繼承了同步器並實現了獨佔式獲取和釋放同步狀態。在tryAcquire(int acquires)方法中,如果經過CAS設置成功(同步狀態設置為1),則代表獲取了同步狀態,而在tryRelease(int releases)方法中只是將同步狀態重置為0。用戶使用Mutex時並不會直接和內部同步器的實現打交道,而是調用Mutex提供的方法,在Mutex的實現中,以獲取鎖的lock()方法為例,只需要在方法實現中調用同步器的模板方法acquire(int args)即可,當前執行緒調用該方法獲取同步狀態失敗後會被加入到同步隊列中等待,這樣就大大降低了實現一個可靠自定義同步組件的門檻

  5. 實現原理:單執行緒在獲取同步狀態(鎖)時,如果未獲取到,則會被創造成一個節點添加到同步隊列的末尾,同步隊列的所有節點都表示一個執行緒的引用,每個節點都在自旋,查看前一個結點是否是頭節點,是的話就嘗試獲取同步狀態,當成功獲取後,會喚醒下一個節點,然後首節點去執行自己的任務,否則添加到隊列末尾

重入鎖
  1. 重入鎖ReentrantLock,顧名思義,就是支援重進入的鎖,它表示該鎖能夠支援一個執行緒對資源的重複加鎖。除此之外,該鎖的還支援獲取鎖時的公平和非公平性選擇

  2. 重進入是指任意執行緒在獲取到鎖之後能夠再次獲取該鎖而不會被鎖所阻塞,該特性的實現需要解決以下兩個問題

    1. 執行緒再次獲取鎖。鎖需要去識別獲取鎖的執行緒是否為當前佔據鎖的執行緒,如果是,則再次成功獲取
    2. 鎖的最終釋放。執行緒重複n次獲取了鎖,隨後在第n次釋放該鎖後,其他執行緒能夠獲取到該鎖。鎖的最終釋放要求鎖對於獲取進行計數自增,計數表示當前鎖被重複獲取的次數,而鎖被釋放時,計數自減,當計數等於0時表示鎖已經成功釋放
    3. 通過判斷前執行緒是否為獲取鎖的執行緒來決定獲取操作是否成功,如果是獲取鎖的執行緒再次求,則將同步狀態值進行增加並返回true,表示獲取同步狀態成功
    4. 成功獲取鎖的執行緒再次獲取鎖,只是增加了同步狀態值,這也就要求ReentrantLock在釋放同步狀態時減少同步狀態值
  3. 公平鎖與非公平鎖

    公平性與否是針對獲取鎖而言的,如果一個鎖是公平的,那麼鎖的獲取順序就應該符合請求的絕對時間順序,也就是FIFO

讀寫鎖
  1. 在基礎部分已經介紹了相關的概念,這裡說一些關於鎖的設計的東西

  2. 讀寫鎖同樣依賴自定義同步器來實現同步功能,而讀寫狀態就是其同步器的同步狀態。回想ReentrantLock中自定義同步器的實現,同步狀態表示鎖被一個執行緒重複獲取的次數,而讀寫鎖的自定義同步器需要在同步狀態(一個整型變數)上維護多個讀執行緒和一個寫執行緒的狀態,使得該狀態的設計成為讀寫鎖實現的關鍵

  3. 如果在一個整型變數上維護多種狀態,就一定需要「按位切割使用」這個變數,讀寫鎖將變數切分成了兩個部分,高16位表示讀,低16位表示寫

當前同步狀態表示一個執行緒已經獲取了寫鎖,且重進入了兩次,同時也連續獲取了兩次讀鎖。讀寫鎖是如何迅速確定讀和寫各自的狀態呢? 答案是通過位運算。假設當前同步狀態值為S,寫狀態等於S&0x0000FFFF(將高16位全部抹去),讀狀態等於S>>>16(無符號補0右移16位)。當寫狀態增加1時,等於S+1,當讀狀態增加1時,等於S+(1<<16),也就是S+0x00010000

根據狀態的劃分能得出一個推論:S不等於0時,當寫狀態(S&0x0000FFFF)等於0時,則讀狀態(S>>>16)大於0,即讀鎖已被獲取

Condition介面
  1. 任意一個Jav對象,都擁有一組監視器方法(定義在java.lang.Object上),主要包括wait()、wait(long timeout)、notify()以及notifyAll()方法,這些方法與synchronized同步關鍵字配合,可以實現等待/通知模式。Condition介面也提供了類似Object的監視器方法,與Lock配合可以實現等待/通知模式,但是這兩者在使用方式以及功能特性上還是有差別的

  2. Condition定義了等待/通知兩種類型的方法,當前執行緒調用這些方法時,需要提前獲取到Condition對象關聯的鎖。Condition對象是由Lock對象(調用Lock對象的newCondition()方法)創建出來的,換句話說,Condition是依賴Lock對象的

    Lock lock = new ReentrantLock();
    Condition condition = lock.newCondition();
    public void conditionWait() throws InterruptedException {
    lock.lock();
    try {
    condition.await();
    } finally {
    lock.unlock();
    }
    }p
    ublic void conditionSignal() throws InterruptedException {
    lock.lock();
    try {
    condition.signal();
    } finally {
    lock.unlock();
    }
    }
    
    1. 等待隊列

      1. 等待隊列是一個FIFO的隊列,在隊列中的每個節點都包含了一個執行緒引用,該執行緒就是在Condition對象上等待的執行緒,如果一個執行緒調用了Condition.await()方法,那麼該執行緒將會
        釋放鎖、構造成節點加入等待隊列並進入等待狀態。事實上,節點的定義復用了同步器中節點的定義,也就是說,同步隊列和等待隊列中節點類型都是同步器的靜態內部類AbstractQueuedSynchronizer.Node

    2. 在Object的監視器模型上,一個對象擁有一個同步隊列和等待隊列,而並發包中的Lock(更確切地說是同步器)擁有一個同步隊列和多個等待隊列

    3. 等待

      1. 調用Condition的await()方法(或者以await開頭的方法),會使當前執行緒進入等待隊列並釋放鎖,同時執行緒狀態變為等待狀態。當從await()方法返回時,當前執行緒一定獲取了Condition相關聯的鎖
      2. 如果從隊列(同步隊列和等待隊列)的角度看await()方法,當調用await()方法時,相當於同步隊列的首節點(獲取了鎖的節點)移動到Condition的等待隊列中
      3. 調用該方法的執行緒成功獲取了鎖的執行緒,也就是同步隊列中的首節點,該方法會將當前執行緒構造成節點並加入等待隊列中,然後釋放同步狀態,喚醒同步隊列中的後繼節點,然後當前執行緒會進入等待狀態
      4. 當等待隊列中的節點被喚醒,則喚醒節點的執行緒開始嘗試獲取同步狀態。如果不是通過其他執行緒調用Condition.signal()方法喚醒,而是對等待執行緒進行中斷,則會拋出InterruptedException
    4. 通知

      1. 調用Condition的signal()方法,將會喚醒在等待隊列中等待時間最長的節點(首節點),在喚醒節點之前,會將節點移到同步隊列中

        public final void signal() {
        if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
        Node first = firstWaiter;
        if (first != null)
        doSignal(first);
        }
        

        調用該方法的前置條件是當前執行緒必須獲取了鎖,可以看到signal()方法進行了isHeldExclusively()檢查,也就是當前執行緒必須是獲取了鎖的執行緒。接著獲取等待隊列的首節點,將其移動到同步隊列並使用LockSupport喚醒節點中的執行緒

Java並發容器和框架

ConcurrentHashMap的實現原理與使用
  1. 為什麼要使用ConcurrentHashMap

    1. HashMap在並發執行put操作時會引起死循環,是因為多執行緒會導致HashMap的Entry鏈表形成環形數據結構,一旦形成環形數據結構,Entry的next節點永遠不為空,就會產生死循環獲
      取Entry
    2. HashMap在並發執行put操作時會引起死循環,是因為多執行緒會導致HashMap的Entry鏈表形成環形數據結構,一旦形成環形數據結構,Entry的next節點永遠不為空,就會產生死循環獲取Entry
    3. HashTable容器在競爭激烈的並發環境下表現出效率低下的原因是所有訪問HashTable的執行緒都必須競爭同一把鎖,假如容器里有多把鎖,每一把鎖用於鎖容器其中一部分數據,那麼當多執行緒訪問容器里不同數據段的數據時,執行緒間就不會存在鎖競爭,從而可以有效提高並發訪問效率,這就是ConcurrentHashMap所使用的鎖分段技術。首先將數據分成一段一段地存儲,然後給每一段數據配一把鎖,當一個執行緒佔用鎖訪問其中一個段數據的時候,其他段的數據也能被其他執行緒訪問
  2. ConcurrentHashMap的結構

    1. ConcurrentHashMap是由Segment數組結構和HashEntry數組結構組成。Segment是一種可重入鎖(ReentrantLock),在ConcurrentHashMap里扮演鎖的角色;HashEntry則用於存儲鍵值對數據。一個ConcurrentHashMap里包含一個Segment數組。Segment的結構和HashMap類似,是一種數組和鏈表結構。一個Segment里包含一個HashEntry數組,每個HashEntry是一個鏈表結構的元素,每個Segment守護著一個HashEntry數組裡的元素,當對HashEntry數組的數據進行修改時,必須首先獲得與它對應的Segment鎖

    2. ConcurrentHashMap的初始化

      ConcurrentHashMap初始化方法是通過initialCapacity、loadFactor和concurrencyLevel等幾個參數來初始化segment數組、段偏移量segmentShift、段掩碼segmentMask和每個segment里的HashEntry數組來實現的

    3. 定位Segment

      既然ConcurrentHashMap使用分段鎖Segment來保護不同段的數據,那麼在插入和獲取元素的時候,必須先通過散列演算法定位到Segment。可以看到ConcurrentHashMap會首先使用Wang/Jenkins hash的變種演算法對元素的hashCode進行一次再散列。

    4. ConcurrentHashMap的操作

      1. get操作

        1. Segment的get操作實現非常簡單和高效。先經過一次再散列,然後使用這個散列值通過散列運算定位到Segment,再通過散列演算法定位到元素
        2. get操作的高效之處在於整個get過程不需要加鎖,除非讀到的值是空才會加鎖重讀。我們知道HashTable容器的get方法是需要加鎖的,那麼ConcurrentHashMap的get操作是如何做到不加鎖的呢? 原因是它的get方法里將要使用的共享變數都定義成volatile類型,如用於統計當前Segement大小的count欄位和用於存儲值的HashEntry的value。定義成volatile的變數,能夠在執行緒之間保持可見性,能夠被多執行緒同時讀,並且保證不會讀到過期的值,但是只能被單執行緒寫(有一種情況可以被多執行緒寫,就是寫入的值不依賴於原值),在get操作里只需要讀不需要寫共享變數count和value,所以可以不用加鎖。之所以不會讀到過期的值,是因為根據Java記憶體模型的happen before原則,對volatile欄位的寫入操作先於讀操作,即使兩個執行緒同時修改和獲取volatile變數,get操作也能拿到最新的值,這是用volatile替換鎖的經典應用場景
      2. put操作

        1. 由於put方法里需要對共享變數進行寫入操作,所以為了執行緒安全,在操作共享變數時必須加鎖。put方法首先定位到Segment,然後在Segment里進行插入操作。插入操作需要經歷兩個步驟,第一步判斷是否需要對Segment里的HashEntry數組進行擴容,第二步定位添加元素的位置,然後將其放在HashEntry數組
          1. 在插入元素前會先判斷Segment里的HashEntry數組是否超過容量(threshold),如果超過閾值,則對數組進行擴容。值得一提的是,Segment的擴容判斷比HashMap更恰當,因為HashMap是在插入元素後判斷元素是否已經到達容量的,如果到達了就進行擴容,但是很有可能擴容之後沒有新元素插入,這時HashMap就進行了一次無效的擴容
          2. 在擴容的時候,首先會創建一個容量是原來容量兩倍的數組,然後將原數組裡的元素進行再散列後插入到新的數組裡。為了高效,ConcurrentHashMap不會對整個容器進行擴容,而只對某個segment進行擴容
      3. size操作

        1. 如果要統計整個ConcurrentHashMap里元素的大小,就必須統計所有Segment里元素的大小後求和。Segment里的全局變數count是一個volatile變數,那麼在多執行緒場景下,是不是直接把所有Segment的count相加就可以得到整個ConcurrentHashMap大小了呢? 不是的,雖然相加時可以獲取每個Segment的count的最新值,但是可能累加前使用的count發生了變化,那麼統計結果就不準了。所以,最安全的做法是在統計size的時候把所有Segment的put、remove和clean方法全部鎖住,但是這種做法顯然非常低效

          因為在累加count操作過程中,之前累加過的count發生變化的幾率非常小,所以ConcurrentHashMap的做法是先嘗試2次通過不鎖住Segment的方式來統計各個Segment大小,如果統計的過程中,容器的count發生了變化,則再採用加鎖的方式來統計所有Segment的大小

          那麼ConcurrentHashMap是如何判斷在統計的時候容器是否發生了變化呢? 使用modCount變數,在put、remove和clean方法里操作元素前都會將變數modCount進行加1,那麼在統計size前後比較modCount是否發生變化,從而得知容器的大小是否發生變化

ConcurrentLinkedQueue

在並發編程中,有時候需要使用執行緒安全的隊列。如果要實現一個執行緒安全的隊列有兩種方式:一種是使用阻塞演算法,另一種是使用非阻塞演算法。使用阻塞演算法的隊列可以用一個鎖(入隊和出隊用同一把鎖)或兩個鎖(入隊和出隊用不同的鎖)等方式來實現。非阻塞的實現方式則可以使用循環CAS的方式來實現。本節讓我們一起來研究一下Doug Lea是如何使用非阻塞的方式來實現執行緒安全隊列ConcurrentLinkedQueue的,相信從大師身上我們能學到不少並發編程的技巧

ConcurrentLinkedQueue是一個基於鏈接節點的無界執行緒安全隊列,它採用先進先出的規則對節點進行排序,當我們添加一個元素的時候,它會添加到隊列的尾部;當我們獲取一個元素時,它會返回隊列頭部的元素。它採用了「wait-free」演算法(即CAS演算法)來實現,該演算法在Michael&Scott演算法上進行了一些修改

阻塞隊列
  1. 阻塞隊列(BlockingQueue)是一個支援兩個附加操作的隊列。這兩個附加的操作支援阻塞的插入和移除方法

    1. 支援阻塞的插入方法:意思是當隊列滿時,隊列會阻塞插入元素的執行緒,直到隊列不滿
    2. 支援阻塞的移除方法:意思是在隊列為空時,獲取元素的執行緒會等待隊列變為非空
  2. 阻塞隊列常用於生產者和消費者的場景,生產者是向隊列里添加元素的執行緒,消費者是從隊列里取元素的執行緒。阻塞隊列就是生產者用來存放元素、消費者用來獲取元素的容器

  3. Java里的阻塞隊列

    1. ArrayBlockingQueue:一個由數組結構組成的有界阻塞隊列
    2. LinkedBlockingQueue:一個由鏈表結構組成的有界阻塞隊列
    3. PriorityBlockingQueue:一個支援優先順序排序的無界阻塞隊列
    4. DelayQueue:一個使用優先順序隊列實現的無界阻塞隊列
    5. SynchronousQueue:一個不存儲元素的阻塞隊列
    6. LinkedTransferQueue:一個由鏈表結構組成的無界阻塞隊列
    7. LinkedBlockingDeque:一個由鏈表結構組成的雙向阻塞隊列
  4. 阻塞隊列的實現原理

    1. 使用通知模式實現。所謂通知模式,就是當生產者往滿的隊列里添加元素時會阻塞住生產者,當消費者消費了一個隊列中的元素後,會通知生產者當前隊列可用

      1. private final Condition notFull;
        private final Condition notEmpty;
        public ArrayBlockingQueue(int capacity, boolean fair) {
        // 省略其他程式碼
        notEmpty = lock.newCondition();
        notFull = lock.newCondition();
        }
        public void put(E e) throws InterruptedException {
        checkNotNull(e);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
        while (count == items.length)
        notFull.await();
        insert(e);
        } finally {
        lock.unlock();
        }
        }p
        ublic E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
        while (count == 0)
        notEmpty.await();
        return extract();
        } finally {
        lock.unlock();
        }
        }p
        rivate void insert(E x) {
        items[putIndex] = x;
        putIndex = inc(putIndex);
        ++count;
        notEmpty.signal();
        }
        
    2. 當往隊列里插入一個元素時,如果隊列不可用,那麼阻塞生產者主要通過LockSupport.park(this)來實現

      public final void await() throws InterruptedException {
      if (Thread.interrupted())
      throw new InterruptedException();
      Node node = addConditionWaiter();
      int savedState = fullyRelease(node);
      int interruptMode = 0;
      while (!isOnSyncQueue(node)) {
      LockSupport.park(this);
      if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
      break;
      }i
      f (acquireQueued(node, savedState) && interruptMode != THROW_IE)
      interruptMode = REINTERRUPT;
      if (node.nextWaiter != null) // clean up if cancelled
      unlinkCancelledWaiters();
      if (interruptMode != 0)
      reportInterruptAfterWait(interruptMode);
      }
      

Java中的常用原子操作類

原子更新基本類型類
  1. AtomicBoolean:原子更新布爾類型
  2. AtomicInteger:原子更新整型
  3. AtomicLong:原子更新長整型
原子更新數組
  1. AtomicIntegerArray:原子更新整型數組裡的元素
  2. AtomicLongArray:原子更新長整型數組裡的元素
  3. AtomicReferenceArray:原子更新引用類型數組裡的元素
原子更新引用類型
  1. AtomicReference:原子更新引用類型
  2. AtomicReferenceFieldUpdater:原子更新引用類型里的欄位
  3. AtomicMarkableReference:原子更新帶有標記位的引用類型
原子更新欄位類
  1. AtomicIntegerFieldUpdater:原子更新整型的欄位的更新器
  2. AtomicLongFieldUpdater:原子更新長整型欄位的更新器
  3. AtomicStampedReference:原子更新帶有版本號的引用類型

Java中的並發工具類

等待多執行緒完成的CountDownLatch
  1. CountDownLatch允許一個或多個執行緒等待其他執行緒完成操作

  2. public class CountDownLatchTest {
    staticCountDownLatch c = new CountDownLatch(2);
    public static void main(String[] args) throws InterruptedException {
    new Thread(new Runnable() {
    @Override
    public void run() {
    System.out.println(1);
    c.countDown();
    System.out.println(2);
    c.countDown();
    }
    }).start();
    c.await();
    System.out.println("3");
    }
    }
    
  3. CountDownLatch的構造函數接收一個int類型的參數作為計數器,如果你想等待N個點完成,這裡就傳入N。當我們調用CountDownLatch的countDown方法時,N就會減1,CountDownLatch的await方法會阻塞當前執行緒,直到N變成零。由於countDown方法可以用在任何地方,所以這裡說的N個點,可以是N個執行緒,也可以是1個執行緒里的N個執行步驟。用在多個執行緒時,只需要把這個CountDownLatch的引用傳遞到執行緒里即可

  4. 注意:計數器必須大於等於0,只是等於0時候,計數器就是零,調用await方法時不會阻塞當前執行緒;若計數器大於執行緒數,那麼就會一直等待,不會執行await()後的語句。CountDownLatch不可能重新初始化或者修改CountDownLatch對象的內部計數器的值。一個執行緒調用countDown方法happen-before,另外一個執行緒調用await方法

同步屏障CyclicBarrier
  1. CyclicBarrier的字面意思是可循環使用(Cyclic)的屏障(Barrier)。它要做的事情是,讓一組執行緒到達一個屏障(也可以叫同步點)時被阻塞,直到最後一個執行緒到達屏障時,屏障才會開門,所有被屏障攔截的執行緒才會繼續運行

  2. CyclicBarrier默認的構造方法是CyclicBarrier(int parties),其參數表示屏障攔截的執行緒數量,每個執行緒調用await方法告訴CyclicBarrier我已經到達了屏障,然後當前執行緒被阻塞

  3. CyclicBarrier還提供一個更高級的構造函數CyclicBarrier(int parties,Runnable barrierAction),用於在執行緒到達屏障時,優先執行barrierAction,方便處理更複雜的業務場景

  4. package java_Multi_thread_programming.module.code46;
    
    import java.util.concurrent.BrokenBarrierException;
    import java.util.concurrent.CyclicBarrier;
    
    public class Test3 {
        public static void main(String[] args) {
            Thread[] threads = new Thread[10];
            CyclicBarrier cyclicBarrier = new CyclicBarrier(10, new Runnable() {
                @Override
                public void run() {
                    System.out.println("開始執行:"+System.currentTimeMillis());
                }
            });
    
            for (int i = 0; i < threads.length; i++) {
                threads[i] = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            cyclicBarrier.await();
                            System.out.println("執行緒"+Thread.currentThread().getName()+"執行:"+ System.currentTimeMillis());
                        } catch (InterruptedException | BrokenBarrierException e) {
                            e.printStackTrace();
                        }
                    }
                });
                threads[i].start();
            }
        }
    }
    
  5. 注意:當傳入的執行緒數大於執行緒使用的數時,會一直等待,知道使用的執行緒數等於攔截的執行緒數

  6. CyclicBarrier和CountDownLatch的區別

    1. CountDownLatch的計數器只能使用一次,而CyclicBarrier的計數器可以使用reset()方法重置。所以CyclicBarrier能處理更為複雜的業務場景。例如,如果計算髮生錯誤,可以重置計數器,並讓執行緒重新執行一次
    2. CountDownLatch的計數器只能使用一次,而CyclicBarrier的計數器可以使用reset()方法重置。所以CyclicBarrier能處理更為複雜的業務場景。例如,如果計算髮生錯誤,可以重置計數器,並讓執行緒重新執行一次
    3. CountDownLatch是執行完指定數量的執行緒後執行await()後的語句(代替join)使用;CyclicBarrier是當要執行的執行緒數夠了才執行每個執行緒中await()後面的語句
    4. CountDownLatch(需要最後執行的語句前面)主要使用await()方法的位置不同於CyclicBarrier(每個執行緒)
控制並發執行緒數的Semaphore
  1. Semaphore(訊號量)是用來控制同時訪問特定資源的執行緒數量,它通過協調各個執行緒,以保證合理的使用公共資源。它比作是控制流量的紅綠燈。比如××馬路要限制流量,只允許同時有一百輛車在這條路上行使,其他的都必須在路口等待,所以前一百輛車會看到綠燈,可以開進這條馬路,後面的車會看到紅燈,不能駛入××馬路,但是如果前一百輛中有5輛車已經離開了××馬路,那麼後面就允許有5輛車駛入馬路,這個例子里說的車就是執行緒,駛入馬路就表示執行緒在執行,離開馬路就表示執行緒執行完成,看見紅燈就表示執行緒被阻塞,不能執行

  2. public class SemaphoreTest {
    private static final int THREAD_COUNT = 30;
    private static ExecutorServicethreadPool = Executors
    .newFixedThreadPool(THREAD_COUNT);
    private static Semaphore s = new Semaphore(10);
    public static void main(String[] args) {
    for (inti = 0; i< THREAD_COUNT; i++) {
    threadPool.execute(new Runnable() {
    @Override
    public void run() {
    try {
    s.acquire();System.out.println("save data");
    s.release();
    } catch (InterruptedException e) {
    }
    }
    });
    }
    threadPool.shutdown();
    }
    }
    
執行緒間交換數據的Exchanger
  1. Exchanger(交換者)是一個用於執行緒間協作的工具類。Exchanger用於進行執行緒間的數據交換。它提供一個同步點,在這個同步點,兩個執行緒可以交換彼此的數據。這兩個執行緒通過exchange方法交換數據,如果第一個執行緒先執行exchange()方法,它會一直等待第二個執行緒也執行exchange方法,當兩個執行緒都到達同步點時,這兩個執行緒就可以交換數據,將本執行緒生產出來的數據傳遞給對方

  2. package java_Multi_thread_programming.module.code46;
    
    import java.util.concurrent.Exchanger;
    
    public class Test4 {
        public static Exchanger<String> exchanger = new Exchanger<>();
        public static void main(String[] args) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    String string = ""+Thread.currentThread().getName();
                    try {
                        System.out.println(string+exchanger.exchange(string));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "執行緒1").start();
    
            new Thread(new Runnable() {
                @Override
                public void run() {
                    String string1 = "" + Thread.currentThread().getName();
                    try {
                        System.out.println(string1+exchanger.exchange(string1));
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "執行緒2").start();
        }
    }
    
    
  3. 注意:如果兩個執行緒有一個沒有執行exchange()方法,則會一直等待,如果擔心有特殊情況發生,避免一直等待,可以使用exchange(V x,longtimeout,TimeUnit unit)設置最大等待時長

Java中的執行緒池

Java中的執行緒池是運用場景最多的並發框架,幾乎所有需要非同步或並發執行任務的程式都可以使用執行緒池。在開發過程中,合理地使用執行緒池能夠帶來3個好處
第一:降低資源消耗。通過重複利用已創建的執行緒降低執行緒創建和銷毀造成的消耗
第二:提高響應速度。當任務到達時,任務可以不需要等到執行緒創建就能立即執行
第三:提高執行緒的可管理性。執行緒是稀缺資源,如果無限制地創建,不僅會消耗系統資源,還會降低系統的穩定性,使用執行緒池可以進行統一分配、調優和監控

執行緒池的實現原理

從圖中可以看出,當提交一個新任務到執行緒池時,執行緒池的處理流程如下

  1. 執行緒池判斷核心執行緒池裡的執行緒是否都在執行任務。如果不是,則創建一個新的工作執行緒來執行任務。如果核心執行緒池裡的執行緒都在執行任務,則進入下個流程
  2. 執行緒池判斷工作隊列是否已經滿。如果工作隊列沒有滿,則將新提交的任務存儲在這個工作隊列里。如果工作隊列滿了,則進入下個流程
  3. 執行緒池判斷執行緒池的執行緒是否都處於工作狀態。如果沒有,則創建一個新的工作執行緒來執行任務。如果已經滿了,則交給飽和策略來處理這個任務

ThreadPoolExecutor執行execute()方法的示意圖如下:

ThreadPoolExecutor執行execute方法分下面4種情況

  1. 如果當前運行的執行緒少於corePoolSize,則創建新執行緒來執行任務(注意,執行這一步驟需要獲取全局鎖)
  2. 如果運行的執行緒等於或多於corePoolSize,則將任務加入BlockingQueue
  3. 如果無法將任務加入BlockingQueue(隊列已滿),則創建新的執行緒來處理任務(注意,執行這一步驟需要獲取全局鎖)
  4. 如果創建新執行緒將使當前運行的執行緒超出maximumPoolSize,任務將被拒絕,並調用RejectedExecutionHandler.rejectedExecution()方法

ThreadPoolExecutor採取上述步驟的總體設計思路,是為了在執行execute()方法時,儘可能地避免獲取全局鎖(那將會是一個嚴重的可伸縮瓶頸)。在ThreadPoolExecutor完成預熱之後(當前運行的執行緒數大於等於corePoolSize),幾乎所有的execute()方法調用都是執行步驟2,而步驟2不需要獲取全局鎖

執行緒池中的執行緒執行任務分兩種情況,如下

  1. 在execute()方法中創建一個執行緒時,會讓這個執行緒執行當前任務
  2. 這個執行緒執行完上圖中1的任務後,會反覆從BlockingQueue獲取任務來執行
執行緒池的使用
  1. 執行緒池的創建

    1. new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime,
      milliseconds,runnableTaskQueue, handler);
      
    2. 創建一個執行緒池時需要輸入幾個參數,如下 :

      1. corePoolSize(執行緒池的基本大小):當提交一個任務到執行緒池時,執行緒池會創建一個執行緒來執行任務,即使其他空閑的基本執行緒能夠執行新任務也會創建執行緒,等到需要執行的任
        務數大於執行緒池基本大小時就不再創建。如果調用了執行緒池的prestartAllCoreThreads()方法,執行緒池會提前創建並啟動所有基本執行緒

      2. runnableTaskQueue(任務隊列):用於保存等待執行的任務的阻塞隊列。可以選擇以下幾個阻塞隊列

        1. ArrayBlockingQueue:是一個基於數組結構的有界阻塞隊列,此隊列按FIFO(先進先出)原則對元素進行排序
        2. LinkedBlockingQueue:一個基於鏈表結構的阻塞隊列,此隊列按FIFO排序元素,吞吐量通常要高於ArrayBlockingQueue。靜態工廠方法Executors.newFixedThreadPool()使用了這個隊列
        3. SynchronousQueue:一個不存儲元素的阻塞隊列。每個插入操作必須等到另一個執行緒調用移除操作,否則插入操作一直處於阻塞狀態,吞吐量通常要高於Linked-BlockingQueue,靜態工廠方法Executors.newCachedThreadPool使用了這個隊列
        4. PriorityBlockingQueue:一個具有優先順序的無限阻塞隊列
      3. maximumPoolSize(執行緒池最大數量):執行緒池允許創建的最大執行緒數。如果隊列滿了,並且已創建的執行緒數小於最大執行緒數,則執行緒池會再創建新的執行緒執行任務。值得注意的是,如果使用了無界的任務隊列這個參數就沒什麼效果

      4. ThreadFactory:用於設置創建執行緒的工廠,可以通過執行緒工廠給每個創建出來的執行緒設置更有意義的名字。使用開源框架guava提供的ThreadFactoryBuilder可以快速給執行緒池裡的線
        程設置有意義的名字,程式碼如下

        new ThreadFactoryBuilder().setNameFormat("XX-task-%d").build();
        
      5. RejectedExecutionHandler(飽和策略):當隊列和執行緒池都滿了,說明執行緒池處於飽和狀態,那麼必須採取一種策略處理提交的新任務。這個策略默認情況下是AbortPolicy,表示無法處理新任務時拋出異常。在JDK 1.5中Java執行緒池框架提供了以下4種策略

        1. AbortPolicy:直接拋出異常
        2. CallerRunsPolicy:只用調用者所在執行緒來運行任務
        3. DiscardOldestPolicy:丟棄隊列里最近的一個任務,並執行當前任務
        4. DiscardPolicy:不處理,丟棄掉。當然,也可以根據應用場景需要來實現RejectedExecutionHandler介面自定義策略。如記錄日誌或持久化存儲不能處理的任務
        5. keepAliveTime(執行緒活動保持時間):執行緒池的工作執行緒空閑後,保持存活的時間。所以,如果任務很多,並且每個任務執行的時間比較短,可以調大時間,提高執行緒的利用率
        6. TimeUnit(執行緒活動保持時間的單位):可選的單位有天(DAYS)、小時(HOURS)、分鐘(MINUTES)、毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和納秒(NANOSECONDS,千分之一微秒)
  2. 向執行緒池提交任務

    1. 可以使用兩個方法向執行緒池提交任務,分別為execute()和submit()方法
    2. execute()方法用於提交不需要返回值的任務,所以無法判斷任務是否被執行緒池執行成功
    3. submit()方法用於提交需要返回值的任務。執行緒池會返回一個future類型的對象,通過這個future對象可以判斷任務是否執行成功,並且可以通過future的get()方法來獲取返回值,get()方法會阻塞當前執行緒直到任務完成,而使用get(long timeout,TimeUnit unit)方法則會阻塞當前執行緒一段時間後立即返回,這時候有可能任務沒有執行完
  3. 關閉執行緒池

    1. 可以通過調用執行緒池的shutdown或shutdownNow方法來關閉執行緒池。它們的原理是遍歷執行緒池中的工作執行緒,然後逐個調用執行緒的interrupt方法來中斷執行緒,所以無法響應中斷的任務可能永遠無法終止。但是它們存在一定的區別,shutdownNow首先將執行緒池的狀態設置成STOP,然後嘗試停止所有的正在執行或暫停任務的執行緒,並返回等待執行任務的列表,而shutdown只是將執行緒池的狀態設置成SHUTDOWN狀態,然後中斷所有沒有正在執行任務的執行緒
    2. 只要調用了這兩個關閉方法中的任意一個,isShutdown方法就會返回true。當所有的任務都已關閉後,才表示執行緒池關閉成功,這時調用isTerminaed方法會返回true。至於應該調用哪一種方法來關閉執行緒池,應該由提交到執行緒池的任務特性決定,通常調用shutdown方法來關閉執行緒池,如果任務不一定要執行完,則可以調用shutdownNow方法
Tags: