綠色認證棕櫚油需要支持 監督機構訂新規 企業採購量不足將受罰

環境資訊中心綜合外電;姜唯 編譯;林大利 審校

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

Java併發包JUC核心原理解析

CS-LogN思維導圖:記錄CS基礎 面試題
開源地址:https://github.com/FISHers6/CS-LogN

JUC

分類

線程管理

  • 線程池相關類

    • Executor、Executors、ExecutorService
    • 常用的線程池:FixedThreadPool、CachedThreadPool、ScheduledThreadPool、SingleThreadExecutor
  • 能獲取子線程的運行結果

    • Callable、Future、FutureTask

併發流程管理

  • CountDwonLatch、CyclicBarrier、Semaphore、Condition

實現線程安全

  • 互斥同步(鎖)

    • Synchronzied、及工具類Vector、Collections
    • Lock接口的相關類:ReentrantLock、讀寫鎖
  • 非互斥同(原子類)

    • 原子基本類型、引用類型、原子升級、累加器
  • 併發容器

    • ConcurrentHashMap、CopyOnWriteArrayList、BlockingQueue
  • 無同步與不可變方案

    • final關鍵字、ThreadLocal棧封閉

線程池

使用線程池的作用好處

  • 降低資源消耗

    • 重複利用已創建的線程降低線程創建和銷毀造成的消耗
  • 提高響應速度

    • 任務到達,可以不需要等到線程創建就能立即執行
  • 提高線程的可管理性

    • 使用線程池可以進行統一的分配,調優和監控

線程池的參數

  • corePoolSize、maximumPoolSize、keepAliveTime、workQueue、threadFactory、handler

  • 圖示

常用線程池的創建與規則

  • 線程添加規則

    • 1.如果線程數量小於corePoolSize,即使工作線程處於空閑狀態,也會創建一個新線程來運行新任務,創建方法是使用threadFactory

    • 2.如果線程數量大於corePoolSize但小於maximumPoolSize,則將任務放入隊列

    • 3.如果workQueue隊列已滿,並且線程數量小於maxPoolSize,則開闢一個非核心新線程來運行任務

    • 4.如果隊列已滿,並且線程數大於或等於maxPoolSize,則拒絕該任務,執行handler

    • 圖示(分別與3個參數比較)

  • 常用線程池

    • newFixedThreadPool

      • 創建固定大小的線程池,使用無界隊列會發生OOM
    • newSingleThreadExecutor

      • 創建一個單線程的線程池,線程數為1
    • newCachedThreadPool

      • 創建一個可緩存的線程池,60s會回收部分空閑的線程。採用直接交付的隊列 SynchronousQueue ,隊列容量為0,來一個創建一個線程
    • newScheduledThreadPool

      • 創建一個大小無限的線程池。此線程池支持定時以及周期性執行任務的需求
  • 如何設置初始化線程池的大小?

    • 可根據線程池中的線程
      處理任務的不同進行分別估計

      • CPU 密集型任務

        • 大量的運算,無阻塞
          通常 CPU 利用率很高
          應配置盡可能少的線程數量
          設置為 CPU 核數 + 1
      • IO 密集型任務

        • 這類任務有大量 IO 操作
          伴隨着大量線程被阻塞
          有利於并行提高CPU利用率
          配置更多數量: CPU 核心數 * 2
  • 使用線程池的注意事項

    • 1.避免任務堆積(無界隊列會OOM)、2.避免線程數過多(cachePool直接交付隊列)、3.排查線程泄露

線程池的狀態和常用方法

  • 線程池的狀態

    • RUNNING(接受並處理任務中)、
      SHUTDOWN(不接受新任務但處理排隊任務)、
      STOP(不接受新任務 也不處理排隊任務 並中斷正在進行的任務)、
      TIDYING、TEMINATED(運行完成)
  • 線程池停止

    • shutdown

      • 通知有序停止,先前提交的任務務會執行
    • shutdownNow

      • 嘗試立即停止,忽略隊列里等待的任務

線程池的源碼解析

  • 線程池的組成

    • 1.線程池管理器
      2.工作線程
      3.任務隊列:無界、有界、直接交付隊列
      4.任務接口Task

    • 圖示

  • Executor家族

    • Executor頂層接口,只有一個execute方法

    • ExecutorService繼承了Executor,增加了一些新的方法,比如shutdown擁有了初步管理線程池的功能方法

    • Executors工具類,來創建,類似Collections

    • 圖示

  • 線程池實現任務復用的原理

    • 線程池對線程作了包裝,不需要啟動線程,不需要重複start線程,只是調用已有線程固定數量的線程來跑傳進來的任務run方法

    • 添加工作線程

      • 4步:1. 獲取線程池狀態、4.判斷是否進入任務隊列 3.根據狀態檢測是否增加工作線程4.執行拒絕handler
    • 重複利用線程執行不同的任務

面試題

  • 為什麼要使用線程池?
  • 如何使用線程池?
  • 線程池有哪些核心參數?
  • 初始化線程池的大小的如何算?
  • shutdown 和 shutdownNow 有什麼區別?

ThreadLocal

ThreadLocal的作用好處

  • 為每個線程提供存儲自身獨立的局部變量,實現線程間隔離
  • 即:達到線程安全,不需要加鎖節省開銷,減少參數傳遞

ThreadLocal的使用場景

  • 1.每個線程需要一個獨享的對象,如 線程不安全的工具類,(線程隔離)
  • 2.每個線程內需要保存全局變量,如 攔截器中的用戶信息參數,讓不同方法直接使用,避免參數傳遞過多,(局部變量安全,參數傳遞)

ThreadLocal的實現原理

  • 每個 Thread 維護着一個 ThreadLocalMap 的引用;ThreadLocalMap 是 ThreadLocal 的內部類,用 Entry 來進行存儲;key就對應一個個ThreadLocal

  • get方法:取出當前線程的ThreadLocalMap,然後調用map.getEntry方法,把ThreadLocal作為key參數傳入,取出對應的value

  • set方法:往 ThreadLocalMap 設置ThreadLocal對應值
    initalValue方法:延遲加載,get的時候設置初始化

  • 圖示

缺陷注意

  • value內存泄漏

    • 原因:ThreadLocal 被 ThreadLocalMap 中的 entry 的 key 弱引用。如果 ThreadLocal 沒有被強引用, 那麼 GC 時 Entry 的 key 就會被回收,但是對應的 value 卻不會回收,就會造成內存泄漏

    • 解決方案:每次使用完 ThreadLocal,都調用它的 remove () 方法,清除value數據。

    • 源碼圖示

面試題

  • ThreadLocal 的作用是什麼?
  • 講一講ThreadLocal的實現原理(組成結構)
  • ThreadLocal有什麼風險?

Callable與Future

Callable

  • 引入目的

    • 解決Runnable的缺陷

      • 1.沒有返回值,因為返回類型為void
      • 2.不能拋出異常,因為沒有繼承Execption接口
  • 是什麼如何使用

    • Callable是類似於Runnable的接口,實現Callable接口的類和實現Runnable的類都是可被其它線程執行的任務。
    • 實現Call方法,可以有返回值

Future

  • 引入目的

    • Future的核心思想是:一個方法的計算過程可能非常耗時,一直在原地等待方法返回,顯然不明智。可以把該計算過程放到子線程去執行,並通過Future去控制方法的計算過程,在計算出結果后直接獲取該結果。
  • 常用方法

    • get方法:獲取結果,在沒有計算出結果前,會進入阻塞態
  • 使用場景

    • 用法1:線程池的submit方法返回Future對象
    • 用法2:用FutureTask來創建Future
  • 注意點

    • 當for循環批量獲取future的結果時,容易block,get方法調用時應使用timeout限制
    • Future和Callable的生命周期不能後退
  • Callable和Future的關係

    • Future相當於一個存儲器,它存儲未來call()任務方法的返回值結果

    • 可以用Future.get方法來獲取Callable接口的執行結果,在call()未執行完畢之前沒調用get的線程會被阻塞

    • 線程池傳入Callable,submit返回Future,get獲取值

  • FutureTask

    • FutureTask是一種包裝器,可以把Callable轉化成Future和Runnable,它同時實現了二者的接口。所以既可以作為Runnable任務被線程執行,又可以作為Future得到Callable的返回值

    • 圖示

final與不變性

什麼是不變性(Immutable)

  • 如果對象在被創建后,狀態就不能被修改,那麼它就是不可變的。
  • 具有不變性的對象一定是線程安全的,我們不需要對其採取任何額外的安全措施,也能保證線程安全。

final的作用

  • 類防止被繼承、方法防止被重寫、變量防止被修改
  • 天生是線程安全的(因為不能修改),而不需要額外的同步開銷

final的3種用法:修飾變量、方法、類

  • final修飾變量

    • 被final修飾的變量,意味着值不能被修改。
      如果變量是對象,那麼對象的引用不能變,但是對象自身的內容依然可以變化。

    • 賦值時機

      • 屬性被聲明為final后,該變量則只能被賦值一次。且一旦被賦值,final的變量就不能再被改變,如論如何也不會變。

      • 區分為3種

        • final instance variable(類中的final屬性)

          • 等號右側、構造函數、初始化代碼塊
        • final static variable(類中的static final屬性)

          • 等號右側、靜態初始化代碼塊
        • final local variable(方法中的final變量)

          • 使用前複製即可
      • 為什麼規定時機

        • 根據JVM對類和成員變量、靜態成員變量的加載規則來看:如果初始化不賦值,後續賦值,就是從null變成新的賦值,這就違反final不變的原則了!
  • final修飾方法(構造方法除外)

    • 不可被重寫,也就是不能被override,即便是子類有同樣名字的方法,那也不是override,與static類似*
  • final修飾類

    • 不可被繼承,例如典型的String類就是final的

棧封閉 實現線程安全

  • 在方法里新建的局部便咯,實際上是存儲在每個線程私有的棧空間,線程棧不能被其它線程訪問,所以不會有線程安全問題,如ThreadLocal

面試題

CAS

什麼是CAS

  • 我認為V的值應該是A,如果是的話那我就把它改成B,如果不是A(說明被別人修改過了),那我就不修改了,避免多人同時修改導致出錯。
  • CAS有三個操作數:內存值V、預期值A、要修改的值B,當且僅當預期值A和內存值V相同時,才將內存值修改為B,否則什麼都不做。最後返回現在的V值。
  • 最終執行CPU處理機提供的的原子指令

缺點

  • ABA問題

    • 我認為 V的值為A,有其它線程在這期間修改了值為B,但它又修改成了A,那麼CAS只是對比最終結果和預期值,就檢測不出是否修改過
  • CAS+自旋,導致自旋時間過長

  • 改進:通過版本號的機制來解決。每次變量更新的時候,版本號加 1,如AtomicStampedReference的compareAndSet ()

