主頁 > 後端開發 > 關于Java高并發編程你需要知道的“升段攻略”

關于Java高并發編程你需要知道的“升段攻略”

2021-03-01 06:11:26 後端開發

關于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方法

轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/264684.html

標籤:Java

上一篇:Redis原始碼剖析之記憶體淘汰策略(Evict)

下一篇:執行緒池執行流程

標籤雲
其他(157675) Python(38076) JavaScript(25376) Java(17977) C(15215) 區塊鏈(8255) C#(7972) AI(7469) 爪哇(7425) MySQL(7132) html(6777) 基礎類(6313) sql(6102) 熊猫(6058) PHP(5869) 数组(5741) R(5409) Linux(5327) 反应(5209) 腳本語言(PerlPython)(5129) 非技術區(4971) Android(4554) 数据框(4311) css(4259) 节点.js(4032) C語言(3288) json(3245) 列表(3129) 扑(3119) C++語言(3117) 安卓(2998) 打字稿(2995) VBA(2789) Java相關(2746) 疑難問題(2699) 细绳(2522) 單片機工控(2479) iOS(2429) ASP.NET(2402) MongoDB(2323) 麻木的(2285) 正则表达式(2254) 字典(2211) 循环(2198) 迅速(2185) 擅长(2169) 镖(2155) 功能(1967) .NET技术(1958) Web開發(1951) python-3.x(1918) HtmlCss(1915) 弹簧靴(1913) C++(1909) xml(1889) PostgreSQL(1872) .NETCore(1853) 谷歌表格(1846) Unity3D(1843) for循环(1842)

