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

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

標籤: java



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

寫在前面

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

java多線程

基礎知識

  • 創建線程的兩種方式:1.繼承Thread類,2.實現Runnable接口。具體兩者的聯繫可以參考我之前的博文《java基礎鞏固筆記(5)-多線程之傳統多線程》
  • 一些基本API:isAlive(),sleep(),getId(),yield()等。
    • isAlive()測試線程是否處於活動狀態
    • sleep()讓“正在執行的線程”休眠
    • getId()取得線程唯一標識
    • 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

發佈了96 篇原創文章 · 獲贊 99 · 訪問量 41萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章