應用場景

  • 1 樂觀鎖:數據庫、git版本號; 自旋 2 concurrentHashMap:CAS+自旋
    3 原子類

CAS底層實現

  • 通過Unsafe獲取待修改變量的內存遞增,
    比較預期值與結果,調用彙編cmpxchg指令

以AtomicInteger為例,分析在Java中是如何利用CAS實現原子操作的?

  • 1.使用Unsafe類拿到value的內存遞增,通過偏移量 直接操作內存數據
  • 2.Unsafe的getAndAddInt方法,使用CAS+自旋嘗試修改數據
  • CAS的參數通過 預期值 與 實際拿到的值進行比較,相同就修改,不相同就自旋
  • Unsafe提供硬件級別的原子操作,最終調用原子彙編指令的cmpxchg指令

鎖的分類

Lock鎖接口

  • 簡介

    • Lock鎖是一種工具,用於控制對共享資源的訪問
    • 如:ReentrantLock
  • Lock和Synchronized的異同點

    • 相同點

      • 都能達到線程安全的目的
    • 不同點

      • Lock 有比 synchronized 更精確的線程語義和更好的性能;高級功能

      • 1 實現原理不同

        • Synchronized 是關鍵字,屬於 JVM 層面,底層是通過 monitorenter 和 monitorexit 完成,依賴於 monitor 對象來完成;
        • Lock 是 java.util.concurrent.locks.lock 包下的,底層是AQS
      • 2 靈活性不同

        • Synchronized 代碼完成之後系統自動讓線程釋放鎖;ReentrantLock 需要用戶手動釋放鎖,加鎖解鎖靈活
      • 3 等待時是否可以中斷

        • Synchronized 不可中斷,除非拋出異常或者正常運行完成;ReentrantLock 可以中斷。一種是通過 tryLock,另一種是 lockInterruptibly () 放代碼塊中,調用 interrupt () 方法進行中斷;
  • 可見性

    • happens-before規則約定;Lock與Synchronized一致都可以保證可見性
    • 即下一個線程加鎖時可以看到上一個釋放鎖的線程發生的所有操作

樂觀鎖與悲觀鎖

  • 悲觀鎖(互斥同步鎖)

    • 思想

      • 鎖住數據,讓別人無法訪問,確保數據萬無一失
    • 實例

      • Synchronized、Lock相關類
      • 應用實例:select 把庫鎖住,屬於悲觀鎖,更新期間其它人不能修改
    • 缺點

      • 在阻塞和喚醒性能開銷大(用戶態核心態切換、上下文切換、檢查是否有線程被喚醒)
      • 持有鎖的線程被阻塞時無法釋放,有可能造成永久阻塞
  • 樂觀鎖

    • 思想

      • 認為自己在操作數據時不會有其它線程干擾,所以不需要鎖住被操作對象
      • 在更新數據的時候,去對比修改期間有沒有被其它人改變過,沒改過就正常修改(類似CAS思想)
      • 樂觀鎖一般由CAS實現:CAS在一個原子操作內把數據對比且交換,在此期間不能被打斷的
    • 實例

      • 原子類、併發容器
      • 應用實例:數據庫版本號控制、git版本號
    • 優缺點對比

      • 悲觀鎖一旦切換就不用再考慮切換CPU等操作了,一勞永逸,開銷固定
      • 樂觀鎖,會一步步嘗試自旋來獲取鎖,自旋開銷
  • 對比

可重入鎖與非可重入鎖

  • 什麼是可重入

    • 拿到鎖的線程又請求這把鎖,允許通過
  • 可重入的好處

    • 避免死鎖(拿到鎖的線程內部又請求該鎖)
    • 提升封裝性,避免一次次加鎖
  • 可重入鎖ReentrantLock與非可重入鎖ThreadPoolExecutor的Worker類對比

公平鎖和非公平鎖

  • 公平鎖

    • 介紹

      • 公平鎖是指多個線程按照申請鎖的順序來獲取鎖,線程直接進入隊列中排隊,隊列中的第一個線程才能獲得鎖
    • 優點

      • 公平鎖的優點是公平執行,等待鎖的線程不會餓死
    • 缺點

      • 缺點是整體吞吐效率相對非公平鎖要低,等待隊列中除第一個線程以外的所有線程都會阻塞,CPU喚醒阻塞線程的開銷比非公平鎖大
  • 非公平鎖

    • 介紹

      • 多個線程加鎖時直接嘗試獲取鎖,獲取不到才會到等待隊列的隊尾等待。但如果此時鎖剛好可用,那麼這個線程可以無需阻塞直接獲取到鎖,所以非公平鎖有可能出現后申請鎖的線程先獲取鎖的場景
    • 優點

      • 減少喚起線程的開銷,整體的吞吐效率高,因為線程有幾率不阻塞直接獲得鎖,CPU不必喚醒所有線程
    • 缺點

      • 處於等待隊列中的線程可能會餓死,或者等很久才會獲得鎖
  • 優缺點對比

  • 源碼分析

共享鎖和排他鎖

  • 排他鎖

    • 介紹

      • 排他鎖,獲取鎖后,既能讀又能寫,但是此時其它線程不能獲取這個鎖了,只能由當前線程修改數據獨享鎖,保證了線程安全,synchronized
      • 又稱為 獨佔鎖,寫鎖
  • 共享鎖

    • 介紹

      • 獲取共享鎖后,其它線程也可以獲取共享鎖完成讀操作,但都不能修改刪除數據
      • 又成為 讀鎖
  • ReentrantReadWriteLock

    • 讀寫鎖的作用

      • 共享鎖減少了多個讀都加鎖的開銷,線程也安全
      • 在讀的地方使用讀鎖,在寫的地方寫鎖;在沒有寫鎖的情況下,讀操作無阻塞,提高程序效率
    • 讀寫鎖的規則

      • 要麼可以多讀,要麼只能一寫
      • 讀寫鎖只是一把鎖,可以通過兩個方式鎖定:讀鎖定 或 寫鎖定
    • 一把鎖兩種方式鎖定

      • readLock() 讀鎖
      • writeLock() 寫鎖
    • 讀線程插隊策略(非公平下)

      • 寫鎖可以隨時插隊,參与競爭
      • 讀鎖僅在等待隊列頭節點為寫的時候不允許插隊;當隊頭為讀的時候可以去插隊。
    • 鎖升級

      • 引入場景

        • 假如一開始持有寫鎖,但我寫需求完了,後面都是讀的需求了,如果還佔用寫鎖就浪費資源開銷
      • 策略

        • 只允許降級,不允許升級
    • 適合場景

      • 讀多寫少,提高併發效率

自旋鎖和阻塞鎖

  • 阻塞鎖

    • 思想

      • 沒拿到鎖之前,會直接把線程阻塞,直到被喚醒
    • 開銷缺陷

      • 阻塞或喚醒一個線程需要操作系統切換CPU狀態來完成,恢復現場等需要消耗處理機時間;如果同步代碼塊的內容過於簡單,狀態轉換消耗的時間有可能比用戶代碼執行的時間還要長,得不償失
  • 自旋鎖

    • 思想

      • 讓當前搶鎖失敗的線程進行自旋,如果在自旋完成后前面鎖定同步資源的線程已經釋放了鎖,那麼當前線程就可以不必阻塞而是直接獲取同步資源,從而避免切換線程的開銷
    • 開銷缺陷

      • 自旋佔用時間長,起始開銷低,但消耗CPU資源開銷會線性增長
  • 源碼分析

    • atomic包下的類基本都是自旋鎖的實現

    • AtomicInteger的實現:自旋鎖實現原理是CAS,Atomic調用Unsafe進行自增add的源碼中的do-while循環就是一個自旋操作,使用CAS如果修改過程中遇到其它線程修改導致沒有秀嘎四成功,就在while里死循環,直至修改成功

    • 圖示

  • 適用場景

    • 多核、臨界區短小

可中斷鎖

  • 介紹

    • 線程B等待線程A釋放鎖時,線程B不想等待了,想處理其它事情,我們可以中斷它
  • 使用場景

    • synchronized是不可中斷鎖,Lock是可中斷鎖(tryLock(time) 和 lockInterruptibly)響應中斷

鎖優化

  • JDK1.6 后對synchronized鎖的優化

    • JDK1.6 對鎖的實現引入了大量的優化,如偏向鎖、輕量級鎖、自旋鎖、適應性自旋鎖、鎖消除、鎖粗化等技術來減少鎖操作的開銷。

    • 偏向鎖

      • 無競爭條件下,消除整個同步互斥,連CAS都不操作;即這個鎖會偏向於第一個獲得它的線程
    • 輕量級鎖

      • 無競爭條件下,通過CAS消除同步互斥,減少傳統的重量級鎖使用操作系統互斥量產生的性能消耗。
    • 重量級鎖

      • 互斥同步鎖
    • 自旋鎖

      • 為了減少線程狀態改變帶來的消耗,不停地執行當前線程
    • 自適應自旋鎖

      • 自旋的時間不固定了,如設置自旋次數
    • 鎖消除

      • 不可能存在共享數據競爭的鎖進行消除;
    • 鎖粗化

      • 鎖粗化就是增大鎖的作用域;如解決加鎖操作在循環體內的頻開銷
  • 寫代碼時的優化

    • 縮小同步代碼塊、如不要鎖住方法
    • 減少鎖的請求次數, 如一批一批請求
    • 參考LongAdder的思想,每個段有自己的計數器,最後才合併

面試題

  • 什麼是公平鎖?什麼是非公平鎖?
  • 自旋鎖解決什麼問題?自旋鎖的原理是什麼?自旋的缺點?
  • 說說 JDK1.6 之後的synchronized 關鍵字底層做了哪些優化,可以詳細介紹一下這些優化嗎?
  • 說說 synchronized 和 java.util.concurrent.locks.Lock 的異同?

原子類atomic包

原子類的作用

  • 原子類的作用和鎖類似,都是為了保證併發下線程安全
  • 粒度更細,變量級別
  • 效率更高,除了高度競爭外

原子類的種類

  • Atomic*基本類型原子類:AtomicInteger、AtomicLong、AtomicBoolean
  • Atomic*Array數組類型原子類:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
  • Atomic*Reference 引用類型原子類:AtomicReference等
  • AtomicIntegerFiledUpdate等升級類型原子類
  • Adder累加器、Accumlator累加器

AtomicInteger

  • 常用方法

    • get、getAndSet、getAndIncrement、compareAndSet(int expect,int update)
  • 實現原理

    • AtomicInteger 內部使用 CAS 原子語義來處理加減等操作。CAS通過判斷內存某個位置的值是否與預期值相等,如果相等則進行值更新
    • CAS 是內部是通過 Unsafe 類實現,而 Unsafe 類的方法都是 native 的,在 JNI 里是藉助於一個 CPU 指令完成的,屬於原子操作。
  • 缺點

    • 循環開銷大。如果 CAS 失敗,會一直嘗試
    • 只能保證單個共享變量的原子操作,對於多個共享變量,CAS 無法保證,引出原子引用類
    • 用CAS存在 ABA 問題

