JAVA基礎複習(二):併發

JAVA基礎複習(二):併發

  • 本文配合《Java併發編程的藝術》食用風味更佳。

背景知識

進程與線程

  • 進程
    • 資源所有權。進程是資源分配的最小單位,由操作系統來安排。
    • 調度/執行。操作系統爲進程分配時間片,通過調度算法來安排處理器調度進程。
  • 線程
    • 進程的主要任務是1.資源分配2.系統調度和執行的單位。將資源分配依然交給進程處理,將系統調度和執行的最小單位細化成線程。
    • 進程具有至少一個線程,線程間可以共享資源,線程間通信的問題也自然解決了。
    • 線程比進程的創建、切換、銷燬的成本小。
  • 多線程(並行編程)的好處
    • 充分利用多個CPU。
    • 防止任務的阻塞,比如socket在accept方法中阻塞等待連接的時候,就可以幹別的事情。阻塞的線程進行阻塞隊列,正常運行的在另一個隊列,時間片不會分配到阻塞隊列。

Java中的線程

  • 新建現成的三種方式
    • 實現Runnable方法,重寫run方法,利用代理模式交給Thread類.start。
    • 繼承Thread類,重寫run方法,start調用
    • 實現Callable接口,重寫call方法,可以有返回值和異常,通過FutureTask類接收返回值。可以在主線程運行的過程中異步運行線程A,然後在主線程中通過判斷是否獲得線程A的返回值來決定下一步操作。
  • 守護線程,即後臺線程,GC的時候用的就是這個,當所有普通線程去世後,守護線程就退出。從普通線程生成的線程默認是普通線程,從守護線程中生成的默認是守護線程。
  • sleep方法可以讓線程休眠一段時間,但是不釋放鎖,因爲sleep是線程方法,和鎖沒關係。
  • 讓出本次處理器時間片,即yield方法,表示建立處理器不要在此時間片時間內繼續執行本程序,但是並不代表一定不執行。
  • join方法,A線程中調用B.join的意思是,A等待B運行完。
  • java的線程調度策略是基於優先級的搶佔式調度
  • 減少上下文切換的方法
    • 無鎖併發編程。多線程競爭鎖的時候會引起上下文切換 TODO:爲什麼,可以通過Hash算法將ID取模分段,不同線程處理不同段。
    • CAS算法, 原子操作,不用上鎖。
    • 採用儘量少的線程,只創建必要數量的線程,不要讓大量線程處於等待狀態。
    • 使用協程而不是線程,協同式多線程是把一個任務分配到不同的線程上,但是一次性只有一個線程能夠運行,並且需要手動的釋放線程,程序員完全掌握線程的釋放時機,佔用時機,本質上是串行化運行,但是可以簡化編程,LUA語言中用的就是協程。

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

原子性、可見性與有序性

原子性

  • 某一個或一組操作視爲不可被其他線程打斷的操作。線程1對A上鎖後,其他申請A鎖的線程都會進入阻塞隊列,直到線程1釋放鎖,如果線程2不要求A鎖,那線程2和線程1是可以交替時間片運行的。舉例,1想在A坑大便,3也想去A坑大便,2想去B坑大便。於是1和3爭奪A坑,1成功了,就上鎖以防止自己一瀉千里的時候3突然進入。3此時只能在外面等着,而2可以去B坑。synchronized具有原子性,釋放鎖之前必須先將變量同步回主內存,因此也具有可見性。
  • 如何保證原子性。
    • 採用局部變量或ThreadLocal類。即將線程共有變量變爲線程私有的,這樣就不怕其他線程改變你的變量了。
    • 採用鎖。爭搶同一份資源的線程,當其中一個爭搶到後就上鎖,其他的進阻塞隊列等待釋放鎖。
      • synchronized可以鎖一個對象,對於成員方法可以用this作爲鎖,對於靜態方法可以用class作爲鎖
      • 可重入鎖是指當一個線程請求一個它已經獲得的鎖的時候,這個請求仍然會成功。
    • 聲明爲final。

可見性

  • 可見性指的是當一個線程修改一個共享變量時,另外一個線程能夠讀到這個修改的值。volatie確保可見性,synchronized保證可見性和原子性,final保證可見性因爲無法被修改,但是也有例外。
  • 可見性不保證原子性,就算i被設計成volatie,多線程下的i++依然會出現問題。

有序性

  • 有序性是指代碼在單線程情況下,執行的最終結果和重排序之前是一樣的,但是放在多線程中卻可能導致併發問題。

volatile和synchronized的應用與實現

應用與實現原理

  • volatile的原理:1.將變量的值寫入當前CPU緩存中。2.通知其他CPU緩存的數據無效,因此其他CPU必須重寫讀取。
  • synchronized的應用
    • http://www.importnew.com/21866.html

    • 對於普通同步方法,鎖是當前實例對象,即防止多個線程同時訪問這個線程的synchronized方法,但是非synchronized方法可以被其他線程訪問。但是鎖的是同一個實例,因此如果A a1=new A(),A a2=new A(),那麼a1不會阻塞a2
      • 在方法前加synchronized關鍵字相當於對整個方法加鎖,鎖是this,即當前對象實例
      • 構造方法不能使用synchronized關鍵字,但是可以用synchronized代碼塊同步
    • 對於靜態同步方法,鎖是當前類的class對象。值得注意的是子類重寫父類方法後不會自動具有synchronized關鍵字,需要顯式給予,如果子類沒有重寫相當於調用父類方法,自動獲得同步效果。
      • 鎖是當前類的class對象,即這個類的所有實例化對象都被鎖。
    • 對於同步方法塊,鎖是synchronized括號裏配置的對象
      • 對指定對象加鎖:除了拿到鎖的那個線程,其他所有想操作lock對象的線程都被阻塞在使用lock的前一句代碼,直到lock被釋放。

          class Test implements Runnable
              {
              private byte[] lock = new byte[0];  // 特殊的instance變量
              public void method()
              {
                  synchronized(lock) {
                      // todo 同步代碼塊
                  }
              }
              
              public void run() {
              
              }
          }
        
      • 如果synchronized(this){}即是對當前對象實例加鎖。

    • 不允許有synchronized和非同步的同名方法
    • 如果寫操作爲synchronized,但是讀操作不是,那麼讀操作可能產生誤讀。因爲非synchronized方法可以讀取數據,但是這個數據可能同時在synchronized方法中存在,同步方法中雖然修改但是還未寫進主存。容易產生髒讀問題。
    • 總結:
      • 普通同步方法,鎖是當前實例對象,即方法的this,即調用方法的實例對象.
      • 靜態同步方法,鎖是當前類的class對象,即所有能調用該靜態方法的類、實例對象
      • 同步方法塊,鎖是括號裏面的對象。
      • 能不能鎖住要看爭奪的是不是同一個對象,比如線程A中的lock和線程B中的lock(如上代碼中的lock),A和B線程如果代理的是不同的Runnable,那麼就不會同步,因爲新建了兩個Runnable對象,也就有了兩個lock。
  • synchronized的原理:通過monitorenter和monitorexit鎖定兩個指令之間的所有指令,當遇到enter的時候就會檢查鎖對象的對象頭,來決定當前線程是CAS嘗試獲取偏向鎖還是自旋還是阻塞。當遇到exit時表示應該釋放鎖。

鎖的底層實現原理

java對象頭中有鎖

  • 爲什麼synchronized鎖的是對象?因爲每個對象的對象頭中都保存着一個鎖的信息。

鎖的升級與對比

  • 輕量鎖。線程中會創建一個存儲鎖記錄的空間,叫做lock record,裏面存有displace mark word:初始化爲鎖的那個對象的對象頭中的mark word。
    • 加鎖的過程是線程通過CAS將對象的mark word替換成指向鎖記錄的指針,而鎖記錄中的owner指針又指向對象頭。(線程的lock record的owner指針指向對象的mark word,而對象的markword已經變爲了指向lock record的指針,這是一個雙向指向的關係。)如果成功,說明當前線程獲得鎖,如果失敗則嘗試自旋獲得。所謂自旋是指線程不放棄CPU時間片,在此期間頻繁查詢某一條件是否爲真(即鎖是否釋放),直到鎖的持有者釋放鎖,因此不會導致鎖釋放時CAS的失敗。自旋的問題是1.佔用CPU時間2.視圖遞歸的獲得自旋鎖必然引起死鎖,因爲本實例已經獲得了一個鎖,但是遞歸中的第二個實例也像獲得自旋鎖,就變成了我等我自己。
    • 解鎖的過程是線程將lock record中的displaced mark word通過CAS放回到對象頭中。如果成功則替換完畢,如果失敗,說明有鎖競爭,此時膨脹爲重量鎖。注意,輕量鎖的輕量體現在不阻塞,只是自旋
  • 重量鎖。就是互斥鎖,其他所有想要鎖的線程,在同步代碼塊處阻塞。當線程釋放鎖後,會喚醒所有阻塞中的線程。
  • 偏向鎖。本質上是最輕量的輕量鎖,用於併發可能性較低的情況。比如你的方法是同步方法,但是其實只是偶爾會發生併發,大多數情況下都是不涉及併發的,就可以用偏向鎖。
    • 加鎖過程,將對象頭的mark word中偏向鎖標誌位設置爲1,存儲線程ID,表明該鎖偏向於這個ID的線程。如果mark word中是偏向鎖,且存儲着ID,就表示這個ID的線程自動獲得了鎖。你看,要滿足兩個條件,1.有偏向鎖。2.存儲着ID。假如是偏向鎖但是沒存儲ID,就嘗試用CAS設置當前進程ID,如果不是偏向鎖,就採用CAS競爭偏向鎖,如果競爭失敗則說明偏向鎖不適合當前場景,應該變爲輕量鎖。
    • 解鎖過程,一旦發生競爭,持有偏向鎖的線程就釋放鎖。釋放之後根據鎖對象狀態,如果鎖對象沒有正在被加鎖,即線程沒有執行同步塊,就撤銷偏向鎖,重新偏向線程。如果鎖對象已經加鎖,就考慮變爲輕量鎖來爭奪鎖。這裏考慮有兩層意思,一是如果釋放的時候沒有釋放成功就會膨脹成重量鎖,而是如果自旋過久也會膨脹成重量鎖。
  • 鎖轉換關係