熱門瀏覽
  • 【C++】Microsoft C++、C 和匯編程式檔案

    ......

    uj5u.com 2020-09-10 00:57:23 more
  • 例外宣告

    相比于斷言適用于排除邏輯上不可能存在的狀態,例外通常是用于邏輯上可能發生的錯誤。 例外宣告 Item 1:當函式不可能拋出例外或不能接受拋出例外時,使用noexcept 理由 如果不打算拋出例外的話,程式就會認為無法處理這種錯誤,并且應當盡早終止,如此可以有效地阻止例外的傳播與擴散。 示例 //不可 ......

    uj5u.com 2020-09-10 00:57:27 more
  • Codeforces 1400E Clear the Multiset(貪心 + 分治)

    鏈接:https://codeforces.com/problemset/problem/1400/E 來源:Codeforces 思路:給你一個陣列,現在你可以進行兩種操作,操作1:將一段沒有 0 的區間進行減一的操作,操作2:將 i 位置上的元素歸零。最終問:將這個陣列的全部元素歸零后操作的最少 ......

    uj5u.com 2020-09-10 00:57:30 more
  • UVA11610 【Reverse Prime】

    本人看到此題沒有翻譯,就附帶了一個自己的翻譯版本 思考 這一題,它的第一個要求是找出所有 $7$ 位反向質數及其質因數的個數。 我們應該需要質數篩篩選1~$10^{7}$的所有數,這里就不慢慢介紹了。但是,重讀題,我們突然發現反向質數都是 $7$ 位,而將它反過來后的數字卻是 $6$ 位數,這就說明 ......

    uj5u.com 2020-09-10 00:57:36 more
  • 統計區間素數數量

    1 #pragma GCC optimize(2) 2 #include <bits/stdc++.h> 3 using namespace std; 4 bool isprime[1000000010]; 5 vector<int> prime; 6 inline int getlist(int ......

    uj5u.com 2020-09-10 00:57:47 more
  • C/C++編程筆記:C++中的 const 變數詳解,教你正確認識const用法

    1、C中的const 1、區域const變數存放在堆疊區中,會分配記憶體(也就是說可以通過地址間接修改變數的值)。測驗代碼如下: 運行結果: 2、全域const變數存放在只讀資料段(不能通過地址修改,會發生寫入錯誤), 默認為外部聯編,可以給其他源檔案使用(需要用extern關鍵字修飾) 運行結果: ......

    uj5u.com 2020-09-10 00:58:04 more
  • 【C++犯錯記錄】VS2019 MFC添加資源不懂如何修改資源宏ID

    1. 首先在資源視圖中,添加資源 2. 點擊新添加的資源,復制自動生成的ID 3. 在解決方案資源管理器中找到Resource.h檔案,編輯,使用整個專案搜索和替換的方式快速替換 宏宣告 4. Ctrl+Shift+F 全域搜索,點擊查找全部,然后逐個替換 5. 為什么使用搜索替換而不使用屬性視窗直 ......

    uj5u.com 2020-09-10 00:59:11 more
  • 【C++犯錯記錄】VS2019 MFC不懂的批量添加資源

    1. 打開資源頭檔案Resource.h,在其中預先定義好宏 ID(不清楚其實ID值應該設定多少,可以先新建一個相同的資源項,再在這個資源的ID值的基礎上遞增即可) 2. 在資源視圖中選中專案資源,按F7編輯資源檔案,按 ID 型別 相對路徑的形式添加 資源。(別忘了先把檔案拷貝到專案中的res檔案 ......

    uj5u.com 2020-09-10 01:00:19 more
  • C/C++編程筆記:關于C++的參考型別,專供新手入門使用

    今天要講的是C++中我最喜歡的一個用法——參考,也叫別名。 參考就是給一個變數名取一個變數名,方便我們間接地使用這個變數。我們可以給一個變數創建N個參考,這N + 1個變數共享了同一塊記憶體區域。(參考型別的變數會占用記憶體空間,占用的記憶體空間的大小和指標型別的大小是相同的。雖然參考是一個物件的別名,但 ......

    uj5u.com 2020-09-10 01:00:22 more
  • 【C/C++編程筆記】從頭開始學習C ++:初學者完整指南

    眾所周知,C ++的學習曲線陡峭,但是花時間學習這種語言將為您的職業帶來奇跡,并使您與其他開發人員區分開。您會更輕松地學習新語言,形成真正的解決問題的技能,并在編程的基礎上打下堅實的基礎。 C ++將幫助您養成良好的編程習慣(即清晰一致的編碼風格,在撰寫代碼時注釋代碼,并限制類內部的可見性),并且由 ......

    uj5u.com 2020-09-10 01:00:41 more
最新发布
  • Rust中的智能指標:Box<T> Rc<T> Arc<T> Cell<T> RefCell<T> Weak

    Rust中的智能指標是什么 智能指標(smart pointers)是一類資料結構,是擁有資料所有權和額外功能的指標。是指標的進一步發展 指標(pointer)是一個包含記憶體地址的變數的通用概念。這個地址參考,或 ” 指向”(points at)一些其 他資料 。參考以 & 符號為標志并借用了他們所 ......

    uj5u.com 2023-04-20 07:24:10 more
  • Java的值傳遞和參考傳遞

    值傳遞不會改變本身,參考傳遞(如果傳遞的值需要實體化到堆里)如果發生修改了會改變本身。 1.基本資料型別都是值傳遞 package com.example.basic; public class Test { public static void main(String[] args) { int ......

    uj5u.com 2023-04-20 07:24:04 more
  • [2]SpinalHDL教程——Scala簡單入門

    第一個 Scala 程式 shell里面輸入 $ scala scala> 1 + 1 res0: Int = 2 scala> println("Hello World!") Hello World! 檔案形式 object HelloWorld { /* 這是我的第一個 Scala 程式 * 以 ......

    uj5u.com 2023-04-20 07:23:58 more
  • 理解函式指標和回呼函式

    理解 函式指標 指向函式的指標。比如: 理解函式指標的偽代碼 void (*p)(int type, char *data); // 定義一個函式指標p void func(int type, char *data); // 宣告一個函式func p = func; // 將指標p指向函式func ......

    uj5u.com 2023-04-20 07:23:52 more
  • Django筆記二十五之資料庫函式之日期函式

    本文首發于公眾號:Hunter后端 原文鏈接:Django筆記二十五之資料庫函式之日期函式 日期函式主要介紹兩個大類,Extract() 和 Trunc() Extract() 函式作用是提取日期,比如我們可以提取一個日期欄位的年份,月份,日等資料 Trunc() 的作用則是截取,比如 2022-0 ......

    uj5u.com 2023-04-20 07:23:45 more
  • 一天吃透JVM面試八股文

    什么是JVM? JVM,全稱Java Virtual Machine(Java虛擬機),是通過在實際的計算機上仿真模擬各種計算機功能來實作的。由一套位元組碼指令集、一組暫存器、一個堆疊、一個垃圾回收堆和一個存盤方法域等組成。JVM屏蔽了與作業系統平臺相關的資訊,使得Java程式只需要生成在Java虛擬機 ......

    uj5u.com 2023-04-20 07:23:31 more
  • 使用Java接入小程式訂閱訊息!

    更新完微信服務號的模板訊息之后,我又趕緊把微信小程式的訂閱訊息給實作了!之前我一直以為微信小程式也是要企業才能申請,沒想到小程式個人就能申請。 訊息推送平臺🔥推送下發【郵件】【短信】【微信服務號】【微信小程式】【企業微信】【釘釘】等訊息型別。 https://gitee.com/zhongfuch ......

    uj5u.com 2023-04-20 07:22:59 more
  • java -- 緩沖流、轉換流、序列化流

    緩沖流 緩沖流, 也叫高效流, 按照資料型別分類: 位元組緩沖流:BufferedInputStream,BufferedOutputStream 字符緩沖流:BufferedReader,BufferedWriter 緩沖流的基本原理,是在創建流物件時,會創建一個內置的默認大小的緩沖區陣列,通過緩沖 ......

    uj5u.com 2023-04-20 07:22:49 more
  • Java-SpringBoot-Range請求頭設定實作視頻分段傳輸

    老實說,人太懶了,現在基本都不喜歡寫筆記了,但是網上有關Range請求頭的文章都太水了 下面是抄的一段StackOverflow的代碼...自己大修改過的,寫的注釋挺全的,應該直接看得懂,就不解釋了 寫的不好...只是希望能給視頻網站開發的新手一點點幫助吧. 業務場景:視頻分段傳輸、視頻多段傳輸(理 ......

    uj5u.com 2023-04-20 07:22:42 more
  • Windows 10開發教程_編程入門自學教程_菜鳥教程-免費教程分享

    教程簡介 Windows 10開發入門教程 - 從簡單的步驟了解Windows 10開發,從基本到高級概念,包括簡介,UWP,第一個應用程式,商店,XAML控制元件,資料系結,XAML性能,自適應設計,自適應UI,自適應代碼,檔案管理,SQLite資料庫,應用程式到應用程式通信,應用程式本地化,應用程式 ......

    uj5u.com 2023-04-20 07:22:35 more