Adder累加器

  • 引入目的/改進思想

    • AtomicLong在每一次加法都要flush和refresh主存,與JMM內存模型有關。工作線程之間不能直接通信,需要通過主內存間接通信
  • 設計思想

    • Java8引入,高併發下LongAdder比AtomicLong效率高,本質是空間換時間
    • 競爭激烈時,LongAdder把不同線程對應到不同的Cell單元上進行修改,降低了衝突的概率,是多段鎖的理念,提高了併發性
    • 每個線程都有自己的一個計數器,不存在競爭
    • sum源碼分析:最終把每一個Cell的計數器與base主變量相加

面試題

  • AtomicInteger 怎麼實現原子操作的?
  • AtomicInteger 有哪些缺點?

併發容器

ConcurrentHashMap

  • 集合類歷史

    • Vector的方法被synchronizd修飾,同步鎖;不允許多個線程同時執行。併發量大的時候性能不好
    • Hashtable是線程安全的HashMap,方法也是被synchronized修飾,同步但併發性能差
    • Collections工具類,提高的有synchronizedList和synchronizedMap,代碼內使用sync互斥變量加鎖
  • 為什麼需要

    • 為什麼不用HashMap

      • 1.多線程下同時put碰撞導致數據丟失
      • 2.多線程下同時put擴容導致數據丟失
      • 3.死循環造成的CPU100%
    • 為什麼不用Collection.synchronizedMap

      • 同步鎖併發性能低
  • 數據結構與併發策略

    • JDK1.7

      • 數組+鏈表,拉鏈法解決衝突
      • 採用分段鎖,每個數組結點是一個獨立的ReentrantLock鎖,可以支持同時併發寫
    • JDK1.8

      • 數組+鏈表+紅黑樹,拉鏈法和樹化解決衝突
      • 採用CAS+synchronized鎖細化
    • 1.7到1.8改變後有哪些優點

      • 1.數據結構由鏈表變為紅黑樹,樹查詢效率更高
      • 2.減少了Hash碰撞,1.7拉鏈法
      • 3.保證了併發安全和性能,分段鎖改成CAS+synchronized
      • 為什麼超過8要轉為紅黑樹,因為紅黑樹存儲空間是結點的兩倍,經過泊松分佈,8衝突概率低
  • 注意事項

    • 組合操作線程不安全,應使用putIfAbsent提供的原子性操作

CopyOnWriteArrayList

  • 引入目的

    • Vector和SynchronizedList鎖的粒度太大併發效率低,並且迭代時無法編輯exceptMod!=Count
  • 適合場景

    • 讀多寫少,如黑名單管理每日更新
  • 讀寫規則

    • 是對讀寫鎖的升級:讀取完全不用加鎖,讀時寫入也不會阻塞。只有寫入和寫入之間需要同步
  • 實現原理

    • 創建數據的新副本,實現讀寫分離,修改時整個副本進行一次複製,完成后最後再替換回去;由於讀寫分離,舊容器不變,所以線程安全無需鎖
    • 在計算機內存中修改不直接修改主內存,而是修改緩存(cache、對拷貝的副本進行修改),再進行同步(指針指向新數據)。
  • 缺點

    • 1.數據一致性問題,拷貝不能保證數據實時一致,只能保證數據最終一致性
    • 2.內存佔用問題,寫複製機制,寫操作時內存會同時駐紮兩個對象的內存

併發隊列

  • 為什麼使用隊列

    • 用隊列可以在線程間傳遞數據,緩存數據
    • 考慮鎖等線程安全問題的重任轉移到了“隊列”上
  • 併發隊列關係圖示

  • BlockingQueue阻塞隊列

    • 阻塞隊列是局由自動阻塞功能的隊列,線程安全;take方法移除隊頭,若隊列無數據則阻塞直到有數據;put方法插入元素,如果隊列已滿就無法繼續插入則阻塞直到隊列里有了空閑空間

    • ArrayBlockQueue

      • 有界可指定容量、可公平
      • Put源碼加鎖,可中斷的上鎖方法。沒滿才可以入隊,否則一直await等待。
    • LinkedBlockingQueue

      • 無界容量為MAX_VALUE,內部結構Node
      • 使用了兩把鎖take鎖和put鎖互補干擾
    • PriorityBlockingQueue

      • 支持優先級,無界隊列
    • SynchronousQueue

      • 直接傳遞的隊列,容量0,效率高線程池的CacheExecutorPool使用其作為工作隊列
    • DelayQueue

      • 無界隊列,根據延遲時間排序
  • 非阻塞隊列

    • ConcurrentLinkedQueue

      • 使用鏈表作為隊列存儲結構
      • 使用Unsafe的CAS非阻塞方法來實現線程安全,無需阻塞,適合對性能要求較高的併發場景
  • 選擇合適的隊列

    • 邊界上看

      • ArrayBlockQueue有界;LinkedBlockQueue無界適合容量大容量激增
    • 內存上看

      • ArrayBlockQueue內部結構是array,從內存存儲上看,連續存儲更加整齊。而LinkedBlockQueue採用鏈表結點,可以非連續存儲。
    • 吞吐量上看

      • 從性能上看LinkedBlockQueue的put鎖和鎖分開,鎖粒度更細,所以優於ArrayBlockQueue

總結併發容器對比

  • 分為3類:Concurrent、CopyOnWrite、Blocking*
  • Concurrent*的特定是大部分使用CAS併發;而CopyOnWrite通過複製一份元數據寫加鎖實現;Blocking通過ReentLock鎖底層AQS實現

併發流程控制工具類

控制併發流程工具類的作用

  • 控制併發流程的工具類,作用是幫助程序員更容易讓線程之間相互配合,來滿足業務邏輯

  • 併發工具類圖示

CountDownLatch倒計時門閂

  • 作用(事件)

    • 一個線程等多個線程、或多個線程等一個線程完成到達,才能繼續執行
  • 常用方法

    • 構造函數中傳入倒數值、await、countDown

Semaphore信號量

  • 作用

    • 用來限制管理數量有限的資源的使用情況,相當於一定數量的“許可證”
  • 常用方法

    • 構造函數中傳入數量、acquire、release

Condition條件對象

  • 作用

    • 等待條件滿足才放行,否則阻塞;一個鎖可以對應多個條件
  • 常用方法

    • lock.newCondition、await、signal

CyclicBarrier循環柵欄

  • 作用(線程)

    • 多個線程互相等待,直到達到同一個同步點(屏障),再繼續一起執行
  • 常用方法

    • 構造函數中傳入個數、await

AQS

AQS的作用

  • AQS是一個用於構建鎖、同步器、協作工具類的框架,有了AQS后,更多的協作工具類都可以很方便的寫出來

AQS的應用場景

  • Exclusive(獨佔)

    • ReentrantLock 公平和非公平鎖
  • Share(共享)

    • Semaphore/CountDownLatch/CyclicBarrier

AQS原理解析

  • 核心三要素

    • 1.sate

      • 使用一個 int 成員變量來表示同步狀態 state,被volatile修飾,會被併發修改,各方法如getState、setState等使用CAS保證線程安全
      • 在ReentrantLock中,表示可重入的次數
      • 在Semaphore中,表示剩餘許可證信號的數量
      • 在CountDownLatch中,表示還需要倒數的個數
    • 2.控制線程搶鎖和配合的FIFO隊列

      • 獲取資源線程的排隊工作
    • 3.期望協作工具類去實現的“獲取/釋放”等喚醒分配的方法策略

  • AQS的用法

    • 第一步:寫一個類,想好協作的邏輯,實現獲取/釋放方法
    • 第二步:內部寫一個Sync類繼承AbstractQueueSynchronizer
    • 第三步:Sync類根據獨佔還是共享重寫tryAcquire/tryRelease或tryAcquireShared和tryReleaseShared等方法,在之前寫的獲取/釋放方法中調用AQS的acquire/release或則Shared方法

AQS應用實例源碼解析

  • AQS在CountDownLatch的應用

    • 內部類Sync繼承AQS

    • 1.state表示門閂倒數的count數量,對應getCount方法獲取

    • 2.釋放方法,countDown方法會讓state減1,直到減為0時就喚醒所有線程。countDown方法調用releaseShared,它調用sync實現的tryReleaseShared,其使用CAS+自旋鎖,來實現安全的計數-1

    • 3.阻塞方法,await會調用sync提供的aquireSharedInterruptly方法,當state不等於0時,最終調用LockUpport的park,它利用Unsafe的park,native方法,把線程加入阻塞隊列

    • 總結

  • AQS在Semphore的應用

    • state表示信號量允許的剩餘許可數量

    • tryAcquire方法,判斷信號量大於0就成功獲取,使用CAS+自旋改變state狀態。如果信號量小於0了,再請求時tryAcquireShared返回負數,調用aquireSharedInterruptly方法就進入阻塞隊列

    • release方法,調用sync實現的releaseShared,會利用AQS去阻塞隊列喚醒一個線程

    • 總結

  • AQS在ReentrantLock的應用

    • state表示已重入的次數,獨佔鎖權保存在AQS的Thread類型的exclusiveOwnerThread變量中
    • 釋放鎖: unlock方法調用sync實現的release方法,會調用tryRelease,使用setState而不是CAS來修改重入次數state,當state減到0完全釋放鎖
    • 加鎖lock方法:調用sync實現的lock方法。CAS嘗試修改鎖的所有權為當前線程,如果修改失敗就要調用acquire方法再次嘗試獲取,acquire方法調用了AQS的tryAcquire,這個實現在ReentantLock的裏面,獲取失敗加入到阻塞隊列

通過AQS自定義同步器

  • 自定義同步器在實現時只需要根據業務邏輯需求,實現共享資源 state 的獲取與釋放方式策略即可
  • 至於具體線程等待隊列的維護(如獲取資源失敗入隊 / 喚醒出隊等),AQS 已經在頂層實現好了

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

Celery淺談

一、Celery 核心模塊

1. Brokers

brokers 中文意思為中間人,在這裏就是指任務隊列本身,接收生產者發來的消息即Task,將任務存入隊列。任務的消費者是Worker,Brokers 就是生產者和消費者存放/拿取產品的地方(隊列)。Celery 扮演生產者和消費者的角色。

常見的 brokers 有 rabbitmq、redis、Zookeeper 等。推薦用Redis或RabbitMQ實現隊列服務。

2. Workers

就是 Celery 中的工作者,執行任務的單元,類似與生產/消費模型中的消費者。它實時監控消息隊列,如果有任務就從隊列中取出任務並執行它。

3. Backend / Result Stores

用於存儲任務的執行結果。隊列中的任務運行完后的結果或者狀態需要被任務發送者知道,那麼就需要一個地方儲存這些結果,就是 Result Stores 了。