原子操作的實現原理

  • CAS,Compare and swap,指的是比較並交換,有兩個操作數,一個是期望的舊值,一個是新值,就有期望的舊值和被替換的值相同,纔會替換爲新值。否則就進行循環CAS,這裏的循環指的是反覆嘗試CAS,如果CAS失敗,則更新期望的舊值,並進行下一次CAS。出現的問題是1.ABA,2.循環開銷大,3.只能保證一個變量的原子操作。解決方案1.時間戳3.將多個變量合成一個對象進行原子更新。
  • 鎖。JAVA中除了偏向鎖都採用了循環CAS的方式。可以理解,因爲偏向鎖只需要一次CAS(在創建偏向鎖的時候),之後的操作都只是比較偏向鎖中的ID是否指向線程而已,一旦出現CAS失敗的情況就根本不會循環CAS,而是直接變爲輕量鎖。輕量鎖情況下通過循環CAS競爭鎖,競爭到的持有鎖,沒有的自旋,直到持有鎖的線程釋放後,自己再CAS拿鎖。如果自旋時間過長,或者持有鎖線程釋放失敗,此時膨脹爲重量鎖。

Java內存模型

java內存模型的基礎

兩個問題與兩種模型

  • 關鍵問題是:線程間的通信與同步。模型爲:消息傳遞與共享內存。
  • java採用共享內存的方式。

java內存模型的抽象結構

  • 只有堆、方法區中的變量(即實例域、靜態域、數組元素)纔會有併發問題。
  • 主內存中是共享變量,每個線程中都有共享變量的一個副本,你在自己的線程裏修改這個副本並不會導致主內存中的變量發生變化,除非你將自己工作線程中的變量刷新到了主內存中。線程B可以通過在主內存中讀取A寫入的變量來實現A與B線程的通信。

java中重排序

  • 3種重排序:編譯器重排序,CPU重排序,內存重排序(緩存與主內存數據不一致,各個CPU或核心間緩存數據也不同步)。JMM通過制定重排序規則,在編譯器層面上禁止部分重排序。在生成的指令中插入內存屏障來禁止部分CPU重排序。
  • 4種內存屏障。loadload,storestore,loadstore,storeload。以storeload爲例,store1 storeload load2,即store1刷新到主存之後才允許讀load2,並且先於一切讀指令。
  • 數據依賴性。注意,數據依賴性只在單線程或單處理器中被考慮,多線程情況下不考慮數據依賴性。
  • as-if-serial。同樣也是單線程情況下,不論怎麼重排序,不能改變結果。
  • 程序順序規則。即happens before,語句a happens before 語句b,但是語句b可能重排序在語句a之前。
  • 重排序會破壞多線程程序語義。

happens-before

  • JMM通過規定一些規則來確保happens-before。happens-before是一種定義,是告訴程序員,只要兩個操作之間滿足了happens before關係,則JMM保證了以下的內存可見性:
    • 一個線程中的每一個寫操作都對下一個操作可見,這叫一個線程的每個操作都happens before後序操作。
    • 一個線程解鎖後,該線程解鎖前做的所有寫操作均對另一個獲得了同一把鎖的線程可見,這叫對一個鎖的解鎖,happens-before隨後對這個鎖的加鎖。
    • 對volatile變量的寫操作對之後所有對這個volatile變量的讀操作都可見。
    • 傳遞性。如果A happens before B,B happen before C,則A happens before C
    • 以上所有“以後”都指的是代碼順序,值得注意的是,代碼順序並不意味着CPU執行順序,因爲有重排序。

順序一致性

  • 數據競爭是指在一個線程中寫一個變量,在另一個線程中讀一個變量,但是寫和讀之間沒有通過同步來排序。JMM保證如果正確同步,就保證順序一致性。
  • 所謂順序一致性指的是A線程內部有a1,a2,a3的代碼執行順序,B線程內部有b1,b2,b3的代碼執行順序。如果正確同步,執行順序應該是a1,a2,a3,b1,b2,b3,整體有序且線程A,B內部都有序。如果同步不正確,b1,a1,a2,b2,a3,b3,線程A,B有序,但整體無序。
  • JMM在未同步時並不保證任何的順序一致性,只保證最小安全性,即數據不會無中生有,要麼是默認初始值,要麼是髒數據。正確同步時同步塊內(臨界區)可以重排序,但是準守順序一致性。
  • long/double都是64位的,因此寫操作分高32位和低32位,非原子操作。但是讀操作必須具有原子性。 不過volatile long/double必然是原子的

內存語義

volatile的意義

  • 簡單理解volatile:具有可見性,對於一個變量的寫操作總能被之後的讀操作看到,即永遠讀到最新值。但是不具有原子性,即i++哪怕變量是volatile的,也不是原子性,因爲有可能第一個線程中的i自加的過程中,第二個線程的i讀到了值,此時第一個線程還沒有涉及到寫的動作,因此volatile的作用也無從談起。
  • volatile的重要意義並不僅僅在修飾的那個變量之前,當其與happens before結合時將有無窮的力量。因爲所有定義在valoatile 變量寫操作之前的寫操作都必然happens before於volatile。而下一次讀volatile有必然讀到最新的值,因此,可以說在讀volatile的時候,必然獲得了最新一次寫volatile之前的所有可見性!

內存語義

  • volatile讀的內存語義爲:必須從主存中讀取。寫的內存語義:立即寫入主存,並且volatile之前的變量寫操作也會立即刷新至主存中。
  • 具體實現:利用內存屏障實現。不允許重排序,以此保證可見性。

鎖的內存語義

  • 釋放鎖後,所有操作都將對獲得鎖的線程可見。
  • 鎖釋放的內存語音:立即寫入主存。獲得鎖:必須從主存中讀取。
  • JUC包中利用volatile和CAS實現鎖的內存語義。在釋放鎖的時候寫volatile類型變量state,在獲得鎖的時候讀volatile變量state,就實現了獲得鎖的線程獲得了釋放鎖的線程的一切操作的可見性。CAS保證原子性,並且具有volatile的讀寫內存語義。由此Lock類即保證可見性又保證原子性。
  • 至於synchronized關鍵字的底層實現,通過monitorenter和monitorextit表示獲取鎖和釋放鎖,每當遇到這個指令就要去根據對象頭鎖的類型來進行鎖爭奪操作。

final的內存語義

  • 要解決什麼問題?構造函數中返回引用和構造方法中賦初值並非順序性的,即有可能先返回了一個引用,但是這個引用沒有賦初值。
  • 構造函數必然先初始化final域,後返回引用。
  • 讀一個對象的final域之前,必然先讀這個對象。
  • 如果final修飾的是引用類型。幾乎和之前一樣,想要讀取這個對象之前,先要初始化這個對象的final域。
  • 總結,只要被構造對象的引用沒有在構造函數中逸出,那即便不使用同步也可以保證在任意線程中都能看到這個final域在構造函數中被初始化之後的值。
  • **什麼叫逸出?**一個類不是通過構造方法的返回值發佈引用,而是在內部發布了引用。

