Java 多線程核心技術梳理(附源碼)

本文對多線程基礎知識進行梳理,主要包括多線程的基本使用,對象及變量的併發訪問,線程間通訊,lock的使用,定時器,單例模式,以及線程狀態與線程組。html

寫在前面

花了一週時間閱讀《java多線程編程核心技術》(高洪巖 著),本文算是此書的整理概括,書中幾乎全部示例,我都親手敲了一遍,並上傳到了個人github上,有興趣的朋友能夠到個人github下載。源碼採用maven構建,多線程這部分源碼位於java-multithread模塊中。java

  • 倉庫地址:java-learninggit

  • git clone: git@github.com:brianway/java-learning.gitgithub

java多線程

基礎知識編程

  • 建立線程的兩種方式:1.繼承Thread類,2.實現Runnable接口。具體二者的聯繫能夠參考我以前的博文《java基礎鞏固筆記(5)-多線程之傳統多線程》api

  • 一些基本API:isAlive(),sleep(),getId(),yield()等。安全

    • isAlive()測試線程是否處於活動狀態多線程

    • sleep()讓「正在執行的線程」休眠併發

    • getId()取得線程惟一標識oracle

    • yield()放棄當前的CPU資源

  • 棄用的API:stop(),suspend(),resume()等,已經棄用了,由於可能產生數據不一樣步等問題。

  • 中止線程的幾種方式:

    • 使用退出標識,使線程正常退出,即run方法完成。

    • 使用interrupt方法中斷線程

  • 線程的優先級:繼承性,規則性,隨機性

    • 線程的優先級具備繼承性. 如,線程A啓動線程B,則B和A優先級同樣

    • 線程的優先級具備規則性. CPU儘可能傾向於把資源優先級高的線程

    • 線程的優先級具備隨機性. 優先級不等同於執行順序,兩者關係不肯定

  • java中的兩種線程:用戶線程和守護(Daemon)線程。

    • 守護線程:進程中不存在非守護線程時,守護線程自動銷燬。典型例子如:垃圾回收線程。

比較和辨析

  • 某個線程與當前線程:當前線程則是指正在運行的那個線程,可由currentThread()方法返回值肯定。例如,直接在main方法裏調用run方法,和調用線程的start方法,打印出的當前線程結果是不一樣的。

  • interrupted()isInterrupted()

    • interrupted()是類的靜態方法,測試當前線程是否已是中斷狀態,執行後具備將狀態標誌清除爲false的功能。

    • isInterrupted()是類的實例方法,測試Thread對象是否已是中斷狀態,但不清楚狀態標誌。

  • sleep()wait()區別:

    • sleep()是Thread類的static(靜態)的方法;wait()方法是Object類裏的方法

    • sleep()睡眠時,保持對象鎖,仍然佔有該鎖;wait()睡眠時,釋放對象鎖

    • 在sleep()休眠時間期滿後,該線程不必定會當即執行,這是由於其它線程可能正在運行並且沒有被調度爲放棄執行,除非此線程具備更高的優先級;wait()使用notify或者notifyAlll或者指定睡眠時間來喚醒當前等待池中的線程

    • wait()必須放在synchronized block中,不然會在runtime時扔出java.lang.IllegalMonitorStateException異常

方法 是否釋放鎖 備註
wait wait和notify/notifyAll是成對出現的, 必須在synchronize塊中被調用
sleep 可以使低優先級的線程得到執行機會
yield yield方法使當前線程讓出CPU佔有權, 但讓出的時間是不可設定的

對象及變量的併發訪問

  • synchronized關鍵字

    • 調用用關鍵字synchronized聲明的方法是排隊運行的。但假如線程A持有某對象的鎖,那線程B異步調用非synchronized類型的方法不受限制。

    • synchronized鎖重入:一個線程獲得對象鎖後,再次請求此對象鎖時是能夠獲得該對象的鎖的。同時,子類可經過「可重入鎖」調用父類的同步方法。

    • 同步不具備繼承性。

    • synchronized使用的「對象監視器」是一個,即必須是同一個對象

  • synchronized同步方法和synchronized同步代碼塊。

    • 對其餘synchronized同步方法或代碼塊調用呈阻塞狀態。

    • 同一時間只有一個線程可執行synchronized方法/代碼塊中的代碼

  • synchronized(非this對象x),將x對象做爲「對象監視器」

    • 當多個線程同時執行synchronized(x){}同步代碼塊時呈同步效果

    • 當其餘線程執行x對象中synchronizd同步方法時呈同步效果

    • 當其餘線程執行x對象方法裏的synchronized(this)代碼塊時呈同步效果

  • 靜態同步synchronized方法與synchronized(class)代碼塊:對當前對應的class類進行持鎖。

    線程的私有堆棧圖