常見的 backend 有 redis、Memcached 甚至常用的數據庫都可以。

4. Tasks

就是想在隊列中進行的任務,有異步任務和定時任務。一般由用戶、觸發器或其他操作將任務入隊,然後交由 workers 進行處理。

5. Beat

定時任務調度器,根據配置定時將任務發送給Brokers。

二、Celery 基本使用

1.創建一個celery application 用來定義你的任務列表,創建一個任務文件就叫tasks.py吧。

from celery import Celery
 
# 配置好celery的backend和broker
app = Celery('task1',  backend='redis://127.0.0.1:6379/0', broker='redis://127.0.0.1:6379/0')
  
#普通函數裝飾為 celery task
@app.task 
def add(x, y):
    return x + y

如此而來,我們只是定義好了任務函數func函數和worker(celery對象)。worker相當於工作者。

2.啟動Celery Worker來開始監聽並執行任務。broker 我們有了,backend 我們有了,task 我們也有了,現在就該運行 worker 進行工作了,在 tasks.py 所在目錄下運行:

[root@localhost ~]# celery -A tasks worker --loglevel=info    # 啟動方法1
[root@localhost ~]# celery -A tasks worker --l debug          # 啟動方法2

現在 tasks 這個任務集合的 worker 在進行工作(當然此時broker中還沒有任務,worker此時相當於待命狀態),如果隊列中已經有任務了,就會立即執行。

3.調用任務:要給Worker發送任務,需要調用 delay() 方法。

import time
from tasks import add
 
# 不要直接add(6, 6),這裏需要用 celery 提供的接口 delay 進行調用
result = add.delay(6, 6)
while not result.ready():
    time.sleep(1)
print('task done: {0}'.format(result.get()))

三、Celery 進階使用

1.celery_config.py:配置文件

from __future__ import absolute_import, unicode_literals
#從python的絕對路徑導入而不是當前的腳本     #在python2和python3做兼容支持的
 
BROKER_URL = 'redis://127.0.0.1:6379/0'
# 指定結果的接受地址
CELERY_RESULT_BACKEND = 'redis://127.0.0.1:6379/0'

2.tasks.py

from __future__ import absolute_import, unicode_literals
#從python的絕對路徑導入而不是當前的腳本     #在python2和python3做兼容支持的
from celery import Celery
 
# 配置好celery的backend和broker, task1:app的名字。broker
app = Celery('task1',                              #
             broker='redis://127.0.0.1:6379/0',   # 消息隊列:連rabbitmq或redis
             backend='redis://127.0.0.1:6379/0')  # 存儲結果:redis或mongo或其他數據庫
  
app.config_from_object("celery_config")
app.conf.update(         # 給app設置參數
    result_expires=3600, # 保存時間為1小時
)
 
#普通函數裝飾為 celery task
@app.task 
def add(x, y):
    return x + y
     
if __name__ == '__main__':
    app.start()

3.啟動worker

[root@localhost ~]``# celery -A tasks worker --loglevel=info

4.test.py

import time
from tasks import add
 
# 不要直接add(4, 4),這裏需要用 celery 提供的接口 delay 進行調用
result = add.delay(6, 6)
print(result.id)
while not result.ready():
    time.sleep(1)
print('task done: {0}'.format(result.get()))

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

助力新能源車 意法半導體推新款車規碳化矽二極體

意法半導體推出新款車規碳化矽(SiC)二極體,以滿足電動汽車和插電式混合動力車(PHEVs,Plug-in Hybrids)等新能源汽車對車載充電器(OBCs,on-board battery chargers)在有限空間內處理大功率的苛刻要求。  

  新款二極體採用先進的技術可防止高電流突波燒毀裝置,其過電流保護是額定電流的2.5倍,因此設計人員可選用更小、更經濟實惠且可靠性和效能都不會受到影響的電流更小的二極體。此新碳化矽二極體通過車規產品測試,反向擊穿電壓提高到650V,能滿足設計人員和汽車廠商欲降低電壓補償係數的要求,以確保車載充電半導體元件的標準與瞬間峰值電壓之間有充足的安全邊際。   這次推出的650V二極體包括TO-220AC功率封裝的10A STPSC10H065DY和TO-220AC封裝的12A STPSC12H065DY。此外,TO-220AB封裝的STPSC20H065CTY和TO-247封裝的STPSC20H065CWY是內建2個10A二極體的雙二極體(dual-diode )產品,可最大幅度地提升空間利用度並減少車載充電器的重量。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

擴大電動車布局 鴻海入股中國和諧汽車

鴻海於 12 月 22 日公告,以台幣約 24.84 億元入股中國和諧汽車(China Harmony Auto Holding Limited),取得 10.526% 的持股,未來雙方將展開新能源、電動車的通路合作。   和諧汽車為中國進口高級品牌汽車的第二大經銷商,擁有 25 個銷售據點,旗下代理品牌超過 10 個,包括寶馬、勞斯萊斯,MINI、奧迪、法拉利、瑪莎拉蒂等等。   而鴻海集團近來在電動車市場投資與布局也動作頻頻,鴻海集團總裁郭台銘 12 月初率領近 20 人的專家團隊,前往河南首三門峽市的速達電動汽車公司進行實地考察,並駕駛了速達電動車,正式開啟了合作契機。緊接著,富士康與北汽合資電動車租賃公司也傳出獲得中國科技局採用的好消息,成功打入當地電動車租賃市場。   這次,由鴻海主動發布重大訊息,代子公司 Foxconn (Far East) Limited 公告為了與目標公司合作發展新事業,將投資和諧汽車,以港幣約 6.09 億元(約台幣 24.84 億元),每股港幣 4.73 元,取得和諧汽車股份,持股比例將達到 10.526%。   (照片來源:)

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

Java是如何實現Future模式的?萬字詳解!

JDK1.8源碼分析項目(中文註釋)Github地址:

https://github.com/yuanmabiji/jdk1.8-sourcecode-blogs

1 Future是什麼?

先舉個例子,我們平時網購買東西,下單後會生成一個訂單號,然後商家會根據這個訂單號發貨,發貨后又有一個快遞單號,然後快遞公司就會根據這個快遞單號將網購東西快遞給我們。在這一過程中,這一系列的單號都是我們收貨的重要憑證。

因此,JDK的Future就類似於我們網購買東西的單號,當我們執行某一耗時的任務時,我們可以另起一個線程異步去執行這個耗時的任務,同時我們可以干點其他事情。當事情幹完后我們再根據future這個”單號”去提取耗時任務的執行結果即可。因此Future也是多線程中的一種應用模式。

擴展: 說起多線程,那麼Future又與Thread有什麼區別呢?最重要的區別就是Thread是沒有返回結果的,而Future模式是有返回結果的。

2 如何使用Future

前面搞明白了什麼是Future,下面我們再來舉個簡單的例子看看如何使用Future。

假如現在我們要打火鍋,首先我們要準備兩樣東西:把水燒開和準備食材。因為燒開水是一個比較漫長的過程(相當於耗時的業務邏輯),因此我們可以一邊燒開水(相當於另起一個線程),一邊準備火鍋食材(主線程),等兩者都準備好了我們就可以開始打火鍋了。

// DaHuoGuo.java

public class DaHuoGuo {
	public static void main(String[] args) throws Exception {
		FutureTask<String> futureTask = new FutureTask<>(new Callable<String>() {
			@Override
			public String call() throws Exception {
				System.out.println(Thread.currentThread().getName() + ":" + "開始燒開水...");
				// 模擬燒開水耗時
				Thread.sleep(2000);
				System.out.println(Thread.currentThread().getName() + ":"  + "開水已經燒好了...");
				return "開水";
			}
		});

		Thread thread = new Thread(futureTask);
		thread.start();

		// do other thing
		System.out.println(Thread.currentThread().getName() + ":"  + " 此時開啟了一個線程執行future的邏輯(燒開水),此時我們可以干點別的事情(比如準備火鍋食材)...");
		// 模擬準備火鍋食材耗時
		Thread.sleep(3000);
		System.out.println(Thread.currentThread().getName() + ":"  + "火鍋食材準備好了");
		String shicai = "火鍋食材";

		// 開水已經稍好,我們取得燒好的開水
		String boilWater = futureTask.get();

		System.out.println(Thread.currentThread().getName() + ":"  + boilWater + "和" + shicai + "已經準備好,我們可以開始打火鍋啦");
	}
}

執行結果如下截圖,符合我們的預期:

從以上代碼中可以看到,我們使用Future主要有以下步驟:

  1. 新建一個Callable匿名函數實現類對象,我們的業務邏輯在Callablecall方法中實現,其中Callable的泛型是返回結果類型;
  2. 然後把Callable匿名函數對象作為FutureTask的構造參數傳入,構建一個futureTask對象;
  3. 然後再把futureTask對象作為Thread構造參數傳入並開啟這個線程執行去執行業務邏輯;
  4. 最後我們調用futureTask對象的get方法得到業務邏輯執行結果。

可以看到跟Future使用有關的JDK類主要有FutureTaskCallable兩個,下面主要對FutureTask進行源碼分析。

擴展: 還有一種使用Future的方式是將Callable實現類提交給線程池執行的方式,這裏不再介紹,自行百度即可。

3 FutureTask類結構分析

我們先來看下FutureTask的類結構:

可以看到FutureTask實現了RunnableFuture接口,而RunnableFuture接口又繼承了FutureRunnable接口。因為FutureTask間接實現了Runnable接口,因此可以作為任務被線程Thread執行;此外,最重要的一點就是FutureTask還間接實現了Future接口,因此還可以獲得任務執行的結果。下面我們就來簡單看看這幾個接口的相關api

// Runnable.java

@FunctionalInterface
public interface Runnable {
    // 執行線程任務
    public abstract void run();
}

Runnable沒啥好說的,相信大家都已經很熟悉了。

// Future.java