雙重檢查鎖定與延遲初始化

  • 經典雙重鎖定,錯誤版(懶漢式:程序需要這個實例時纔去創建對象,創建時機晚於類加載過程,因此被稱作懶)
    public class Instance{
        private Instance instance;
        private Instance(){}
        public Instance getInstance(){
            if(instance==null){
                synchronized(Instance.class){
                    if(instance==null)
                    instance = new Instance();
                }
            }
            return instance;
        }
    }
  • 問題:因爲構造函數中可能存在重排序,即先返回對象引用,後初始化。這在單線程中沒什麼問題,只要在使用對象之前確認初始化即可,但是在多線程中,別的線程可能使用了其他線程中還未初始化完畢但已經返回引用的實例。
  • 解決方法:
  • 1.利用volatile修飾instance,以禁止初始化和返回instance實例的重排序。
  • 2.採用餓漢式單例(類加載的時候就創建好實例)
    • 當調用getInstance方法時,因爲使用了invokeStatic指令,會進行類加載,因此會初始化類變量Instance。因爲類加載時有鎖以同步類加載的初始化過程,以此來保證單例。因爲類加載的時候就創建好實例,因此外界無法獲取到還未初始化的實例。
        public class Instance{
            private Instance(){}
            private static Instance = new Instance();
            public static Instance getInstance(){
                return instance
            }
        }

JSR-133的語義增強

  • volatile,嚴格限制volatile變量與普通變量的重排序,並且volatile的寫-讀與鎖的釋放-獲取具有相同語義。
  • final通過讀、寫重排序規則,保證final變量具有初始化安全性。

併發編程基礎

理解線程

線程狀態

  • 線程的狀態分爲新建,運行,阻塞,等待(被動喚醒),超時等待(超時喚醒),終止六種狀態。

Daemon線程

  • 守護線程,當一個JVM中不存在非守護線程後,即退出。守護線程退出時的finally塊不一定會執行,因此不得在Daemon線程的finally塊中做資源釋放之類的工作。可以設置一個線程爲Daemon。

Thread部分源碼分析

  • 構造方法。構造方法中調用init方法,新建線程都是從當前線程中派生出來的,繼承了當前線程的組,優先級,是否爲守護線程等信息。
  • start。調用start0方法,其實是運行Runnable的run方法。
  • interrupt相關。把中斷理解爲一個標誌位,當調用thread.interrupt()的時候,表示想要中斷thread線程,將標誌位置真。thread內部需要自己寫代碼判斷是否有中斷,如果有中斷需要自己寫代碼處理中斷。在有阻塞的情況下,比如sleep或者wait,此時線程會阻塞,但是會不停的輪詢標誌位,一旦發現標誌位爲真,就會拋出異常,並清除標誌位。可以通過isInterrupted方法來判斷標誌位,通過interrupted方法手動清除中斷標記。
    • 還有兩種情況當thread.IsInterrupted會返回false。1.拋出異常前清除中斷標誌位。2.線程結束後,返回false。
  • 過期的suspend,resume和stop方法。意思分別爲暫停,恢復和停止。看上去很美好,但是會帶來副作用,stop過於強勢,會直接終結線程而不保證資源的正確釋放。suspend/resume不會釋放鎖,因爲容易造成死鎖。
    • 假如有A,B兩個線程,A線程在獲得某個鎖之後被suspend阻塞,這時A不能繼續執行,線程B在獲得相同的鎖之後才能調用resume方法將A喚醒,但是此時的鎖被A佔有,B不能繼續執行,也就不能及時的喚醒A,此時A,B兩個線程都不能繼續向下執行而形成了死鎖。這就是suspend被棄用的原因。

    • 代替方式:通過wait和notify的等待通知機制代替提暫停和恢復,新機制的特點是不抱鎖睡覺。通過interrupt和下面的安全終止進程來實現stop,新的關閉的特點就是手動釋放資源。
  • 安全的終止進程。設置一個標誌位,當標誌位改變的時候,線程中斷。這個邏輯的代碼都需要自己手寫,相當於自己寫一個線程的析構函數。
  • join。**在A線程中調用B線程.join();代表直到B線程結束,A才繼續運行。這是怎麼做到的呢?
    • 1.在A線程中鎖B線程實例,然後while(B線程沒死){B線程.wait()}。再說一遍,這不代表B線程wait,而是A線程從同步隊列中加入了等待隊列。
    • 2.線程B結束後,在JVM中調用notifyAll(),將所有想要持有B線程鎖的線程加入同步隊列中。那麼自然A也在同步隊列中,因此A線程得以繼續運行。
  • ThreadLocal。指的是線程變量,即線程獨有的,主要是其數據結構比較有意思。Thread中有成員變量,是ThreadLocal類型的,這個類本身可以理解爲是一個類似於Math的工具類,也不需要實例化。關鍵是ThreadLocal裏面有個內部類叫ThreadLocalMap,這是一個鍵值對的Map類,鍵默認就是ThreadLocal,值是任意類型的,通過ThreadLocal中的泛型決定。這個Map類和HashMap完全不同,沒有采用拉鍊法,而是採用開放地址法,如果發現hash衝突了則向後移動一位,直到有空位。因此可以簡略的說**每個線程中都有一個ThreadLocal實例,而ThreadLocalMap(map這是ThreadLocal的類變量,因此所有的ThreadLocal都公用這個一個map)中保存着所有線程中的ThreadLocal和對應的值。**所以A線程的threadlocalA.get()的實際過程是先獲得A線程中的ThreadLocalMap實例,然後通過這個map和threadlocalA(作爲key)獲得值。那如何在線程中使用ThreadLocal呢?只要線程中有threadlocal變量,然後調用set方法,就會自動將這個變量添加到thread和threadlocalmap裏。

wait/notify,等待/通知機制

  • 爲什麼叫通知機制?相當於一個線程可以通知其他線程開始幹活,整個過程開始於一個線程,最終執行於另一個線程。

  • wait/notify使得雖然同步塊內的內容保證原子性,但是仍然可以交錯運行,意思是A的同步塊內有a1,a2兩件事,B的同步塊內有b1,b2兩件事。如果沒有wait、notify,那麼a1,a2必須要全部運行完畢後才釋放鎖,然後b1,b2。但是通過wait可以a1後提前釋放鎖,當場阻塞,然後運行完b1後,notify讓A繼續a2,使得線程間可以有協作。

  • 永遠在循環裏採用wait和notify,因爲這樣可以在線程睡眠前後都檢查wait的條件。爲什麼要這樣呢?如果用if,用notifyAll的時候會產生虛假喚醒,如果喚醒了多個線程A,B,都放入同步隊列中,A被喚醒直接開始生產,生產達到上限後A線程wait。此時可能是消費者線程或者生產者線程獲得鎖,如果生產者線程B獲得鎖,將直接開始生產,這是不對的,在生產以前還要判斷一下是否要wait。

      while(條件不滿足){
          對象.wait();
      }
      對應的處理邏輯
    
  • 如果想用obj的wait和notify,那麼一定要先獲取obj的鎖,即同步塊一定要鎖obj。只有獲得鎖的線程才配wait和notify,wait釋放鎖,但是notify只喚醒,不釋放鎖,同步塊結束才釋放鎖。

  • wait的本質是從同步隊列放到等待隊列,notify是從等待隊列放置到同步隊列。同步隊列表示有爭搶鎖的機會,等待隊列沒有機會。同步隊列中的狀態爲阻塞。

管道輸入、輸出流

  • PipedOutputStream,PipedInputStream,PipedWriter,PipeReader,用於線程之間的數據傳輸,可以在一個線程中接收輸入,但是在另一個線程中輸出。用之前要用connect函數將輸入輸出流連接起來。

顯式鎖

顯式鎖的接口:Lock接口

  • 其實比起synchronized,我更喜歡lock一點,因爲可以手動控制上鎖和解鎖的時機。

      Lock lock = new ReentrantLock();
      lock.lock();
      try{
      }finally{
          lock.unlock();
      }
    
  • 區別與synchronized的特性

    • 嘗試非阻塞地獲取鎖,tryLock
    • 能被中斷的獲取鎖,lockInterruptly
    • 超時獲取鎖,tryLock(time)
  • 輪詢鎖,即tryLock方法,特性是如果獲得鎖立即返回true,否則返回false。可以根據這個特性來多次嘗試獲取,通過自定義的重試策略進行輪詢。好處是避免了死鎖,因爲trylock後發現沒有獲得鎖B,就嘗試釋放已經獲取的鎖A,然後再重新嘗試獲得鎖A和B,這樣就避免了死鎖。

  • 可中斷鎖,第一個try塊用來捕捉可中斷鎖中可能拋出的異常,第二個try塊用於釋放鎖。和synchronized處理中斷的方式(標誌位置1,是否處理看被中斷方的心情)不同,一旦被中斷就立刻拋出異常,然後必須處理。比如t2線程中lock.lockinterruptly,那麼只要調用t2.interrupt()即可中斷t2線程。

    Lock lock = new ReentrantLock();
    try {   //第1個try塊

        lock.lockInterruptibly();   //可中斷的鎖

        try {   //第2個try塊
            // ... 具體代碼
        } finally {
            lock.unlock();  //釋放鎖
        }

    } catch (InterruptedException e) {//第一個try所配對的catch,用來處理可能存在的中斷
        // ... 在被中斷的時候的處理代碼
    }
  • 定時鎖。可中斷的基礎上還加入了超時的概念,即如果在一定時間內無法獲得鎖,就GTMDTTKP。
    Lock lock = new ReentrantLock();
    try {
        boolean result = lock.tryLock(1000L, TimeUnit.MILLISECONDS);
        if (result) {   //在指定時間內獲取鎖成功
            try {
                // ... 獲取到鎖的代碼
            } finally {
                lock.unlock();  //釋放鎖
            }

        } else {   //在指定時間內獲取鎖失敗
            // ... 獲取鎖失敗的代碼

        }
    } catch (InterruptedException e) {
        // ... 被中斷時的異常處理代碼
    }