javaSE_多線程-線程的私有堆棧

  • volatile關鍵字:主要做用是使變量在多個線程間可見。加volatile關鍵字可強制性從公共堆棧進行取值,而不是從線程私有數據棧中取得變量的值

    • 在方法中while循環中設置狀態位(不加volatile關鍵字),在外面把狀態位置位並不可行,循環不會中止,好比JVM在-server模式。

    • 緣由:是私有堆棧中的值和公共堆棧中的值不一樣步

    • volatile增長了實例變量在多個線程間的可見性,但不支持原子性

  • 原子類:一個原子類型就是一個原子操做可用的類型,可在沒有鎖的狀況下作到線程安全。但原子類也不是徹底安全,雖然原子操做是安全的,可方法間的調用卻不是原子的,須要用同步。

讀取公共內存圖

javaSE_多線程-讀取公共內存.png

辨析和零散補充

  • synchronized靜態方法與非靜態方法:synchronized關鍵字加static靜態方法上是給Class類上鎖,能夠對類的全部實例對象起做用;synchronized關鍵字加到非static靜態方法上是給對象上鎖,對該對象起做用。這兩個鎖不是同一個鎖。

  • synchronized和volatile比較

    • 1)關鍵字volatile是線程同步的輕量級實現,性能比synchronized好,且volatile只能修飾變量,synchronized可修飾方法和代碼塊。

    • 2)多線程訪問volatile不會發生阻塞,synchronized會出現阻塞

    • 3)volatile能保證數據可見性,不保證原子性;synchronized能夠保證原子性,也能夠間接保證可見性,由於synchronized會將私有內存和公共內存中的數據作同步

    • 4)volatile解決的是變量在多個線程間的可見性,synchronized解決的是多個線程訪問資源的同步性。

  • String常量池特性,故大多數狀況下,synchronized代碼塊都不適用String做爲鎖對象。

  • 多線程死鎖。使用JDK自帶工具,jps命令+jstack命令監測是否有死鎖。

  • 內置類與靜態內置類。

  • 鎖對象的的改變。

  • 一個線程出現異常時,其所持有的鎖會自動釋放。

變量在內存中的工做過程圖

javaSE_多線程-變量在內存中的工做過程.png

線程間通訊

  • 等待/通知機制:wait()notify()/notifyAll()。wait使線程中止運行,notify使中止的線程繼續運行。

    • wait():將當前執行代碼的線程進行等待,置入"預執行隊列"。

      • 在調用wait()以前,線程必須得到該對象的對象級別鎖;

      • 執行wait()方法後,當前線程當即釋放鎖;

      • 從wait()返回前,線程與其餘線程競爭從新得到鎖

      • 當線程呈wait()狀態時,調用線程的interrup()方法會出現InterrupedException異常

      • wait(long)是等待某一時間內是否有線程對鎖進行喚醒,超時則自動喚醒。

    • notify():通知可能等待該對象的對象鎖的其餘線程。隨機挑選一個呈wait狀態的線程,使它等待獲取該對象的對象鎖。

      • 在調用notify()以前,線程必須得到該對象的對象級別鎖;

      • 執行完notify()方法後,不會立刻釋放鎖,要直到退出synchronized代碼塊,當前線程纔會釋放鎖。

      • notify()一次只隨機通知一個線程進行喚醒

    • notifyAll()notify()差很少,只不過是使全部正在等待隊中等待同一共享資源的「所有」線程從等待狀態退出,進入可運行狀態。

  • 每一個鎖對象有兩個隊列:就緒隊列和阻塞隊列。

    • 就緒隊列:存儲將要得到鎖的線程

    • 阻塞隊列:存儲被阻塞的的線程

  • 生產者/消費者模式

    • 「假死」:線程進入WAITING等待狀態,呈假死狀態的進程中全部線程都呈WAITING狀態。

      • 假死的主要緣由:有可能連續喚醒同類。notify喚醒的不必定是異類,也許是同類,如「生產者」喚醒「生產者」。

      • 解決假死:將notify()改成notifyAll()

    • wait條件改變,可能出現異常,須要將if改爲while

  • 經過管道進行線程間通訊:一個線程發送數據到輸出管道,另外一個線程從輸入管道讀數據。

    • 字節流:PipedInputStreamPipedOutputStream

    • 字符流:PipedReaderPipedWriter

  • join():等待線程對象銷燬,具備使線程排隊運行的做用。

    • join()與interrupt()方法彼此遇到會出現異常。

    • join(long)可設定等待的時間

  • joinsynchronized的區別:join在內部使用wait()方法進行等待;synchronized使用的是「對象監視器」原理做爲同步

  • join(long)sleep(long)的區別:join(long)內部使用wait(long)實現,因此join(long)具備釋放鎖的特色;Thread.sleep(long)不釋放鎖。

  • ThreadLocal類:每一個線程綁定本身的值

    • 覆寫該類的initialValue()方法可使變量初始化,從而解決get()返回null的問題

    • InheritableThreadLocal類可在子線程中取得父線程繼承下來的值。