public interface Future<V> {
    /**
     * 嘗試取消線程任務的執行,分為以下幾種情況:
     * 1)如果線程任務已經完成或已經被取消或其他原因不能被取消,此時會失敗並返回false;
     * 2)如果任務還未開始執行,此時執行cancel方法,那麼任務將被取消執行,此時返回true;TODO 此時對應任務狀態state的哪種狀態???不懂!!
     * 3)如果任務已經開始執行,那麼mayInterruptIfRunning這個參數將決定是否取消任務的執行。
     *    這裏值得注意的是,cancel(true)實質並不能真正取消線程任務的執行,而是發出一個線程
     *    中斷的信號,一般需要結合Thread.currentThread().isInterrupted()來使用。
     */
    boolean cancel(boolean mayInterruptIfRunning);
    /**
     * 判斷任務是否被取消,在執行任務完成前被取消,此時會返回true
     */
    boolean isCancelled();
    /**
     * 這個方法不管任務正常停止,異常還是任務被取消,總是返回true。
     */
    boolean isDone();
    /**
     * 獲取任務執行結果,注意是阻塞等待獲取任務執行結果。
     */
    V get() throws InterruptedException, ExecutionException;
    /**
     * 獲取任務執行結果,注意是阻塞等待獲取任務執行結果。
     * 只不過在規定的時間內未獲取到結果,此時會拋出超時異常
     */
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

Future接口象徵著異步執行任務的結果即執行一個耗時任務完全可以另起一個線程執行,然後此時我們可以去做其他事情,做完其他事情我們再調用Future.get()方法獲取結果即可,此時若異步任務還沒結束,此時會一直阻塞等待,直到異步任務執行完獲取到結果。

// RunnableFuture.java

public interface RunnableFuture<V> extends Runnable, Future<V> {
    /**
     * Sets this Future to the result of its computation
     * unless it has been cancelled.
     */
    void run();
}

RunnableFutureFutureRunnable接口的組合,即這個接口表示又可以被線程異步執行,因為實現了Runnable接口,又可以獲得線程異步任務的執行結果,因為實現了Future接口。因此解決了Runnable異步任務沒有返回結果的缺陷。

接下來我們來看下FutureTaskFutureTask實現了RunnableFuture接口,因此是FutureRunnable接口的具體實現類,是一個可被取消的異步線程任務,提供了Future的基本實現,即異步任務執行后我們能夠獲取到異步任務的執行結果,是我們接下來分析的重中之重。FutureTask可以包裝一個CallableRunnable對象,此外,FutureTask除了可以被線程執行外,還可以被提交給線程池執行。

我們先看下FutureTask類的api,其中重點方法已經紅框框出。

上圖中FutureTaskrun方法是被線程異步執行的方法,get方法即是取得異步任務執行結果的方法,還有cancel方法是取消任務執行的方法。接下來我們主要對這三個方法進行重點分析。

思考

  1. FutureTask覆寫的run方法的返回類型依然是void,表示沒有返回值,那麼FutureTaskget方法又是如何獲得返回值的呢?
  2. FutureTaskcancel方法能真正取消線程異步任務的執行么?什麼情況下能取消?

因為FutureTask異步任務執行結果還跟Callable接口有關,因此我們再來看下Callable接口:

// Callable.java

@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     */
    V call() throws Exception;
}

我們都知道,Callable<V>接口和Runnable接口都可以被提交給線程池執行,唯一不同的就是Callable<V>接口是有返回結果的,其中的泛型V就是返回結果,而Runnable接口是沒有返回結果的。

思考: 一般情況下,Runnable接口實現類才能被提交給線程池執行,為何Callable接口實現類也可以被提交給線程池執行?想想線程池的submit方法內部有對Callable做適配么?

4 FutureTask源碼分析

4.1 FutureTask成員變量

我們首先來看下FutureTask的成員變量有哪些,理解這些成員變量對後面的源碼分析非常重要。

// FutureTask.java

/** 封裝的Callable對象,其call方法用來執行異步任務 */
private Callable<V> callable;
/** 在FutureTask裏面定義一個成員變量outcome,用來裝異步任務的執行結果 */
private Object outcome; // non-volatile, protected by state reads/writes
/** 用來執行callable任務的線程 */
private volatile Thread runner;
/** 線程等待節點,reiber stack的一種實現 */
private volatile WaitNode waiters;
/** 任務執行狀態 */
private volatile int state;

// Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE;
// 對應成員變量state的偏移地址
private static final long stateOffset;
// 對應成員變量runner的偏移地址
private static final long runnerOffset;
// 對應成員變量waiters的偏移地址
private static final long waitersOffset;

這裏我們要重點關注下FutureTaskCallable成員變量,因為FutureTask的異步任務最終是委託給Callable去實現的。

思考

  1. FutureTask的成員變量runner,waitersstate都被volatile修飾,我們可以思考下為什麼這三個成員變量需要被volatile修飾,而其他成員變量又不用呢?volatile關鍵字的作用又是什麼呢?
  2. 既然已經定義了成員變量runner,waitersstate了,此時又定義了stateOffset,runnerOffsetwaitersOffset變量分別對應runner,waitersstate的偏移地址,為何要多此一舉呢?

我們再來看看stateOffset,runnerOffsetwaitersOffset變量這三個變量的初始化過程:

// FutureTask.java

static {
    try {
        UNSAFE = sun.misc.Unsafe.getUnsafe();
        Class<?> k = FutureTask.class;
        stateOffset = UNSAFE.objectFieldOffset
            (k.getDeclaredField("state"));
        runnerOffset = UNSAFE.objectFieldOffset
            (k.getDeclaredField("runner"));
        waitersOffset = UNSAFE.objectFieldOffset
            (k.getDeclaredField("waiters"));
    } catch (Exception e) {
        throw new Error(e);
    }
    }

4.2 FutureTask的狀態變化

前面講了FutureTask的成員變量,有一個表示狀態的成員變量state我們要重點關注下,state變量表示任務執行的狀態。

// FutureTask.java

/** 任務執行狀態 */
private volatile int state;
/** 任務新建狀態 */
private static final int NEW          = 0;
/** 任務正在完成狀態,是一個瞬間過渡狀態 */
private static final int COMPLETING   = 1;
/** 任務正常結束狀態 */
private static final int NORMAL       = 2;
/** 任務執行異常狀態 */
private static final int EXCEPTIONAL  = 3;
/** 任務被取消狀態,對應cancel(false) */
private static final int CANCELLED    = 4;
/** 任務中斷狀態,是一個瞬間過渡狀態 */
private static final int INTERRUPTING = 5;
/** 任務被中斷狀態,對應cancel(true) */
private static final int INTERRUPTED  = 6;

可以看到任務狀態變量state有以上7種狀態,0-6分別對應着每一種狀態。任務狀態一開始是NEW,然後由FutureTask的三個方法set,setExceptioncancel來設置狀態的變化,其中狀態變化有以下四種情況:

  1. NEW -> COMPLETING -> NORMAL:這個狀態變化表示異步任務的正常結束,其中COMPLETING是一個瞬間臨時的過渡狀態,由set方法設置狀態的變化;
  2. NEW -> COMPLETING -> EXCEPTIONAL:這個狀態變化表示異步任務執行過程中拋出異常,由setException方法設置狀態的變化;
  3. NEW -> CANCELLED:這個狀態變化表示被取消,即調用了cancel(false),由cancel方法來設置狀態變化;
  4. NEW -> INTERRUPTING -> INTERRUPTED:這個狀態變化表示被中斷,即調用了cancel(true),由cancel方法來設置狀態變化。

4.3 FutureTask構造函數

FutureTask有兩個構造函數,我們分別來看看:

// FutureTask.java

// 第一個構造函數
public FutureTask(Callable<V> callable) {
    if (callable == null)
        throw new NullPointerException();
    this.callable = callable;
    this.state = NEW;       // ensure visibility of callable
}

可以看到,這個構造函數在我們前面舉的“打火鍋”的例子代碼中有用到,就是Callable成員變量賦值,在異步執行任務時再調用Callable.call方法執行異步任務邏輯。此外,此時給任務狀態state賦值為NEW,表示任務新建狀態。

我們再來看下FutureTask的另外一個構造函數:

// FutureTask.java

// 另一個構造函數
public FutureTask(Runnable runnable, V result) {
    this.callable = Executors.callable(runnable, result);
    this.state = NEW;       // ensure visibility of callable
}

這個構造函數在執行Executors.callable(runnable, result)時是通過適配器RunnableAdapter來將Runnable對象runnable轉換成Callable對象,然後再分別給callablestate變量賦值。

注意,這裏我們需要記住的是FutureTask新建時,此時的任務狀態stateNEW就好了。

4.4 FutureTask.run方法,用來執行異步任務

前面我們有講到FutureTask間接實現了Runnable接口,覆寫了Runnable接口的run方法,因此該覆寫的run方法是提交給線程來執行的,同時,該run方法正是執行異步任務邏輯的方法,那麼,執行完run方法又是如何保存異步任務執行的結果的呢?

我們現在着重來分析下run方法:

// FutureTask.java

public void run() {
    // 【1】,為了防止多線程併發執行異步任務,這裏需要判斷線程滿不滿足執行異步任務的條件,有以下三種情況:
    // 1)若任務狀態state為NEW且runner為null,說明還未有線程執行過異步任務,此時滿足執行異步任務的條件,
    // 此時同時調用CAS方法為成員變量runner設置當前線程的值;
    // 2)若任務狀態state為NEW且runner不為null,任務狀態雖為NEW但runner不為null,說明有線程正在執行異步任務,
    // 此時不滿足執行異步任務的條件,直接返回;
    // 1)若任務狀態state不為NEW,此時不管runner是否為null,說明已經有線程執行過異步任務,此時沒必要再重新
    // 執行一次異步任務,此時不滿足執行異步任務的條件;
    if (state != NEW ||
        !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                     null, Thread.currentThread()))
        return;
    try {
        // 拿到之前構造函數傳進來的callable實現類對象,其call方法封裝了異步任務執行的邏輯
        Callable<V> c = callable;
        // 若任務還是新建狀態的話,那麼就調用異步任務
        if (c != null && state == NEW) {
            // 異步任務執行結果
            V result;
            // 異步任務執行成功還是始遍標誌
            boolean ran;
            try {
                // 【2】,執行異步任務邏輯,並把執行結果賦值給result
                result = c.call();
                // 若異步任務執行過程中沒有拋出異常,說明異步任務執行成功,此時設置ran標誌為true
                ran = true;
            } catch (Throwable ex) {
                result = null;
                // 異步任務執行過程拋出異常,此時設置ran標誌為false
                ran = false;
                // 【3】設置異常,裏面也設置state狀態的變化
                setException(ex);
            }
            // 【3】若異步任務執行成功,此時設置異步任務執行結果,同時也設置狀態的變化
            if (ran)
                set(result);
        }
    } finally {
        // runner must be non-null until state is settled to
        // prevent concurrent calls to run()
        // 異步任務正在執行過程中,runner一直是非空的,防止併發調用run方法,前面有調用cas方法做判斷的
        // 在異步任務執行完后,不管是正常結束還是異常結束,此時設置runner為null
        runner = null;
        // state must be re-read after nulling runner to prevent
        // leaked interrupts
        // 線程執行異步任務后的任務狀態
        int s = state;
        // 【4】如果執行了cancel(true)方法,此時滿足條件,
        // 此時調用handlePossibleCancellationInterrupt方法處理中斷
        if (s >= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
}

可以看到執行異步任務的run方法主要分為以下四步來執行:

  1. 判斷線程是否滿足執行異步任務的條件:為了防止多線程併發執行異步任務,這裏需要判斷線程滿不滿足執行異步任務的條件;
  2. 若滿足條件,執行異步任務:因為異步任務邏輯封裝在Callable.call方法中,此時直接調用Callable.call方法執行異步任務,然後返回執行結果;
  3. 根據異步任務的執行情況做不同的處理:1) 若異步任務執行正常結束,此時調用set(result);來設置任務執行結果;2)若異步任務執行拋出異常,此時調用setException(ex);來設置異常,詳細分析請見4.4.1小節
  4. 異步任務執行完后的善後處理工作:不管異步任務執行成功還是失敗,若其他線程有調用FutureTask.cancel(true),此時需要調用handlePossibleCancellationInterrupt方法處理中斷,詳細分析請見4.4.2小節