顯式鎖的基礎:隊列同步器

AQS幹了一件是什麼事兒?

  • AQS幾乎是JUC包中鎖和其他組件的基礎。比如ReentrantLock中就有一個Sync內部類繼承自AQS,ReentrantLock實現了Lock的方法,實現的手段是底層調用AQS的模板方法,外面包裝成Lock接口方法。換而言之,AQS是底層,Lock是表現。
  • AQS有兩個核心,state同步狀態和獲取/修改狀態。通過不同的獲取和修改同步狀態的方式,使得AQS有不同的表現形式,以應對JUC中不同的組件。

AQS中的同步隊列

  • 同步隊列中每個節點爲Node,Node代表一個線程,還包括線程的等待狀態、前後節點。
  • 同步隊列有head和tail兩個節點用於操作這個FIFO的隊列。在獨佔模式下隊列頭代表獲取同步狀態成功的節點,這是唯一的,因此更新頭結點的過程是無併發的。當一個線程獲得同步狀態(或者鎖)的時候,其他線程都要添加到隊列尾,這是存在競爭的,因此通過循環CAS實現添加到尾節點。在共享模式下,釋放頭結點和增加尾節點都是需要循環CAS的。
    • 獲得鎖是獲得同步狀態的一個子集。獲得同步狀態可以幹很多事,比如你獲得同步狀態後通過CAS把它修改掉以表明你獲得了鎖,這是一種方式。但是你也可以做別的事,隨着進一步的學習,我們後面再看。

AQS部分源碼分析

  • acquire。注意這個方法我們是不能重寫的,我們只能重寫tryaAcquire方法,即以下的分析,只有第一步我們可以定製。比如我們寫一個MyLock extends AQS,那麼只需要重寫tryAcquire,但是調用acquire方法,即可實現下列描述的全部功能。
    • tryAcquire:在AQS中僅僅是輸出異常,告知你應該自己重寫這個方法,用來嘗試獲取同步狀態。失敗後進入addWaiter方法,即將同步失敗的線程加入同步隊列中
    • addWaiter:此方法中嘗試通過CAS設置尾節點,失敗後通過enq方法利用死循環CAS確保尾節點設置成功
    • acquireQueued:此方法作用是維護這個隊列的每個節點。
      • 如果這個節點是當前隊列的第二個節點就嘗試獲取同步狀態,如果不是就採取以下措施:維護隊列中的waitStatus
      • shouldParkAfterFailedAcquire:更新隊列中節點的waitstatus,如果前驅節點是-1SIGNAL表示前驅節點在同步隊列中等待中,即當前節點狀態爲0即INITIAL初始化。如果前驅節點是1CANCELED說明前驅節點中的線程已經被終止,因此當前節點需要向前尋找到還沒有被終止的線程然後連接,如果出現[-1,-1,1,1],那再加入節點應該變成[-1,-1,0]。如果前驅節點是別的,比如說是0,那麼就把0變成-1,把自己設置爲0.因此一個正常的隊列應該是[-1,-1,-1,-1,-1,0].
      • parkAndCheckInterrupt:只是加入阻塞隊列還沒用,必須要調用系統底層函數將線程阻塞。這樣就做到物理意義上的線程阻塞和邏輯意義上的線程阻塞相統一,物理意義上指的是這個線程確實被阻塞了,邏輯意義上指的是這個線程在同步隊列的非頭節點。
      • 總結:頭節點的後節點允許死循環式的檢查自身是否得到喚醒,而其他階段在處理完waitstatus之後就進入了阻塞狀態。
  • release。
    • tryRelease:在AQS中僅僅是輸出一個異常,告訴你怎麼釋放你應該自己去重寫這個方法。
    • unparkSuccessor。如果釋放成功,則喚醒下一個節點,如果下一個節點的狀態爲1,則從尾節點向前遍歷到第一個狀態爲負的。
    • LockSupport.unpark(s.thread).喚醒next節點的線程並替代頭結點。
  • 獨佔鎖實例,互斥鎖
    public class PlainLock {
        private static class Sync extends AbstractQueuedSynchronizer {

            @Override
            protected boolean tryAcquire(int arg) {
                return compareAndSetState(0, 1);
            }

            @Override
            protected boolean tryRelease(int arg) {
                setState(0);
                return true;
            }

            @Override
            protected boolean isHeldExclusively() {
                return getState() == 1;
            }
        }

        private Sync sync = new Sync();


        public void lock() {
            sync.acquire(1);
        }

        public void unlock() {
            sync.release(1);
        }
    }
  • acquiredShared:

    • tryAcquireShared:返回值不再是boolean,而是int,表示一個區間,即剩餘共享的數量,當<0時代表不可共享,應當加入同步隊列中。
    • addWaiter。若tryAS<0,則addWaiter。和獨佔鎖的區別是,1.增加的節點均表示爲共享模式2.通過setHeadAndPropagate來設置頭結點。
      • setHeadAndPropagate(node,int propagate)。後面的參數表示還允許傳播多少個頭節點。怎麼理解呢?比如允許2個線程持有共享鎖,那麼當A,B線程共享後,C只能執行addWaiter,但是很巧,C在加入隊列的過程中,A,B已經釋放了鎖,那C在tryAS的結果是1>0,說明自己可以當做頭結點,同時propagate=1,說明還能傳播一個,讓在隊列中等待的D也可以獲得鎖。怎麼獲得呢?如果propagate>0,且當前節點node.next == SHARED,那麼就立即釋放當前節點。你想啊,立即釋放當前節點的意思不就是說讓下一個節點變成頭結點麼?
  • releaseShared:

    • tryReleaseShared,調用dotRS,其中將當前節點waitStatus置0然後喚醒後序節點。如果當前狀態已經爲0了,那就將頭結點設置爲-3PROPAGATE,表示下一次同步狀態獲取將無條件傳播,什麼時候會導致這種情況呢?就是隊列中有且僅有頭結點。
  • 共享鎖實例,允許兩個線程共享

    public class DoubleLock {
        private static class Sync extends AbstractQueuedSynchronizer {

            public Sync() {
                super();
                setState(2);    //設置同步狀態的值
            }

            @Override
            protected int tryAcquireShared(int arg) {
                while (true) {
                    int cur = getState();
                    int next = getState() - arg;
                    if (compareAndSetState(cur, next)) {
                        return next;
                    }
                }
            }

            @Override
            protected boolean tryReleaseShared(int arg) {
                while (true) {
                    int cur = getState();
                    int next = cur + arg;
                    if (compareAndSetState(cur, next)) {
                        return true;
                    }
                }
            }
        }

        private Sync sync = new Sync();

        public void lock() {
            sync.acquireShared(1);     
        }

        public void unlock() {
            sync.releaseShared(1);
        } 
    }
  • 超時獨佔鎖
    • tryAcquireNanos().和《java併發編程的藝術》描寫的不同,java1.8中對這邊的邏輯代碼進行了修改。通過deadline來表示停止自旋的時間,在死循環中判斷,如果自己超過了deadline的時間後還在死循環中,就跳出循環並返回false。如果自己沒有獲得同步狀態並且時間還沒到,就用parkNanos方法讓自己堵塞一段時間,這個時間最長不超過nanosTimeout。
      • LockSupport.parkNanos(this, nanosTimeout);這句話是調用系統底層函數,要求該線程阻塞不超過nanosTimeout時間。
      • 如果設置的超時時間非常短,就不會使用該超時時間,而是依然採用自旋,因爲非常短的超時等待無法做到精確,如果此時仍然進行超時等待,可能導致超時等待時間遠遠大於設定值。

LockSupport工具類

  • 通過park阻塞線程,unpark喚醒線程,park(nanos)阻塞最長不超過nanos的時間,在tryAcquire(nanos)中會用到,而一般的tryA中則是直接阻塞直到喚醒。