Lock的使用

  • ReentrantLock類:實現線程之間的同步互斥,比synchronized更靈活

    • lock(),調用了的線程就持有了「對象監視器」,效果和synchronized同樣

  • 使用Condition實現等待/通知:比wait()和notify()/notyfyAll()更靈活,好比可實現多路通知。

    • 調用condition.await()前須先調用lock.lock()得到同步監視器

Object與Condition方法對比

Object Condition
wait() await()
wait(long timeout) await(long time,TimeUnit unit)
notify() signal()
notifyAll() signalAll()

一些API

方法 說明
int getHoldCount() 查詢當前線程保持此鎖定的個數,即調用lock()方法的次數
int getQueueLength() 返回正在等待獲取此鎖定的線程估計數
int getWaitQueueLength(Condition condition) 返回等待與此鎖定相關的給定條件Conditon的線程估計數
boolean hasQueueThread(Thread thread) 查詢指定的線程是否正在等待獲取此鎖定
boolean hasQueueThreads() 查詢是否有線程正在等待獲取此鎖定
boolean hasWaiters(Condition) 查詢是否有線程正在等待與此鎖定有關的condition條件
boolean isFair() 判斷是否是公平鎖
boolean isHeldByCurrentThread() 查詢當前線程是否保持此鎖定
boolean isLocked() 查詢此鎖定是否由任意線程保持
void lockInterruptibly() 若是當前線程未被中斷,則獲取鎖定,若是已經被中斷則出現異常
boolean tryLock() 僅在調用時鎖定未被另外一個線程保持的狀況下,才獲取該鎖定
boolean tryLock(long timeout,TimeUnit unit) 若是鎖定在給定等待時間內沒有被另外一個線程保持,且當前線程未被中斷,則獲取該鎖定
  • 公平鎖與非公平鎖

    • 公平鎖表示線程獲取鎖的順序是按照加鎖的順序來分配的,即FIFO先進先出。

    • 非公平鎖是一種獲取鎖的搶佔機制,隨機得到鎖。

  • ReentrantReadWriteLock

    • 讀讀共享

    • 寫寫互斥

    • 讀寫互斥

    • 寫讀互斥

定時器

經常使用API

方法 說明
schedule(TimerTask task, Date time) 在指定的日期執行某一次任務
scheduleAtFixedRate(TimerTask task, Date firstTime, long period) 在指定的日期以後按指定的間隔週期,無限循環的執行某一任務
schedule(TimerTask task, long delay) 以執行此方法的當前時間爲參考時間,在此時間基礎上延遲指定的毫秒數後執行一次TimerTask任務
schedule(TimerTask task, long delay, long period) 以執行此方法的當前時間爲參考時間,在此時間基礎上延遲指定的毫秒數,再以某一間隔時間無限次數地執行某一TimerTask任務
  • schedulescheduleAtFixedRate的區別:schedule不具備追趕執行性;scheduleAtFixedRate具備追趕執行性

單例模式與多線程

  • 當即加載/「餓漢模式」:調用方法前,實例已經被建立了。經過靜態屬性new實例化實現的

  • 延遲加載/「懶漢模式」:調用get()方法時實例才被建立。最多見的實現辦法是在get()方法中進行new實例化

    • 缺點:多線程環境中,會出問題

    • 解決方法

      • 聲明synchronized關鍵字,但運行效率很是低下

      • 同步代碼塊,效率也低

      • 針對某些重要代碼(實例化語句)單獨同步,效率提高,但會出問題

      • 使用DCL雙檢查鎖

      • 使用enum枚舉數據類型實現單例模式

拾遺補增

方法與狀態關係示意圖

javaSE_多線程-方法與狀態關係示意圖.png

  • 線程的狀態:Thread.State枚舉類,參考官網APIEnum Thread.State

  • 線程組:線程組中能夠有線程對象,也能夠有線程組,組中還能夠有線程。可批量管理線程或線程組對象。

  • SimpleDateFormat非線程安全,解決辦法有:

    • 建立多個SimpleDateFormat類的實例

    • 使用ThreadLocal類

  • 線程組出現異常的處理

    • setUncaughtExceptionHandler()給指定線程對象設置異常處理器

    • setDefaultUncaughtExceptionHandler()對全部線程對象設置異常處理器

參考資料


做者@brianway更多文章:我的網站 | CSDN | oschina

相關文章
相關標籤/搜索