這裏值得注意的是判斷線程滿不滿足執行異步任務條件時,runner是否為null是調用UNSAFECAS方法compareAndSwapObject來判斷和設置的,同時compareAndSwapObject是通過成員變量runner的偏移地址runnerOffset來給runner賦值的,此外,成員變量runner被修飾為volatile是在多線程的情況下, 一個線程的volatile修飾變量的設值能夠立即刷進主存,因此值便可被其他線程可見。

4.4.1 FutureTask的set和setException方法

下面我們來看下當異步任務執行正常結束時,此時會調用set(result);方法:

// FutureTask.java

protected void set(V v) {
    // 【1】調用UNSAFE的CAS方法判斷任務當前狀態是否為NEW,若為NEW,則設置任務狀態為COMPLETING
    // 【思考】此時任務不能被多線程併發執行,什麼情況下會導致任務狀態不為NEW?
    // 答案是只有在調用了cancel方法的時候,此時任務狀態不為NEW,此時什麼都不需要做,
    // 因此需要調用CAS方法來做判斷任務狀態是否為NEW
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        // 【2】將任務執行結果賦值給成員變量outcome
        outcome = v;
        // 【3】將任務狀態設置為NORMAL,表示任務正常結束
        UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // final state
        // 【4】調用任務執行完成方法,此時會喚醒阻塞的線程,調用done()方法和清空等待線程鏈表等
        finishCompletion();
    }
}

可以看到當異步任務正常執行結束后,且異步任務沒有被cancel的情況下,此時會做以下事情:將任務執行結果保存到FutureTask的成員變量outcome中的,賦值結束後會調用finishCompletion方法來喚醒阻塞的線程(哪裡來的阻塞線程?後面會分析),值得注意的是這裏對應的任務狀態變化是NEW -> COMPLETING -> NORMAL

我們繼續來看下當異步任務執行過程中拋出異常,此時會調用setException(ex);方法。

// FutureTask.java

protected void setException(Throwable t) {
    // 【1】調用UNSAFE的CAS方法判斷任務當前狀態是否為NEW,若為NEW,則設置任務狀態為COMPLETING
    // 【思考】此時任務不能被多線程併發執行,什麼情況下會導致任務狀態不為NEW?
    // 答案是只有在調用了cancel方法的時候,此時任務狀態不為NEW,此時什麼都不需要做,
    // 因此需要調用CAS方法來做判斷任務狀態是否為NEW
    if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
        // 【2】將異常賦值給成員變量outcome
        outcome = t;
        // 【3】將任務狀態設置為EXCEPTIONAL
        UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
        // 【4】調用任務執行完成方法,此時會喚醒阻塞的線程,調用done()方法和清空等待線程鏈表等
        finishCompletion();
    }
}

可以看到setException(Throwable t)的代碼邏輯跟前面的set(V v)幾乎一樣,不同的是任務執行過程中拋出異常,此時是將異常保存到FutureTask的成員變量outcome中,還有,值得注意的是這裏對應的任務狀態變化是NEW -> COMPLETING -> EXCEPTIONAL

因為異步任務不管正常還是異常結束,此時都會調用FutureTaskfinishCompletion方法來喚醒喚醒阻塞的線程,這裏阻塞的線程是指我們調用Future.get方法時若異步任務還未執行完,此時該線程會阻塞。

// FutureTask.java

private void finishCompletion() {
    // assert state > COMPLETING;
    // 取出等待線程鏈表頭節點,判斷頭節點是否為null
    // 1)若線程鏈表頭節點不為空,此時以“後進先出”的順序(棧)移除等待的線程WaitNode節點
    // 2)若線程鏈表頭節點為空,說明還沒有線程調用Future.get()方法來獲取任務執行結果,固然不用移除
    for (WaitNode q; (q = waiters) != null;) {
        // 調用UNSAFE的CAS方法將成員變量waiters設置為空
        if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) {
            for (;;) {
                // 取出WaitNode節點的線程
                Thread t = q.thread;
                // 若取出的線程不為null,則將該WaitNode節點線程置空,且喚醒正在阻塞的該線程
                if (t != null) {
                    q.thread = null;
                    //【重要】喚醒正在阻塞的該線程
                    LockSupport.unpark(t);
                }
                // 繼續取得下一個WaitNode線程節點
                WaitNode next = q.next;
                // 若沒有下一個WaitNode線程節點,說明已經將所有等待的線程喚醒,此時跳出for循環
                if (next == null)
                    break;
                // 將已經移除的線程WaitNode節點的next指針置空,此時好被垃圾回收
                q.next = null; // unlink to help gc
                // 再把下一個WaitNode線程節點置為當前線程WaitNode頭節點
                q = next;
            }
            break;
        }
    }
    // 不管任務正常執行還是拋出異常,都會調用done方法
    done();
    // 因為異步任務已經執行完且結果已經保存到outcome中,因此此時可以將callable對象置空了
    callable = null;        // to reduce footprint
}

finishCompletion方法的作用就是不管異步任務正常還是異常結束,此時都要喚醒且移除線程等待鏈表的等待線程節點,這個鏈表實現的是一個是Treiber stack,因此喚醒(移除)的順序是”後進先出”即後面先來的線程先被先喚醒(移除),關於這個線程等待鏈表是如何成鏈的,後面再繼續分析。

4.4.2 FutureTask的handlePossibleCancellationInterrupt方法

4.4小節分析的run方法里的最後有一個finally塊,此時若任務狀態state >= INTERRUPTING,此時說明有其他線程執行了cancel(true)方法,此時需要讓出CPU執行的時間片段給其他線程執行,我們來看下具體的源碼:

// FutureTask.java

private void handlePossibleCancellationInterrupt(int s) {
    // It is possible for our interrupter to stall before getting a
    // chance to interrupt us.  Let's spin-wait patiently.
    // 當任務狀態是INTERRUPTING時,此時讓出CPU執行的機會,讓其他線程執行
    if (s == INTERRUPTING)
        while (state == INTERRUPTING)
            Thread.yield(); // wait out pending interrupt

    // assert state == INTERRUPTED;

    // We want to clear any interrupt we may have received from
    // cancel(true).  However, it is permissible to use interrupts
    // as an independent mechanism for a task to communicate with
    // its caller, and there is no way to clear only the
    // cancellation interrupt.
    //
    // Thread.interrupted();
}

思考: 為啥任務狀態是INTERRUPTING時,此時就要讓出CPU執行的時間片段呢?還有為什麼要在義務任務執行后才調用handlePossibleCancellationInterrupt方法呢?

4.5 FutureTask.get方法,獲取任務執行結果

前面我們起一個線程在其`run`方法中執行異步任務后,此時我們可以調用`FutureTask.get`方法來獲取異步任務執行的結果。

// FutureTask.java

public V get() throws InterruptedException, ExecutionException {
    int s = state;
    // 【1】若任務狀態<=COMPLETING,說明任務正在執行過程中,此時可能正常結束,也可能遇到異常
    if (s <= COMPLETING)
        s = awaitDone(false, 0L);
    // 【2】最後根據任務狀態來返回任務執行結果,此時有三種情況:1)任務正常執行;2)任務執行異常;3)任務被取消
    return report(s);
}

可以看到,如果任務狀態state<=COMPLETING,說明異步任務正在執行過程中,此時會調用awaitDone方法阻塞等待;當任務執行完后,此時再調用report方法來報告任務結果,此時有三種情況:1)任務正常執行;2)任務執行異常;3)任務被取消。

4.5.1 FutureTask.awaitDone方法

FutureTask.awaitDone方法會阻塞獲取異步任務執行結果的當前線程,直到異步任務執行完成。

// FutureTask.java

private int awaitDone(boolean timed, long nanos)
    throws InterruptedException {
    // 計算超時結束時間
    final long deadline = timed ? System.nanoTime() + nanos : 0L;
    // 線程鏈表頭節點
    WaitNode q = null;
    // 是否入隊
    boolean queued = false;
    // 死循環
    for (;;) {
        // 如果當前獲取任務執行結果的線程被中斷,此時移除該線程WaitNode鏈表節點,並拋出InterruptedException
        if (Thread.interrupted()) {
            removeWaiter(q);
            throw new InterruptedException();
        }

        int s = state;
        // 【5】如果任務狀態>COMPLETING,此時返回任務執行結果,其中此時任務可能正常結束(NORMAL),可能拋出異常(EXCEPTIONAL)
        // 或任務被取消(CANCELLED,INTERRUPTING或INTERRUPTED狀態的一種)
        if (s > COMPLETING) {
            // 【問】此時將當前WaitNode節點的線程置空,其中在任務結束時也會調用finishCompletion將WaitNode節點的thread置空,
            // 這裏為什麼又要再調用一次q.thread = null;呢?
            // 【答】因為若很多線程來獲取任務執行結果,在任務執行完的那一刻,此時獲取任務的線程要麼已經在線程等待鏈表中,要麼
            // 此時還是一個孤立的WaitNode節點。在線程等待鏈表中的的所有WaitNode節點將由finishCompletion來移除(同時喚醒)所有
            // 等待的WaitNode節點,以便垃圾回收;而孤立的線程WaitNode節點此時還未阻塞,因此不需要被喚醒,此時只要把其屬性置為
            // null,然後其有沒有被誰引用,因此可以被GC。
            if (q != null)
                q.thread = null;
            // 【重要】返回任務執行結果
            return s;
        }
        // 【4】若任務狀態為COMPLETING,此時說明任務正在執行過程中,此時獲取任務結果的線程需讓出CPU執行時間片段
        else if (s == COMPLETING) // cannot time out yet
            Thread.yield();
        // 【1】若當前線程還沒有進入線程等待鏈表的WaitNode節點,此時新建一個WaitNode節點,並把當前線程賦值給WaitNode節點的thread屬性
        else if (q == null)
            q = new WaitNode();
        // 【2】若當前線程等待節點還未入線程等待隊列,此時加入到該線程等待隊列的頭部
        else if (!queued)
            queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
                                                 q.next = waiters, q);
        // 若有超時設置,那麼處理超時獲取任務結果的邏輯
        else if (timed) {
            nanos = deadline - System.nanoTime();
            if (nanos <= 0L) {
                removeWaiter(q);
                return state;
            }
            LockSupport.parkNanos(this, nanos);
        }
        // 【3】若沒有超時設置,此時直接阻塞當前線程
        else
            LockSupport.park(this);
    }
}