Condition接口

  • 用法
    • 作用於Object類的wait和notify類似,但是因爲這個顯式鎖是我們手動new出來的,因此這個Condition也是來自於鎖的。即一個鎖可以有一個condition,這和synchronized是類似的,即一個synchronized鎖一個對象,一個對象可以有wait和notify一組關係,一個Lock具有多個condition,這個condition有await和signal。
    • 上文說到的多個就是顯著優點,一個鎖可有多個狀態。Lock在conditionA中await的意思是,lock只能在conditiona的signal或signalAll及中斷中被喚醒,無法被本lock的conditionB喚醒。
  • 實現原理即方法分析
    • AQS中包括一個ConditionObject impelements Condition。比如ReentrantLock中方法newCondition()本質上是返回ReentrantLock中Sync類實例sync的new ConditionObject()。這個不是類方法,因此一個Lock可以return很多condition出來。
    • 等待隊列是一個FIFO隊列,每個節點複用了同步隊列的Node,但是是單向隊列,只指向後項節點,其輔助頭尾引用爲firstWriter與lastWriter。condition的await方法本質是將當前持有鎖的這個線程放置到等待隊列的末尾,這個過程必然是不存在鎖競爭的,因爲調用await之前該線程還沒有釋放鎖。signal的名字叫喚醒,聽上去好像調用之後原本await的線程就會突然起牀一樣,但是實際上只是把原本await的線程加入同步隊列,讓他有機會被調用而已。
    • ConditionObject實現了包括await,await(nanos),signal,signalAll等方法。
      • await方法,能調用await的線程必然持有鎖,因此必然是同步隊列的首節點,因此此方法簡單來說就是將同步隊列的頭節點放到等待隊列的尾節點
      • signal方法,能調用signal的線程必然持有鎖,因此必然是同步隊列的首節點,但是和這個首節點沒啥關係,重點是會將等待隊列的首節點放置到同步隊列的尾節點,讓原本等待隊列中必然阻塞的線程到同步隊列中參與競爭

顯式鎖的一種常見實現:重入鎖,ReentrantLock

  • synchronized是隱式可重入的,因爲一個線程不論獲得多少次obj的鎖,這個對象頭都是指向這個線程的,所以必然可重入。但是設計語言層面的鎖的時候,可重入鎖的實現有兩點要注意——1.線程再次獲得鎖的時候,tryAcquire是成功的。2.線程獲得了1000次鎖,釋放的時候也要釋放1000次,就像代碼塊一樣,你lock.lock了一千次,那必須lock.unlock一千次,直到最後一個unlock該線程才徹底釋放鎖。

  • ReentrantLock,內部類static final Sync extends AQS,static final FairSync和NonfairSync extend Sync。


  • 構造函數:默認爲非公平鎖,sync變量 = new NonfairSync(),因此lock.lock()實際上調用的是非公平鎖的lock()。

  • lock。lock方法邏輯是這樣的

    • **非公平情況下:**調用nonfairsync的lock,先嚐試一次CAS,這次cas是插隊的,如果失敗了則調用acquire方法(說明要排隊了),在acquire方法中先調用tryAcquire方法(這個方法在nonfairsync中已經重寫了),然後調用addWaiter和acquireQueued。
      • nonfairsync的TryAcquire。上文說到在nonfairSync中已經重寫了tryAcquire,重寫的邏輯都在nonfairTryAcquire中。state用於計數,通過當前線程是否等於持鎖線程來決定計數器是否自增,以表明可重入的概念。如果持有鎖的線程和當前想要持有鎖的線程是同一個,則state自增,如果不存在持有鎖的線程,則當前線程持有鎖,如果當前想要持有的鎖不是已經持有鎖的,返回false。通過tryAcquire中對想要獲得鎖的線程和持有鎖的線程的判斷來實現可重入
    • 公平情況下,調用fair的lock,lock中直接acquire,說明不管哪一個線程都要排隊。在acquire方法中先調用tryAcquire方法(這個方法在nfairsync中已經重寫了),然後調用addWaiter和acquireQueued。
      • fairsync的tryAcquire。與非公平的不同,公平的要額外判斷一下本線程是否是同步隊列中頭結點的下一個。通過AQS的hasQueuePredecessors實現,但是注意這個方法返回的意思是:還有前驅節點,取反後表示當前節點就是頭結點的下一個。**爲什麼要判斷?**因爲發出lock請求的線程不一定在隊列中,比如剛剛釋放的哪一個線程,如果它也來搶,就會因爲在此處判斷爲不是頭節點的後一個而被放置到隊列的尾端,從而實現了FIFO,即公平。
    • **爲什麼會出現不公平?不是每次都從隊列頭結點的下一個取麼?不是必然按着順序麼?**鎖釋放後,喚醒下一個節點,此節點內的線程通過CAS嘗試獲取鎖,但是此刻可能有另一個線程嘗試獲取,這個線程並不在隊列中。比如剛剛釋放鎖的那個線程,它就不在隊列中,但是它還想獲取鎖,此時它也CAS,因此有可能插隊。但是公平鎖的情況下,會直接把插隊的放置到隊尾。

    https://zhuanlan.zhihu.com/p/33793637

  • unlock。邏輯是調用AQS的實現類nonfairsync的release方法,因爲在sync類中重寫了tryRelease,因此調用sync的tR方法。

    • tryRelease。這個方法不是寫在nonfairsync裏面的,而是寫在其父類Sync內的。只要釋放一次鎖就減少一次計數,當計數器爲0的時候,該線程徹底釋放鎖。
    • 通過setExclusiveOwnerThread(null),標記爲獨佔鎖的持有線程爲null。
  • tryLock:就是不執行acquire,直接執行tryacquire,沒有後續加入隊列的操作。

  • tryLock(time):先立刻tryAcquire一下,然後再doAcquireNanos,在tryA中是不存在加入隊列的操作的,在doAN中是存在的,因爲doAN中要加入阻塞隊列先休眠一段時間再重新檢測自己是否能獲得同步狀態。

顯式鎖的另一種常見實現:讀寫鎖,ReentrantReadWriteLock

  • 讀是共享的,寫是排他的,不同線程間的讀和寫是互斥的,但是一個線程可以同時獲得讀鎖和寫鎖。讀寫鎖依然是可重入且支持公平/非公平設定的。
  • 讀寫狀態的設計,將state這個int類型的32位分爲高16位和1第16位。高16位表示讀鎖狀態,低16位表示寫鎖狀態。
  • ReentrantReadWriteLock中有Sync extends AQS,fairSync和nonfairSync extends Sync,WriteLock impelements Lock,ReadLock impelements Lock。
  • 寫鎖的獲取和釋放
    • lock。直接調用sync.acquire,和可重入鎖不同,這裏沒有直接CAS,讀寫鎖的公平性問題容後再說。這裏是Sync重寫的tryacquire,其中邏輯有四條:1.只要存在讀鎖,不論是不是當前線程,都不允許獲得寫鎖,即不能鎖升級。試想一個場景,1234號線程都有讀鎖正在讀取數據,4號線程獲得了寫鎖,然後寫完後釋放寫鎖,1234還同時持有讀鎖就一起讀數據,結果只有4號讀到的是最新的,123讀的都不是最新的,因此只要有讀鎖,就不允許獲得寫鎖。。。2.從可重入角度,如果想持有鎖的線程和正在持有鎖的線程不一樣,則false。3.因爲只能容納16位的寫鎖,如果可重入次數過多,失敗。4.因爲公平性考慮,寫鎖的lock操作在公平鎖情況下不允許插隊,在非公平鎖情況下可以插隊。第4點在代碼中的體現就是,在fairsync中需要判斷是否爲首節點的後項節點,在nonfairsync中不需要判斷。
    • unlock。直接調用sync.release,tryRelease方法在Sync類中被重寫,沒什麼好說的,減少低16位的計數器即可。
  • 讀鎖的獲取和釋放
    • lock,調用sync.acquireShared.這個是共享鎖,邏輯是:1.如果寫鎖存在,則不得加讀鎖,其他情況加讀鎖,但是允許鎖降級,即獲得寫鎖的線程可以獲得讀鎖

    https://my.oschina.net/meandme/blog/1839265

    • 這篇帖子說到一種情況,在非公平鎖情況下,因爲讀的頻率遠遠大於寫,而且有讀鎖的時候寫線程不得持有寫鎖,因此可能寫線程一直搶不到鎖,從而產生寫線程飢餓,爲解決這個問題,如果隊列中讀線程後面緊接着是寫線程就會優先給寫線程。
  • 鎖降級,寫鎖降級成讀鎖。
    • 操作流程:線程先獲取寫鎖,然後獲取讀鎖,然後釋放寫鎖。一定是在持有寫鎖的時候再持有讀鎖,這個叫做鎖降級。
    • 使用場景:前半段只希望單一線程讀,後半段希望讀可以並行。
    • 目的是:如果線程1不獲取讀鎖而是直接釋放寫鎖,線程2獲取了寫鎖並且寫入了數據,此時線程1無法感知到線程2的數據。如果線程1先獲得讀鎖,線程2因爲這個世界存在讀鎖因此他的寫鎖請求會被阻塞(還記得麼,tryacquire中只要有讀鎖就不允許獲得寫鎖),因此線程1可以繼續使用數據。

Java併發容器和框架

ConcurrentHashMap

概述

  • 爲什麼要用ConcurrentHashMap?
    • hashmap在多線程情況下擴容可能造成循環鏈表(1.8之前,1.8之後採用尾插法,不會造成循環鏈表),next指針永遠不爲null,插入的時候也可能出現數據丟失。

    https://juejin.im/post/5a66a08d5188253dc3321da0

    • hashtable效率低下且過時,之所以效率低下是因爲hashtable的鎖是對整個hashtable上鎖,所有訪問hashtable的線程都是競爭關係,對於這一點ConcurrentHashMap通過分段鎖進行了改進。
  • 1.8之前是通過segment進行分段鎖
  • 1.8對table的每一個桶加鎖,每一個桶裏放的是鏈表或紅黑樹

方法分析

https://blog.csdn.net/u010723709/article/details/48007881

  • 5種構造函數
    • 多了一種可以控制併發等級concurrencyLevel的構造函數。有一個新的成員變量叫volatile sizeCtl,-1表示正在初始化或者有1個線程正在擴容,-N表示有n-1個線程正在擴容,正數表示下一次擴容大小。相較於hashmap,爲了應對併發,如果初始值是可設定最大容量的1/2即採用最大容量,在hashmap中大於最大容量才限幅成最大容量。
  • initTable().
    • table[]的初始化會延遲到第一次putVal。通過initTable初始化並保證併發安全。
      • initTable中通過CAS將sizeCtl設置爲-1表示當前線程創建了一個table,其他線程檢測到該變量變爲-1後就yield讓出時間片。初始化table後將sizecnt設爲0.75*容量。
    • 三個原子操作
      • tabAt,找到table位於i位的node。
      • casTabAt,通過cas設置位於i位的node,使用cas時要求你已經知道了原來這個節點的值是多少
      • setTabAt,利用volatile方法設置i爲的node
  • transfer(),擴容。
    • ForwardingNode,一個特殊的節點,hash值爲-1,用來表示當前節點爲空,或者已經被別的線程擴容完畢。
    • 單線程
      • 利用cas確保只有一個線程可以調用transfer(oldtable,null)方法(程序中通過判斷第二個參數是否爲null來決定是否要新建nextTable),其他線程只能調用transfer(oldtable,nt)以確保可以單線程的構建一個nextTable,容量是原來的兩倍。將sizeCnt設置爲(rs << RESIZE_STAMP_SHIFT) + 2))反正是個負數,盲猜一個-2。當sizeCnt爲負數時候,每有一個線程想要參與擴容就+1,但是這咋還能越加越少呢,太真實了吧,反正這邊就是用一系列的位運算達成了以下結果:sizeCnt的負數表示單線程結束,-N表示有N-1個線程參與擴容。
    • 多線程
      • 多線程並不是比如5個線程都從頭到尾遍歷oldTable進行添加,而是每個線程分配16個桶。線程1是31到16,線程2就是15到0這樣分配的,從後向前遍歷。
      • 只要遍歷到forwarddingNode就說明這裏已經被處理過了,即跳過當前位置。如果當前位置還未被處理就對當前位置上鎖,然後對這個鏈表進行如下處理。還記得我們的hashmap中的loHead和hiHead麼,就是我說的兩開花操作,擴容後只可能在i+n和i兩個下標。因此ConcurrentHashMap通過算法將原鏈表拆分成兩部分,一部分以CAS放在了i,一部分放在了i+n。

    https://www.jianshu.com/p/f6730d5784ad

  • put()。
    • ConcurrentHashMap的key和value不允爲null,因爲map.get(key)==null時無法判斷key不存在還是value爲null。那我可以用map.containsKey(key)來判斷啊,不行,因爲你get和contains之間由於併發的關係可能key已經從存在變爲不存在了。
    • putVal()內部有個超級大死循環,put成功才返回,因此:
      • 如果當前位置爲空,那就直接put,不用加鎖,因爲如果後面有人併發put的話,自然會加鎖
      • 如果當前位置的hash爲-1,說明遇到了forwardingNode,則在putVal中通過helpTransfer方法使得該線程協助擴容。
      • 如果非空且hash不是-1,那就要加鎖put。
      • 大循環中總有一個時刻能put成功。
  • get().
    • 根據hash值確定位置,如果能查就正常查,如果發現要查的節點的hash=-1,說明這個節點已經不在oldTable,要去nextTable查詢,因此調用Node的find方法,在nextTable中查詢。這也是爲什麼ForwardingNode的next指向了nextTable。
    • ForwardingNode重寫了Node的find方法。調用find方法的如果是forwardingNode,先獲取next指向的nextTable,然後根據hash在nextTable中找。
  • size()與mappingCount()只返回一個大概值,在老版本中是不加鎖的測試兩次大小如果相同就返回值,不然就加鎖所有的segement。可惜1.8已經無法用這種方式實現統計個數了。

ConcurrentLinkedQueue,併發無界單向非阻塞隊列

概述

  • 基於單向鏈表的無界線程安全隊列,FIFO。添加元素添加到尾部,從頭部刪除元素,通過CAS實現,變量基本都是volatile。

方法分析

  • 構造函數:head和tail引用都指向空的頭節點,便於之後用頭插法等。
  • 核心思想:head不一定是真的head,tail也不一定是真的tail,但是可以通過head找到真的head,通過tail找到真的tail。入隊元素不能爲空,刪除元素師需要先設置元素節點Node的值爲null。理由和之前一樣,如果出隊操作返回null,不知道是隊內有元素爲null還是隊內無元素。那你就說了,可以判斷隊列的size是否爲空啊,但是和concurrenthashmap一樣,在併發情況下兩次操作之間誰有知道發生了什?
  • offer:通過cas將元素添加到隊尾,但是不一定移動tail,只有當tail距離最後一個元素大於等於HOPS(默認爲1)時纔會設置爲tail。offer中做兩件事,首先判斷真正的尾節點在哪,然後將入隊節點插入到真正的尾節點後面,如果tail距離真正的尾節點超過HOPS則更新尾節點。
    • 爲什麼要設計tail不是真正的尾節點,而要間隔HOPS個呢?因爲寫volatile的開銷比較大,而讀額開銷小,因此減少寫的次數(每次都要設置尾節點,自然寫的次數就增加了)。
  • poll。不是每次出隊都移動head,只有當head與真正的頭節點相距超過HOPS時才更新頭結點。怎麼判斷真正的頭節點在哪裏呢?出隊的節點並不會斷開node,而是將node的值域item變爲null,因此真正的頭節點向後遍歷到第一個不爲null的節點就是真正的頭節點。

阻塞隊列

爲什麼要有阻塞隊列?

  • 多個線程都想對隊列中值進行讀寫,即出隊入隊操作。若線程不安全,則會造成出隊入隊失敗,因此原有的offer和poll方法必須設計爲線程安全的。
  • 阻塞是爲了保證隊列在空和滿的情況下仍然保證併發安全,即隊列滿了就不向裏面加入元素了,隊列空了就不向裏面取元素了。
  • 相對於concurrentlinkedqueue,阻塞隊列的效率更低,因爲用了鎖,而concurrentlinkedqueue用的是cas和volatile。
  • 小結:所以同步是爲了多個線程對隊列進行出隊入隊操作的有序,阻塞是爲了防止滿和空後的錯誤操作。

阻塞隊列的重要方法

方法\處理方式 拋出異常 返回特殊值 一直阻塞 超時退出
插入方法 add(e) offer(e) put(e) offer(e,time,unit)
移除方法 remove() poll() take() poll(time,unit)
檢查方法 element() peek() 不可用 不可用
  • put和take是其他隊列中所沒有的方法,put是生產者將元素放入隊列,如果隊列已滿則阻塞,take是消費者從隊列中拿出元素,如果隊列已滿則阻塞。如果是無界隊列,即隊列沒有容量限制,那麼take和put一定不會阻塞。
  • add和remove會拋出異常,即如果隊列滿則拋出滿異常,空則空異常。這段代碼是AbstractQueue保證的,對offer進行了封裝。
  • offer和poll隊列滿或空則false,否則true,無界隊列的offer一定是true。還提供超時方法。