FutureTask.awaitDone方法主要做的事情總結如下:

  1. 首先awaitDone方法裏面是一個死循環;
  2. 若獲取結果的當前線程被其他線程中斷,此時移除該線程WaitNode鏈表節點,並拋出InterruptedException;
  3. 如果任務狀態state>COMPLETING,此時返回任務執行結果;
  4. 若任務狀態為COMPLETING,此時獲取任務結果的線程需讓出CPU執行時間片段;
  5. q == null,說明當前線程還未設置到WaitNode節點,此時新建WaitNode節點並設置其thread屬性為當前線程;
  6. queued==false,說明當前線程WaitNode節點還未加入線程等待鏈表,此時加入該鏈表的頭部;
  7. timed設置為true時,此時該方法具有超時功能,關於超時的邏輯這裏不詳細分析;
  8. 當前面6個條件都不滿足時,此時阻塞當前線程。

我們分析到這裏,可以直到執行異步任務只能有一個線程來執行,而獲取異步任務結果可以多線程來獲取,當異步任務還未執行完時,此時獲取異步任務結果的線程會加入線程等待鏈表中,然後調用調用LockSupport.park(this);方法阻塞當前線程。直到異步任務執行完成,此時會調用finishCompletion方法來喚醒並移除線程等待鏈表的每個WaitNode節點,這裏這裏喚醒(移除)WaitNode節點的線程是從鏈表頭部開始的,前面我們也已經分析過。

還有一個特別需要注意的就是awaitDone方法裏面是一個死循環,當一個獲取異步任務的線程進來后可能會多次進入多個條件分支執行不同的業務邏輯,也可能只進入一個條件分支。下面分別舉兩種可能的情況進行說明:

情況1
當獲取異步任務結果的線程進來時,此時異步任務還未執行完即state=NEW且沒有超時設置時:

  1. 第一次循環:此時q = null,此時進入上面代碼標號【1】的判斷分支,即為當前線程新建一個WaitNode節點;
  2. 第二次循環:此時queued = false,此時進入上面代碼標號【2】的判斷分支,即將之前新建的WaitNode節點加入線程等待鏈表中;
  3. 第三次循環:此時進入上面代碼標號【3】的判斷分支,即阻塞當前線程;
  4. 第四次循環:加入此時異步任務已經執行完,此時進入上面代碼標號【5】的判斷分支,即返回異步任務執行結果。

情況2
當獲取異步任務結果的線程進來時,此時異步任務已經執行完即state>COMPLETING且沒有超時設置時,此時直接進入上面代碼標號【5】的判斷分支,即直接返回異步任務執行結果即可,也不用加入線程等待鏈表了。

4.5.2 FutureTask.report方法

get方法中,當異步任務執行結束后即不管異步任務正常還是異常結束,亦或是被cancel,此時獲取異步任務結果的線程都會被喚醒,因此會繼續執行FutureTask.report方法報告異步任務的執行情況,此時可能會返回結果,也可能會拋出異常。

// FutureTask.java

private V report(int s) throws ExecutionException {
    // 將異步任務執行結果賦值給x,此時FutureTask的成員變量outcome要麼保存着
    // 異步任務正常執行的結果,要麼保存着異步任務執行過程中拋出的異常
    Object x = outcome;
    // 【1】若異步任務正常執行結束,此時返回異步任務執行結果即可
    if (s == NORMAL)
        return (V)x;
    // 【2】若異步任務執行過程中,其他線程執行過cancel方法,此時拋出CancellationException異常
    if (s >= CANCELLED)
        throw new CancellationException();
    // 【3】若異步任務執行過程中,拋出異常,此時將該異常轉換成ExecutionException后,重新拋出。
    throw new ExecutionException((Throwable)x);
}

4.6 FutureTask.cancel方法,取消執行任務

我們最後再來看下FutureTask.cancel方法,我們一看到FutureTask.cancel方法,肯定一開始就天真的認為這是一個可以取消異步任務執行的方法,如果我們這樣認為的話,只能說我們猜對了一半。

// FutureTask.java

public boolean cancel(boolean mayInterruptIfRunning) {
    // 【1】判斷當前任務狀態,若state == NEW時根據mayInterruptIfRunning參數值給當前任務狀態賦值為INTERRUPTING或CANCELLED
    // a)當任務狀態不為NEW時,說明異步任務已經完成,或拋出異常,或已經被取消,此時直接返回false。
    // TODO 【問題】此時若state = COMPLETING呢?此時為何也直接返回false,而不能發出中斷異步任務線程的中斷信號呢??
    // TODO 僅僅因為COMPLETING是一個瞬時態嗎???
    // b)當前僅當任務狀態為NEW時,此時若mayInterruptIfRunning為true,此時任務狀態賦值為INTERRUPTING;否則賦值為CANCELLED。
    if (!(state == NEW &&
          UNSAFE.compareAndSwapInt(this, stateOffset, NEW,
              mayInterruptIfRunning ? INTERRUPTING : CANCELLED)))
        return false;
    try {    // in case call to interrupt throws exception
        // 【2】如果mayInterruptIfRunning為true,此時中斷執行異步任務的線程runner(還記得執行異步任務時就把執行異步任務的線程就賦值給了runner成員變量嗎)
        if (mayInterruptIfRunning) {
            try {
                Thread t = runner;
                if (t != null)
                    // 中斷執行異步任務的線程runner
                    t.interrupt();
            } finally { // final state
                // 最後任務狀態賦值為INTERRUPTED
                UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED);
            }
        }
    // 【3】不管mayInterruptIfRunning為true還是false,此時都要調用finishCompletion方法喚醒阻塞的獲取異步任務結果的線程並移除線程等待鏈表節點
    } finally {
        finishCompletion();
    }
    // 返回true
    return true;
}

以上代碼中,當異步任務狀態state != NEW時,說明異步任務已經正常執行完或已經異常結束亦或已經被cancel,此時直接返回false;當異步任務狀態state = NEW時,此時又根據mayInterruptIfRunning參數是否為true分為以下兩種情況:

  1. mayInterruptIfRunning = false時,此時任務狀態state直接被賦值為CANCELLED,此時不會對執行異步任務的線程發出中斷信號,值得注意的是這裏對應的任務狀態變化是NEW -> CANCELLED
  2. mayInterruptIfRunning = true時,此時會對執行異步任務的線程發出中斷信號,值得注意的是這裏對應的任務狀態變化是NEW -> INTERRUPTING -> INTERRUPTED

最後不管mayInterruptIfRunningtrue還是false,此時都要調用finishCompletion方法喚醒阻塞的獲取異步任務結果的線程並移除線程等待鏈表節點。

FutureTask.cancel源碼中我們可以得出答案,該方法並不能真正中斷正在執行異步任務的線程,只能對執行異步任務的線程發出中斷信號。如果執行異步任務的線程處於sleepwaitjoin的狀態中,此時會拋出InterruptedException異常,該線程可以被中斷;此外,如果異步任務需要在while循環執行的話,此時可以結合以下代碼來結束異步任務線程,即執行異步任務的線程被中斷時,此時Thread.currentThread().isInterrupted()返回true,不滿足while循環條件因此退出循環,結束異步任務執行線程,如下代碼:

public Integer call() throws Exception {
    while (!Thread.currentThread().isInterrupted()) {
        // 業務邏輯代碼
        System.out.println("running...");

    }
    return 666;
}

注意:調用了FutureTask.cancel方法,只要返回結果是true,假如異步任務線程雖然不能被中斷,即使異步任務線程正常執行完畢,返回了執行結果,此時調用FutureTask.get方法也不能夠獲取異步任務執行結果,此時會拋出CancellationException異常。請問知道這是為什麼嗎?

因為調用了FutureTask.cancel方法,只要返回結果是true,此時的任務狀態為CANCELLEDINTERRUPTED,同時必然會執行finishCompletion方法,而finishCompletion方法會喚醒獲取異步任務結果的線程等待列表的線程,而獲取異步任務結果的線程喚醒后發現狀態s >= CANCELLED,此時就會拋出CancellationException異常了。

5 總結

好了,本篇文章對FutureTask的源碼分析就到此結束了,下面我們再總結下FutureTask的實現邏輯:

  1. 我們實現Callable接口,在覆寫的call方法中定義需要執行的業務邏輯;
  2. 然後把我們實現的Callable接口實現對象傳給FutureTask,然後FutureTask作為異步任務提交給線程執行;
  3. 最重要的是FutureTask內部維護了一個狀態state,任何操作(異步任務正常結束與否還是被取消)都是圍繞着這個狀態進行,並隨時更新state任務的狀態;
  4. 只能有一個線程執行異步任務,當異步任務執行結束后,此時可能正常結束,異常結束或被取消。
  5. 可以多個線程併發獲取異步任務執行結果,當異步任務還未執行完,此時獲取異步任務的線程將加入線程等待列表進行等待;
  6. 當異步任務線程執行結束后,此時會喚醒獲取異步任務執行結果的線程,注意喚醒順序是”後進先出”即後面加入的阻塞線程先被喚醒。
  7. 當我們調用FutureTask.cancel方法時並不能真正停止執行異步任務的線程,只是發出中斷線程的信號。但是只要cancel方法返回true,此時即使異步任務能正常執行完,此時我們調用get方法獲取結果時依然會拋出CancellationException異常。

擴展: 前面我們提到了FutureTaskrunner,waitersstate都是用volatile關鍵字修飾,說明這三個變量都是多線程共享的對象(成員變量),會被多線程操作,此時用volatile關鍵字修飾是為了一個線程操作volatile屬性變量值后,能夠及時對其他線程可見。此時多線程操作成員變量僅僅用了volatile關鍵字仍然會有線程安全問題的,而此時Doug Lea老爺子沒有引入任何線程鎖,而是採用了UnsafeCAS方法來代替鎖操作,確保線程安全性。

6 分析FutureTask源碼,我們能學到什麼?

我們分析源碼的目的是什麼?除了弄懂FutureTask的內部實現原理外,我們還要借鑒大佬寫寫框架源碼的各種技巧,只有這樣,我們才能成長。

分析了FutureTask源碼,我們可以從中學到:

  1. 利用LockSupport來實現線程的阻塞\喚醒機制;
  2. 利用volatileUNSAFECAS方法來實現線程共享變量的無鎖化操作;
  3. 若要編寫超時異常的邏輯可以參考FutureTaskget(long timeout, TimeUnit unit)的實現邏輯;
  4. 多線程獲取某一成員變量結果時若需要等待時的線程等待鏈表的邏輯實現;
  5. 某一異步任務在某一時刻只能由單一線程執行的邏輯實現;
  6. FutureTask中的任務狀態satate的變化處理的邏輯實現。