有哪些阻塞隊列?

  • ArrayBlockingQueue 數組有界隊列FIFO
    • 看了一下源碼,沒什麼大不了的,就是通過ReentrantLock和Condition內部實現了一個消費者生產者模式。具體來說是:offer和poll方法在進隊和出隊的時候都加顯式鎖從而實現併發隊列,put和take通過notFull和notEmpty兩個lock的Condition來await或者signal,從而實現消費者生產者模式。有界是通過構造函數指定的,沒有無參構造函數,規定了必須有界,這個界就是內部數組的大小,且不擴容。
    • 默認非公平鎖,先阻塞的線程不一定先操作隊列。
  • LinkedBlockingQueue 鏈表單向有界隊列FIFO
    • 數據結構Node爲單向鏈表節點,有界的界默認是int的最大值,這是和concurrentLinkedQueue的第一個區別
    • 計算size時,不需要遍歷整個隊列,直接返回atomInteger類型變量count值即可,而concurrentLinkedQueue則需要遍歷
    • count在每次入隊和出隊是都會利用AtomXXX類的CAS進行原子自增或自減少,如下代碼:
      • 那你就奇怪了,爲什麼明明已經加鎖了還要通過cas來改變數量呢?因爲LinkedBlockingQueue有putLock和takeLock兩把鎖,支持兩個線程一個增加一個減少,這我不僅就要發問,爲什麼呢?因爲一個是尾插法,一個是刪除頭節點,可以同時進行,爲什麼可以同時進行了,因爲在LinkedBlockingQueue中有head和last引用,在插入時不會改變head引用,在刪除時不會改變last引用。
  • PriorityBlockingQueue 支持優先級排序的無界隊列
    • 類似PriorityQueue,最小堆,因此元素是升序排列的。因爲是無界,因此put的時候永遠不會阻塞, 即不需要await。
    • 因爲優先級隊列會擴容,因此是無界的
  • DelayQueue 使用優先級隊列實現的無界隊列
    • 內部也是priorityqueue,因此無界。但是隊列中的元素必須實現delayed接口,此接口要實現兩個方法,一個是比較大小,這個建議設置爲讓延時時間最長的放在隊列末尾,不然隊列頭因爲延時時間沒到而阻塞,隊列後面的元素延時時間到了卻因爲在後面而出不來,另一個是getDelay返回當前元素還需要延遲多少時間才能獲取。
    • 有什麼用?1.緩存系統的設計,用delayqueue存儲緩存元素的有效期,一旦能從隊列中獲取元素則說明緩存過期。2.定時任務調度,一旦能從隊列中獲取到任務就立即執行。
    • 阻塞的策略,只要沒有達到延時時間就阻塞。
  • SynchronousQueue 不存儲元素的隊列
    • put操作在沒有take的時候會阻塞,直到有take纔會完成一次配對
    • 可以設置公平和非公平,公平情況下用隊列實現(隊列存儲線程),非公平情況下用棧實現。公平指的是線程1put,線程2put,線程3take此時線程1和線程3配對,就像隊列一樣,[1put,2put]隊列FIFO,遇到take後1put出隊。非公平是線程2和線程3配對,棧是[2put,1put],因爲棧是先進後出,所以take匹配2put。
    • 如果沒有消費者線程,offer直接返回false。
  • LinkedTransferQueue 鏈表組成的無界隊列
    • 不是通過鎖實現的
    • 多了transfer和tryTransfer方法。transfer的作用是,如果有線程請求take就將當前生產的資源直接給它而不經過容器,如果沒有線程請求take就放置到隊列末尾直到有線程take才返回。tryTransfer就是嘗試一次直接傳遞給消費者,不論結果都返回。

    transfer算法比較複雜,大致的理解是採用所謂雙重數據結構(dual data structures)。之所以叫雙重,其原因是方法都是通過兩個步驟完成:保留與完成。比如消費者線程從一個隊列中取元素,發現隊列爲空,他就生成一個空元素放入隊列,所謂空元素就是數據項字段爲空。然後消費者線程在這個字段上旅轉等待。這叫保留。直到一個生產者線程意欲向隊例中放入一個元素,這裏他發現最前面的元素的數據項字段爲NULL,他就直接把自已數據填充到這個元素中,即完成了元素的傳送。

    • 如果隊列中有元素,tansfer會等到隊列中元素消費完畢後才傳遞,所以如果只用transfer方法就是SynchronousQueue,因爲隊列中永遠不可能出現值。
      -只用put就是無界的LinkedBlockingQueue的功能,只用offer就是concurrentlinkedqueue的功能。
  • LinkedBlockingDeque 鏈表雙向隊列,有界
    • 實現方法類似於ArrayBlockingQueue而不是LinedBlockingQueue,是一把鎖兩個狀態。

Fork-join框架

基本概念

  • 將大任務分解成多個小任務,然後將每個小任務的結果彙總的框架。
  • 工作竊取,將某個線程從其他的隊列中竊取來執行。因爲是大任務分解的子任務們放到了不同的隊列,每個隊列都創建了一個單獨的線程來執行,A線程執行A隊列的任務,B線程執行B隊列的任務。但是如果A執行的快,B執行的慢,就會造成木桶效應,因此A會竊取B隊列中的任務完成。如下圖,展示的是雙端隊列,當線程1的任務做完後會竊取線程2的尾端事務做以防止線程間的競爭。

使用方法

  • 繼承RecursiveTask類用於返回結果的任務,繼承RescursiveAction表示不用返回結果的任務。ForkJoinPool類對象用submit方法調用前兩種任務,通過Future f獲得返回值,通過f的get方法獲得值。Task和Action都繼承與ForkJoinTask
  • 繼承後的類需要實現compute方法,該方法中需要遞歸的創建類,所以可以說ForkJoin的任務類都是遞歸類,在類的方法裏要新建任務。邏輯就是一個終止條件,如果滿足就返回,不然就將任務切分成小塊,交給不同的任務類對象.fork處理,然後將返回值join起來。

原子類與併發工具類

原子類

  • 基本類型 AtomicInteger、AtommicLong、AtomicBoolean。
    • 爲什麼沒String?因爲String保存的是常量,無法對String本身進行修改,因此也無所謂原子操作。
    • 其他的基本類型呢?先強轉成integer,再強轉回來,反正底層都是4字節存儲。
  • 數組類型 AtomicIntegerArray、AtommicLongArray、AtomicReferenceArray,傳入下標,期望當前值,設定值。
  • 引用類型 AtomicReference(原子的使得引用從一個對象改爲指向另一個對象),AtomicReferenceFieldUpdater(原子的更新引用裏的字段),AtomicMarkableReference(原子的更新<Object,Boolean>類型)
  • 更新字段 AtomicIntegerFieldUpdater,AtomicLongFieldUpdater,AtomicStampedReference.最後一個可以加時間戳以解決ABA問題,前面兩個構建的時候要指定類和字段

併發工具類

  • CountDownLatch,倒計時門栓,說明倒計時結束就開門:定一個倒計時,調用countDown方法時即倒數一次,countdownlatch的await方法阻塞當前線程直到倒數爲0。也就是說,倒計時結束即結束阻塞,有點類似join。
    • join:在A線程調用B.join,則A阻塞直到B完成。
    • countdownLatch:設置一個倒計時爲2的countdownlatch,在A線程中開啓B,C線程,在B線程結束處調用c.countdown,在C線程結束出調用c.countdown,在a線程開啓b,c後調用c.await,即當B,C線程運行完畢後,a線程纔會運行c.await後語句。
  • CyclicBarrier,循環柵欄:物理意義是,讓多個線程到達規定的設定點後才停止阻塞。比如CyclicBarrier cb,A,B,C線程,你希望有在A執行到a語句,B執行到b語句,C執行到c語句時,開啓D線程,那就在a,b,c語句後加cb.await。只有當執行到a.b.c語句時,纔會解除籬笆。
    • 不是我說,這和上面的countdownlatch沒區別啊?有!區別在後者是可循環利用的,可以用reset方法重置,但是countdownlatch只能用一次。
  • Semaphore,信號量,用於流量控制。本質上就是共享鎖的應用,指定最多多少個線程可以共享,但是不是用lock和unlock,直接用的acquire和release。那麼直接的麼,直接用AQS的方法,人家鎖都還包裝了一層呢。
  • Exchanger,交換着,用於線程間寫作的工具類。比如A線程錄入銀行流水,錄入完後調用e.exchange(T t1),此時A線程阻塞,B線程同時也在錄入同樣的銀行流水,流入完後調用e.exchange(T t2),此時A線程停止阻塞,兩個線程交換t1,t2.

線程池

爲什麼要用線程池?

  • 降低資源消耗。通過重複利用已創建的線程降低線程創建和銷燬造成的消耗。
  • 提高響應速度。任務到達時不需要再創建線程,而是之間利用已有的線程。
  • 提高線程的可管理性。使用線程池可以對線程進行統一的分配,調優和監控。

實現原理

https://juejin.im/entry/58fada5d570c350058d3aaad

  • 當新的任務出現時,線程池會如何處理呢?1.嘗試從核心池中分配工作線程。2.核心池沒有線程了,就加入等待隊列。3.等待隊列滿了,就嘗試從最大線程池中創建線程。4.最大線程池都滿了,就交給飽和策略(rejectedExecutionHandler)。
  • https://juejin.im/post/5c33400c6fb9a049fe35503b#heading-1

  • AtomicInteger ctl,高3位保存線程池狀態,低29位保存當前線程數量,線程池狀態有如下狀態
    • running,接收新任務,處理隊列任務
    • shutdown,不接受行任務,但處理隊列任務
    • stop,不接受行任務,也不處理隊列任務,中斷所有處理中的任務
    • tidying,所有任務都被終結,有效線程爲0,調用terminated方法
    • terminated,當terminated方法結束後
  • worker exttends AQS impelements Runnable。worker線程封裝了我們交給線程池處理的任務,因此worker可以複用的處理多個任務。
    • 內部實現了一個不可重入的互斥鎖,這個鎖是用來控制是否可以中斷的。lock方法獲取獨佔鎖表示當前線程正在執行,如果正在執行則不應該中斷,如果空閒則可以中斷。這樣的目的是爲了線程池使用shutdown方法或tryTerminate方式時可以判斷線程池中的線程是否空閒從而進行關閉。
    • 構造方法是通過ThreadFactory(線程池的構造函數中傳入)構建一個線程(這個線程是執行任務的線程,即複用線程),將成員變量thread設置爲this,即worker線程啓動後率先調用自己的run方法,然後在自己的run方法中調用ThreadPoolExecutor.runworker方法從而實現任務。要實現的任務通過firstTask保存。
    • runworker(this),執行w.firstTask,方法中留給子類兩個方式可以實現,beforeExecute和afterExecute
  • 構造函數參數須知 ThreadPoolExecutor(…)
    • corePoolSize,核心線程池數量。
    • maximumPoolSize,最大線程池數量
    • keepAliveTime,線程空閒時的存活時間,即當線程沒有任務執行時繼續存活的時間
    • unit,上面這個參數的單位,是一個TimeUnit枚舉類的常量
    • workQueue,保存等待執行的任務的阻塞隊列
      • SynchronousQueue,直接切換,不存在任務隊列
      • 有界隊列:LinkedBQ,ArrayBQ,LinkedBD
      • 無界隊列:PriorityBQ,DelayBQ,LinkedTransferQ
    • threadFactory,用來創建新的線程,默認使用Executors.defaultThreadFactory()創建,規定線程優先級,是否爲守護線程,線程名稱。
    • RejectedExecutionHandler,表示線程的飽和策略。
      • AbortPolicy:直接拋出異常、默認的
      • CallerRunsPolicy:用調用者所在的線程執行任務
      • DiscardOldestPoliscy:丟棄阻塞隊列中最靠前的任務並執行當前任務
      • DiscardPolicy:直接丟棄任務
      • 以上四個都是內部類,定義了四種不同的策略。
  • execute方法:
    • addworker(command,boolean),通過第二個參數決定是核心還是非核心。
      • 新建一個worker線程,傳入當前任務。 每次新建任務都要使用mainLock。
        • mainlock,即全局鎖,這是一個可重入鎖,作用是保證線程創建的穩定性,所謂穩定性是指線程池的中斷操作會導致創建線程的不穩定。
        • mainlock有一個condition叫termination,用於支持終止操作。
      • 真是因爲mainLock的存在,所以線程池的設計思路是,儘量減少mianlock的加鎖過程,因此纔有了核心池和最大池的概念。當核心池打滿後,一個合理的阻塞隊列+核心池基本可以滿足功能,如果需要頻繁addworker,說明線程池需要調整。
    • worker線程在工作的時候,如果當前任務做完了,就會從阻塞隊列中take任務做。
  • processWorkerExit方法
    • runworker運行結束後,即當前任務運行完或者getTask()null時,調用processWorkerExit。什麼時候null呢?1.線程池狀態爲STOP,且阻塞隊列爲空。2.線程池allowCoreThreadTimeOut設置爲true且當前只有一個線程,並且該線程停止運行。並不是說當前只有一個線程且該線程停止運行後,getTask就是null。線程池中有一個timed成員變量默認爲false,即設置keepalive變量僅僅決定了從隊列中拿線程的最大時間,超出前用poll拿,超出後用take拿。
      • boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;所以當core爲0的時候,timed也幾乎是true的,即用完就關閉
    • 移除的時候也要用mainlock,因爲只要涉及到線程的增加和刪除,都要用mainlock,所以用keepalivetime可以有效降低mainlock次數,但是也不易設計太大,白白佔用系統資源。
    • 用一個HashSet workers,保存了全部的工作線程,從這裏面移除。
    • 調用tryTerminate(),此方法會根據線程池狀態判斷是否結束線程池。
  • 一個工作線程的生命週期
    • execute方法判斷是否addworker,創建worker實例,調用worker線程的run方法,run方法中調用runworker運行firstTask或者從隊列中take來的。如果超過一段時間都不來任務就結束線程。結束線程會觸發一次檢查是否需要關閉線程池的tryTerminate方法。
  • 關閉線程池
    • shutdown方法,將線程池狀態切換到shutdown狀態(準備關閉狀態,不接客了,但是當前的客人得處理完),調用interruputIdeWorkers嘗試中斷所有空閒worker,最後調用tryTerminate嘗試關閉線程池。
      • interruputIdeWorkers方法會遍歷workers中的每一個線程,如果是空閒的就調用interrupt方法。遍歷關閉之前要獲取mainlock鎖,因爲workers是hashset的,這是非線程安全的。
    • shutdownNow,設置黃臺爲STOP(不僅不接客,還要把現在的客人趕走),中斷所有工作線程無論是否空閒,取出阻塞隊列中還未執行的任務。反正就是盡一切手段讓線程池隊列沒東西,線程全中斷以進入tidying狀態,最後進入tryTerminate方法。
    • 實測未關閉線程池會導致main函數退出後程序不完全退出,因爲默認的timed都爲false
  • 線程池的監控
    • getTaskCount 獲得線程池中已經執行的和未執行的任務總數
    • getCompeletedTaskCount 獲取已完成的
    • getLargestPoolSize,獲取曾經創建過的最大線程數量
    • getPoolSize 線程池當前線程數量
    • getActiveCount,線程中正在執行任務的線程數量

如何合理的使用線程池

  • CPU密集型還是IO密集型還是混合
    • CPU密集型說明每個線程計算量大,儘量安排線程少,線程數=CPU數量+1
    • IO密集型說明CPU處理負擔小且線程常常阻塞等待,進行線程多,線程數=CPU*2
  • 任務是否具有優先級
    • 有優先級則考慮阻塞隊列採用priorityBQ
  • 任務執行時間
    • 根據不同時間可以交給不同的線程池處理,靈活配置keepalive時間
  • 任務的依賴性,比如數據庫
    • 因爲線程提交sql要經過網絡再到數據庫服務器,等數據庫處理完再返回CPU,因此線程常常阻塞等待,可以考慮採用多個線程
  • 總結:計算量大線程池小,計算量小且線程容易阻塞線程池大
  • 儘量使用有界隊列,以防止內存被佔滿。

Executor框架

爲什麼要有這框架?

  • 核心點是,我們啓動線程的方法是new Thread(new Runnable).start,那麼Thread即是線程的執行者,又是線程本體。因此嘗試將線程本體和線程的執行者分開,邏輯上分成任務和執行器兩個概念。任務就是一個Task,可以是實現Runnable或者實現callbale的,執行器就是Executor。
  • 分離的好處是Task重視任務的邏輯實現,Executor重視控制線程的啓動執行和關閉,讓線程更易管理,採用線程池讓效率更高,開銷更小。

Executor框架中有哪些東西?

  • Executor是一切執行器的本源,實際上用的是ThreadPoolExecutor類和ScheduledThreadExecutor類,此接口只規定了excute方法,ExecutorServicre接口定義了submit方法,shutdown等關閉方法
    • ThreadPoolExecutor就是上一章節線程池的實現類,我們可以定製也可以採用其子類,將在下一小節介紹。
    • ScheduleThreadPoolExecutor可以再給定的延遲後執行命令或者定期執行命令。類似於定時器任務,但是更加靈活。
  • Runnable和Callable是所有任務的本源,callable的任務可以返回結果和拋出異常。
  • Future接口的FutureTask實現類用於接收callable類任務的返回值,get方法接收返回值,接收到之前會阻塞。
    • submit(callable c)中會將c包裝成FutureTask impelements Runnable,Future,因爲實現了Runnable方法,所以submit方法會調用execute方法運行FutureTask。
    • 當調用Future.get方法時會判斷此時futureTask類的狀態,若還沒有運行完畢則阻塞。
    • 1.8中已經不再使用基於AQS的sync了,而是內部自己維護一個static final waitnode單鏈表,每個節持有一個線程,和AQS類似,頭結點是運行線程,運行後會喚醒下一個。因此不同的FutureTask實例實際上是通過類變量這個鏈表實現的同步。
  • Executors工廠類,可以創建SingleThreadExector,Cache線程池等。
    • newSingleThreadExecutor,採用LinkedBQ,max和core都爲1
    • newCachedThreadPool,採用SynchronousQ,核心爲0,最大爲Int最大值。因爲採用同步隊列,本質上隊列就是傳送作用。因爲core爲0,keepalive時間爲60,即線程超過60秒沒有任務就自動取消,getTask爲null。
    • newFixedThreadPool,採用LinkedBQ,max和core數量相同
    • newWorkStealingPool,採用ForkJoinPool
    • newScheduledThreadPool,採用DelayWorkQueue
    • newSingleThreadScheduledExecutor
      • 優先級隊列保存內部任務的開始時間,執行後更新開始時間再重新入delayqueue
  • 如何使用
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章