以上列舉的幾點都是我們可以學習參考的地方。

若您覺得不錯,請無情的轉發和點贊吧!

【源碼筆記】Github地址:

https://github.com/yuanmabiji/Java-SourceCode-Blogs

公眾號【源碼筆記】,專註於Java後端系列框架的源碼分析。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

強迫餵食太殘忍 紐約市2022年起禁售鵝肝

摘錄自2019年10月31日中央社報導

紐約市議會31日以以42票贊成、6票反對的壓倒性票數,通過將從2022年起禁售鵝肝。 議員認為,為滿足人類口腹之慾而逼迫鴨鵝將肝臟養肥,是很殘忍的事。

這項法律將自2022年10月起生效,禁止任何組織銷售、提供甚至是處理鵝肝。 違者每次違規將被罰500到2000美元。

 不過,鵝肝農民大喊不公,揚言將採取法律行動。農民聲稱他們的生產作業並不殘忍,是運動人士誇大了動物承受的苦難。 他們說,他們餵食鴨鵝的玉米量,並未超過牠們自己食用的量。

全球目前已有數國禁止生產鵝肝,包括英國等。鵝肝產業正在探索替代方法,尋找不需利用強迫餵食手段生產鵝肝的辦法。

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

抗議美政府對抗氣候變遷不力 珍芳達再度被捕

摘錄自2019年11月2日中央通訊社美國報導

奧斯卡影后、同時也是倡議人士的珍芳達(Jane Fonda)2日在華府國會參議院前抗議政府處理氣候變遷不力,再度遭警方逮捕。

法新社報導,珍芳達跟記者開玩笑說:「這次我可能會被關上一晚,一晚沒關係,沒什麼大不了。」珍芳達在警方為她戴上塑膠手銬時說,這不是她第一次被捕。

珍芳達和女星羅姍娜艾奎特(Rosanna Arquette)、凱薩琳琪娜(Catherine Keener)等數十名倡議人士,坐在不得示威的聯邦參議院大樓前高呼口號而被捕。

年近82歲的珍芳達自1970年代以來即是和平主義者,目前仍活躍影壇。她說,自己受到瑞典環保少女桑柏格(Greta Thunberg)的感召而加入反氣候變遷運動。

珍芳達表示:「抗爭的方式有很多種,但我深受桑柏格和全世界示威年輕學子的啟發。」

她說:「我是名人,所以利用我的名氣傳達出我們所面臨危機的訊息,這個危機將決定我們孩子和孫子的未來世界是否還適合人類居住。」

曾於1972年親赴越南抗議越戰的珍芳達表示,現在迫切需要採取行動。赴越抗議為她贏得「河內珍」(Hanoi Jane)的反戰明星稱號。

珍芳達說:「我們只剩11年的時間可以扭轉情勢,我們要很勇敢、很團結和堅定。」

穿著一身搶眼紅色大衣的珍芳達說的是,科學研究顯示,人類若要避免引發災難性全球暖化,必須在2030年前大幅減少碳排放量。

珍芳達表示,她已做好至少在1月中旬以前會被一再逮捕的心理準備,因為在那之後,她得回好萊塢拍攝下一季的得獎影集「同妻俱樂部」(Grace and Frankie)。榕

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

推廣電動車 基隆市 2 座停車場供充電樁免費充電

停車也可免費充電了,基隆市博愛和成功 2 停車場各提供 2 部電動車充電器,電動車只要停車就可以免費充電。   為推廣綠能載具,基隆市政府向經濟部工業局爭取,在基隆博愛與成功停車場,各裝置 2 部電動車充電器,每部充電器可同時為 2 輛汽車充電。   博愛停車場管理站金微歡指出,民眾停車的同時,就可以免費充電,1 輛車充到飽約需 2 個半小時至 3 小時,使用悠遊卡啟動,就可免費充電,不過,基隆電動車還不普遍,目前 1 個月只有個位數的車輛充電。

本站聲明:網站內容來源於EnergyTrend https://www.energytrend.com.tw/ev/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案

程序員敲代碼時耳機里聽的到底是什麼?

我是風箏,公眾號「古時的風箏」,一個兼具深度與廣度的程序員鼓勵師,一個本打算寫詩卻寫起了代碼的田園碼農!
文章會收錄在 JavaNewBee 中,更有 Java 後端知識圖譜,從小白到大牛要走的路都在裏面。

程序員上班戴耳機聽歌難道不是正常的嗎,真的還有公司不允許程序員戴耳機的嗎?不戴耳機能寫代碼?

那程序員的耳機里聽的是什麼呢?我採訪了一眾程序員朋友。

鋼鐵程序員王二麻子同學

聽的什麼?我根本就不知道,我只是不想讓別人打擾我

有時候開發確實是比較費腦子的,尤其是遇到複雜邏輯的時候。正當思如泉涌、靈感迸發的時候,旁人看着我坐在那裡一動不動,好像什麼都沒有做,其實我腦子里正在構思一個複雜的流程。

這時候,突然有個人走過來打斷我,前面的思考都白費了,你說傷心不,你說氣人不。

所以,為了防止上面的情況出現,不得不戴上耳機。至於聽什麼,不重要,我只是告訴別人,別過來,我現在沒時間。

生活需要儀式感的劉精神同學

聽什麼不重要,關鍵是儀式感

生活需要儀式感,寫代碼也需要儀式感啊。當我戴上耳機的那一刻,我感覺精神抖擻,彷彿遊離的三魂六魄都回來了,寫代碼更有動力。

要是不戴耳機,感覺渾身無力,只想摸魚,寫代碼什麼的,根本想都想不起來。

心疼自己的高愛己同學

其實我就是不想聽我的机械鍵盤聲音,實在太吵了

你也知道,筆記本自帶的鍵盤總感覺軟綿綿的,敲起來實在不給力,嚴重影響我的工作效率。那必須得買個鍵盤啊,在多個朋友的推薦下,我就買了一個青軸鍵盤。你別說,觸感真不錯,每按一下,都感覺指尖有一股電流滑過,同時伴隨着啪啪啪的聲音,感覺寫代碼效率明顯變高了,更神奇的是 bug 都比以前少了,你說神奇不。

但是呢,有個缺點,就是時間久了吧,感覺稍微有那麼一點點吵,所以我就配了個耳機,從此之後,既得到了直達靈魂的觸感,又不會感覺吵了。更神奇的一點是,以前罵我的同事好像都不罵我了。

真的在聽東西的同學

雖然存在以上幾位同學說的情況,但大多數情況下, 我們是真的在聽東西。比如我,為了聽高品質的,還專門買了網易雲音樂的 VIP 。

為什麼是東西,而不是音樂呢,因為有的人不是在聽音樂。

我聽說有人在寫代碼的時候聽評書,更有厲害的,竟然聽相聲。那我想只有三種可能。

  1. 根本沒在寫代碼,可能是在摸魚。
  2. 評書一點都不精彩,相聲一點都不好笑,僅僅是一門語言的藝術而已。
  3. 這是個大佬,已經進入忘我的境界,聽到的不是語言,而是聲音解析成比特位時產生的白噪音,一般人聽不到,只有某些段位的大佬可以。

當然畢竟大佬不常有,而普通群眾常用。大部分人聽的確實是音樂。比如我吧,我戴耳機真的是在聽音樂,為了降噪、減少干擾,提高專註力,提升效率。

什麼類型的音樂更受程序員歡迎呢

英文歌

英文歌是大多數程序員的最愛。請看網易雲音樂給我的每日歌曲推薦,除了伍佰的一首中文歌亂入,剩下的都是英文歌。

你真的認為我英語很好嗎,正好相反,之所以英文歌那麼受歡迎,就是因為聽不懂歌詞是什麼意思,這樣才不會被歌詞帶跑偏,沒錯,我們聽的就是這個節奏。

那要是中文歌就不一樣了,比如說當我聽到「你的酒館對我打了烊,子彈在我心頭上了膛」的時候,我就以為真的要打烊了,子彈真的要上膛了,從而引發一系列思考,酒館為什麼對我打烊,子彈為什麼要上膛,生意不做了嗎,刑法基本法則不懂嗎。

為了證明這一點,我到網易雲音樂上搜了「程序員」相關的歌單,點進去一看,大部分也都是英文歌,看來大家英文都不太好。

然後我又搜了「產品狗」相關的歌單,同樣也是英文歌為主,可見我們雖然不太對付,請參考歌單『產品狗如何說服程序猿』和『程序員如何回應產品狗』,但是方法論還是差不多的。

純音樂

純音樂也是很受歡迎的,我有個同事最喜歡聽貝多芬的命運交響曲,我就沒那麼文藝了。我一般都是聽那種激昂的小提琴或者聽完感覺自己馬上要登基了的那種,不容易犯困。

白噪音

小提琴太勁爆,不能常聽,犯困的下午聽聽可以提神,bug 太多又不想改的時候可以聽聽。大多數時候,不需要那麼亢奮,保持內心的平靜就是寫代碼最好的狀態。那就聽聽白噪音好了,比如雨聲、風聲、溪水聲、鳥唱蟬鳴。

我最喜歡的就是雨聲,嘩啦啦的大雨,再配上驚雷,簡直不要太平靜,innerpeace。

比如下面這個,一聽就是一個小時。

保留曲目

每個人都有自己家私藏的歌單,百聽不厭的那種,當然每個人的品位不一樣、口味兒不一樣,比如當年有個哥們兒異常興奮的把他的珍藏歌單分享給我,我當即表示很感動,一定要認真聽一聽。可當我硬着頭皮聽完第二首的時候,我內心是拒絕的,只能委婉的跟哥們兒說:這 TM 什麼玩意兒。當然是不會破壞友誼的小船的,如果是不熟的朋友,那肯定會豎起大拇指,並且連連點頭稱讚:真不錯,品位棒棒噠。

我沒有薦歌啊,向別人推薦歌曲犹如喂別人食物,你覺得好吃別人並一定覺得好吃。下面是我的 2018 年年度聽歌報告,Sophie Zelmani(蘇菲.珊曼妮)的熱門50單曲就是我的保留曲目,白聽不厭,而且更重要的不仔細聽,還是聽不出歌詞的意思,正好適合寫代碼用。

另外,作為一個程序員鼓勵師,為了鼓勵我自己,我也創建了一個「程序員鼓勵師」的歌單,經常拿出來聽聽接收鼓勵。

作為程序員的你,耳機里有什麼特殊的內容嗎?

壯士且慢,先給點個贊吧,總是被白嫖,身體吃不消!

我是風箏,公眾號「古時的風箏」。一個兼具深度與廣度的程序員鼓勵師,一個本打算寫詩卻寫起了代碼的田園碼農!你可選擇現在就關注我,或者看看歷史文章再關注也不遲。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

※教你寫出一流的銷售文案?

※超省錢租車方案