Java 多線程編程基礎——Thread 類

線程

咱們在閱讀程序時,表面看來是在跟蹤程序的處理流程,實際上跟蹤的是線程的執行。

單線程程序java

在單線程程序中,在某個時間點執行的處理只有一個。數據庫

Java 程序執行時,至少會有一個線程在運行,這個運行的線程被稱爲主線程(Main Thread)。編程

Java 程序在主線程運行的同時,後臺線程也在運行,例如:垃圾回收線程、GUI 相關線程等。緩存

Java 程序的終止是指除守護線程(Daemon Thread)之外的線程所有終止。守護線程是執行後臺做業的線程,例如垃圾回收線程。咱們能夠經過 setDaemon() 方法把線程設置爲守護線程。服務器

多線程程序網絡

由多個線程組成的程序稱爲多線程程序(Multithreaded Program)。多個線程運行時,各個線程的運行軌跡將會交織在一塊兒,同一時間點執行的處理有多個。多線程

多線程應用場景:併發

  • GUI 應用程序:存在專門執行 GUI 操做的線程(UI Thread)
  • 耗時任務:文件與網絡的 I/O 處理
  • 網絡服務器同時處理多個客戶端請求場景

P.S. 使用 java.nio 包中的類,有時即使不使用線程,也能夠執行兼具性能和可擴展性的 I/O 處理。異步

並行(parallel)與併發(concurrent)的區別ide

程序運行存在順序、並行與併發模式。

  • 順序(sequential)用於表示多個操做依次處理。
  • 並行用於表示多個操做同時處理,取決於 CPU 的個數。
  • 併發用於表示將一個操做分割成多個部分而且容許無序處理。

併發相對於順序和並行來講比較抽象。單個 CPU 併發處理即爲順序執行,多個 CPU 併發處理能夠並行執行。

若是是單個 CPU,即使多個線程同時運行,併發處理也只能順序執行,在線程之間不斷切換。

併發處理包括:併發處理的順序執行、併發處理的並行執行。

併發處理

線程和進程的區別

  • 線程之間共享內存
    進程和線程之間最大的區別就是內存是否共享。
    一般,每一個進程都擁有彼此獨立的內存空間。一個進程不能夠擅自讀取、寫入其餘進程的內存。正由於每一個進程內存空間獨立,無需擔憂被其餘進程破壞。
    線程之間共享內存,使得線程之間的通訊實現起來更加天然、簡單。一個線程向實例中寫入內容,其餘線程就能夠讀取該實例的內容。當有多個線程能夠訪問同一個實例時,須要正確執行互斥處理。
  • 線程的上下文切換快
    進程和線程之間的另外一個區別就是上下文切換的繁重程度。
    當運行中的進程進行切換時,進程要暫時保存自身的當前狀態(上下文信息)。而接着開始運行的進程須要恢復以前保存的自身的上下文信息。
    當運行中的線程進行切換時,與進程同樣,也會進行上下文切換。但因爲線程管理的上下文信息比進程少,因此通常來講,線程的上下文切換要比進程快。

當執行緊密關聯的多項工做時,一般線程比進程更加適合。

多線程程序的優勢和成本

優勢:

  • 充分利用硬件資源如多核 CPU、I/O 設備、網絡設備並行工做。
  • 提升 GUI 應用程序響應性,UI Thread 專一界面繪製、用戶交互,額外開啓線程執行後臺任務。
  • 網絡應用程序簡化建模,每一個客戶端請求使用單獨的線程進行處理。

缺點(成本):

  • 建立線程須要消耗系統資源和時間,準備線程私有的程序計數器和棧。
  • 線程調度和切換一樣須要成本,線程切換出去時須要保存上下文狀態信息,以便再次切換回來時可以恢復以前的上下文狀態。

相對而言,如果存在耗時任務須要放入子線程中實際執行,線程使用成本能夠不計。

多線程編程的重要性

硬件條件知足多線程並行執行的條件以外,還須要程序邏輯可以保證多線程正確地運行,考慮到線程之間的互斥處理和同步處理。

Thread 類

線程的建立與啓動

建立與啓動線程的兩種方法:

  1. 利用 Thread 類的子類實例化,建立並啓動線程。
  2. 利用 Runnable 接口的實現類實例化,建立並啓動線程。

線程的建立與啓動步驟——方法一:

  • 聲明 Thread 的子類(extends Thread),並重寫 run() 方法。
  • 建立該類的實例
  • 調用該實例的 start() 方法啓動線程

Thread 實例和線程自己不是同一個東西,建立 Thread 實例,線程並未啓動,直到 start() 方法調用,一樣就算線程終止了,實例也不會消失。可是一個 Thread 實例只能建立一個線程,一旦調用 start() 方法,無論線程是否正常/異常結束,都沒法再次經過調用 start() 方法建立新的線程。而且重複調用 start() 方法會拋出 IllegalThreadStateException 異常。

Thread run( ) 方法 和 start() 方法:

  • run() 方法是能夠重複調用的,可是不會啓動新的線程,於當前線程中執行。run() 方法放置於 Runnable 接口旨在封裝操做。
  • start() 方法主要執行如下操做:啓動新的線程,並在其中調用 run() 方法。

線程的建立與啓動步驟——方法二:

  • 聲明類並實現 Runnable 接口(implements Runnable),要求必須實現 run() 方法。
  • 建立該類的實例
  • 以該實例做爲參數建立 Thread 類的實例 Thread(Runnable target)
  • 調用 Thread 類的實例的 start() 方法啓動線程

無論是利用 Thread 類的子類實例化的方法(1),仍是利用 Runnable 接口的實現類實例化的方法(2),啓動新線程的方法最終都是 Thread 類的 start() 方法。

Java 中存在單繼承限制,若是類已經有一個父類,則不能再繼承 Thread 類,這時能夠經過實現 Runnable 接口來實現建立並啓動新線程。

Thread 類自己實現了 Runnable 接口,並將 run() 方法的重寫(override)交由子類來完成。

線程的屬性

id 和 name

經過 Thread(String name) 構造方法或 void setName(String name),給 Thread 設置一個友好的名字,能夠方便調試。

優先級

Java 語言中,線程的優先級從1到10,默認爲5。但因程序實際運行的操做系統不一樣,優先級會被映射到操做系統中的取值,所以 Java 語言中的優先級主要是一種建議,多線程編程時不要過於依賴優先級。

線程的狀態

Thread.State 枚舉類型(Enum)包括:

  • NEW
    線程實例化後,還沒有調用 start() 方法啓動。
  • RUNNABLE
    可運行狀態,正在運行或準備運行。
  • BLOCKED
    阻塞狀態,等待其餘線程釋放實例的鎖。
  • WAITING
    等待狀態,無限等待其餘線程執行特定操做。
  • TIMED_WAITING
    時限等待狀態,等待其餘線程執行指定的有限時間的操做。
  • TERMINATED
    線程運行結束

線程的方法

currentThread() 方法

Thread 類的靜態方法 currentThread() 返回當前正在執行的線程對象。

sleep() 方法

Thread 類的靜態方法 sleep() 可以暫停(休眠)當前線程(執行該語句的線程)運行,放棄佔用 CPU。線程休眠期間能夠被中斷,中斷將會拋出 InterruptedException 異常。sleep() 方法的參數以毫秒做爲單位,不過一般狀況下,JVM 沒法精確控制時間。

sleep() 方法調用須要放在 try catch 語句中,可能拋出 InterruptedException 異常。InterruptedException 異常可以取消線程處理,可使用 interrupt() 方法在中途喚醒休眠狀態的線程。

多線程示例程序中常用 sleep() 方法模擬耗時任務處理過程。

yield() 方法

Thread 類的靜態方法 yield() 可以暫停當前線程(執行該語句的線程)運行,讓出 CPU 給其餘線程優先執行。若是沒有正在等待的線程,或是線程的優先級不高,當前線程可能繼續運行,即 yield() 方法沒法確保暫停當前線程。yield() 方法相似 sleep() 方法,可是不能指定暫停時間。

join() 方法

Thread 類的實例方法,持有 Thread 實例的線程,將會等待調用 join() 方法的 Thread 實例表明的線程結束。等待期間能夠被中斷,中斷將會拋出 InterruptedException 異常。

示例程序:

public class HelloThread extends Thread {
    @Override
    public void run() {
        System.out.println("hello");
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new HelloThread();
        thread.start();
        thread.join();
    }
}

main() 方法所在的主線程將會等待 HelloThread 子線程執行 run() 方法結束後再執行,退出程序。

併發編程特性

  • 原子性
  • 可見性
  • 有序性

原子性操做問題

原子性概念來源於數據庫系統,一個事務(Transaction)中的全部操做,要麼所有完成,要麼所有不完成,不會結束在中間某個環節。事務在執行過程當中發生錯誤,會被恢復(Rollback)到事務開始前的狀態,就像這個事務歷來沒有執行過同樣。

併發編程的原子性指對於共享變量的操做是不可分的,Java 基本類型除 long、double 外的賦值操做是原子操做。

非原子操做例如:

counter++;
  1. 讀取 counter 的當前值
  2. 在當前值基礎上加1
  3. 將新值從新賦值給 counter

Java 語言的解決方式:

  • 使用 synchronized 關鍵字
  • 使用 java.util.concurrent.atomic

內存可見性問題

計算機結構中,CPU 負責執行指令,內存負責讀寫數據。CPU 執行速度遠超內存讀寫速度,緩解二者速度不一致引入了高速緩存。 預先拷貝內存數據的副本到緩存中,便於 CPU 直接快速使用。

所以計算機中除內存以外,數據還有可能保存在 CPU 寄存器和各級緩存當中。這樣一來,當訪問一個變量時,可能優先從緩存中獲取,而非內存;當修改一個變量時,可能先將修改寫到緩存中,稍後纔會同步更新到內存中。

對於單線程程序來講沒有太大問題,可是多線程程序並行執行時,內存中的數據將會不一致,最新修改可能還沒有同步到內存中。須要提供一種機制保證多線程對應的多核 CPU 緩存中的共享變量的副本彼此一致——緩存一致性協議。

Java 語言的解決方式:

  • 使用 volatile 關鍵字
  • 使用 synchronized 關鍵字

若是隻是解決內存可見性問題,使用 synchronized 關鍵字成本較高,考慮使用 volatile 關鍵字更輕量級的方式。

指令重排序問題

有序性:即程序執行的順序嚴格按照代碼的前後順序執行。

Java 容許編譯器和處理器爲了提升效率對指令進行重排序,重排序過程不會影響到單線程程序的執行,卻會可能影響到多線程程序併發執行時候的正確性。

volatile 關鍵字細節

Java 使用 volatile 關鍵字修飾變量,保證可見性、有序性。

  • 保證變量的值一旦被修改後當即更新寫入內存,同時默認從內存讀取變量的值。(可見性)
  • 禁止指令重排序(有序性)

可是 volatile 關鍵字沒法保證對變量操做是原子性的。

線程的互斥處理(synchronized 關鍵字細節)

每一個線程擁有獨立的程序計數器(指令執行行號)、棧(方法參數、局部變量等信息),多個線程共享堆(對象),這些區域對應 JVM 內存模型。當多個線程操做堆區的對象時候,可能出現多線程共享內存的問題。

競態條件

銀行取款問題
if(可用餘額大於等於取款金額) {

可用餘額減去取款金額

}
多個線程同時操做時,餘額確認(可用餘額大於等於取款金額)和取款(可用餘額減去取款金額)兩個操做可能穿插執行,沒法保證線程之間執行順序。

線程 A 線程 B
可用餘額(1000)大於等於取款金額(1000)?是的 切換執行線程 B
線程 A 處於等待狀態 可用餘額(1000)大於等於取款金額(1000)?是的
線程 A 處於等待狀態 可用餘額減去取款金額(1000-1000 = 0)
切換執行線程 A 線程 B 結束
可用餘額減去取款金額(0 - 1000 = -1000) 線程 B 結束

當有多個線程同時操做同一個對象時,可能出現競態條件(race condition),沒法預期最終執行結果,與執行操做的時序有關,須要「交通管制」——線程的互斥處理。

Java 使用 synchronized 關鍵字執行線程的互斥處理。synchronized 關鍵字能夠修飾類的實例方法、靜態方法和代碼塊。

synchronized 關鍵字保護的是對象而非方法、代碼塊,使用鎖來執行線程的互斥處理。

synchronized 修飾靜態方法和實例方法時保護的是不一樣的對象:

  • synchronized 修飾實例方法是使用該類的實例對象 this。
  • synchronized 修飾靜態方法是使用該類的類對象 class。

每一個對象擁有一個獨立的鎖,同一對象內的全部 synchronized 方法共用。

bank_synchronized

synchronized 方法注意事項

基於 synchronized 關鍵字保護的是對象原則,有以下推論:

  • 一個實例中的 synchronized 方法每次只能由一個線程運行,而非 synchronized 方法則能夠同時由多線程運行。
  • 一個實例中的多個 synchronized 方法一樣沒法多線程運行。
  • 不一樣實例中的 synchronized 方法能夠同時由多線程運行。
  • synchronized 修飾的靜態方法(this 對象)和實例方法(class 對象)之間,能夠同時被多線程執行。
synchronized 方法同步使用

synchronized 方法具備可重入性,即獲取鎖後能夠在一個 synchronized 方法,調用其餘須要一樣鎖的 synchronized 方法。

通常在保護實例變量時,將全部訪問該變量的方法設置爲 synchronized 同步方法。

若是隻是想讓方法中的某一部分由一個線程運行,而非整個方法,則可以使用 synchronized 代碼塊,精確控制互斥處理的執行範圍。

synchronized 方法執行流程
  1. 嘗試獲取對象鎖,若是獲取到鎖進入2,未獲取到鎖則加入鎖的等待隊列進入阻塞狀態等待被喚醒。
  2. 執行 synchronized 方法
  3. 釋放對象鎖,若是等待隊列存在線程正在等待獲取鎖,將其喚醒,當有多個線程處於等待隊列,沒法明確喚醒某一個,由多個線程競爭獲取。

死鎖

死鎖是指兩個或兩個以上的進程(線程)在執行過程當中,因爭奪資源而形成的一種互相等待的現象,若無外力做用,它們都將沒法推動下去。

死鎖產生的四個必要條件

  1. 互斥條件:指進程對所分配到的資源進行排它性使用,即在一段時間內某資源只由一個進程佔用。若是此時還有其它進程請求資源,則請求者只能等待,直至佔有資源的進程用畢釋放。
  2. 請求和保持條件:指進程已經保持至少一個資源,但又提出了新的資源請求,而該資源已被其它進程佔有,此時請求進程阻塞,但又對本身已得到的其它資源保持不放。
  3. 不剝奪條件:指進程已得到的資源,在未使用完以前,不能被剝奪,只能在使用完時由本身釋放。
  4. 循環等待條件:指在發生死鎖時,必然存在一個進程——資源的環形鏈,即進程集合 {P0,P1,P2,···,Pn} 中的 P0 正在等待一個 P1 佔用的資源;P1 正在等待 P2 佔用的資源,……,Pn 正在等待已被 P0 佔用的資源。

產生死鎖必須同時知足上述四個條件,只要其中任一條件不成立,死鎖可避免。

應該儘可能避免在持有一個鎖的同時,申請另外一個鎖。若是確實須要多個鎖,應該按照相同的順序獲取鎖。

線程的協做(wait()、notify() 方法細節)

多線程之間除了在競爭中作互斥處理,還須要相互協做。協做的前提是清楚共享的條件變量。

wait()、notify()、notifyAll() 都是 Object 類的實例方法,而不是 Thread 類中的方法。這三個方法與其說是針對線程的操做,倒不如說是針對實例的條件等待隊列的操做。

操做 obj 條件等待隊列中的線程(喚醒、等待):

  • obj.wait() 將當前線程放入 obj 的條件等待隊列。
  • obj.notify() 從 obj 的條件等待隊列喚醒一個線程。
  • obj.notifyAll() 喚醒 obj 條件等待隊列中的全部線程。

wait() 等待方法

每一個對象擁有一個鎖和鎖的等待隊列,另外還有一個表示條件的等待隊列,用於線程間的協做。調用 wait() 方法會將當前線程放入條件隊列等待,等待條件須要等待時間或者依靠其餘線程改變(notify()/notifyAll() )。等待期間一樣能夠被中斷,中斷將會拋出 InterruptedException 異常。

Object 類的 wait() 方法和 Thread 類的 sleep() 方法在控制線程上主要區別在於對象鎖是否釋放,從方法所屬類能夠看出 Object 類的 wait() 方法包含對象鎖管理機制。

  • wait() 實例方法用於線程間通訊協做
  • sleep() 靜態方法用於暫停當前線程
  • 二者均會放棄佔用 CPU
wait() 方法執行過程
  • 將當前線程放入條件隊列等待,釋放對象鎖。
  • 當前線程進入 WAITINGTIMED_WAITING 狀態。
  • 等待時間或者被其餘線程喚醒(notify()/notifyAll() ),從條件隊列中移除等待線程。

    • 喚醒的線程得到對象鎖,進入 RUNNABLE 狀態,從 wait() 方法返回,從新執行等待條件檢查。
    • 喚醒的線程沒法得到對象鎖,進入 BLOCKED 狀態,加入對象鎖的等待隊列,繼續等待。

notify() 喚醒方法

notify() 和 notifyAll() 方法的區別

  • notify() 方法會喚醒等待隊列中的一個線程。
  • notifyAll() 方法會喚醒等待隊列中全部線程。

一般使用 notifyAll() 方法,相比於 notify() 方法代碼更具健壯性,可是喚醒多個線程速度慢些。

注意:調用 notify() 方法以後,喚醒條件隊列中等待的線程,並將其移除隊列。被喚醒的線程並不會當即運行,由於執行 notify() 方法的線程還持有着鎖,等待 notify() 方法所處的同步(synchronized)代碼塊執行結束才釋放鎖。隨後等待的線程得到鎖從 wait() 方法返回,從新執行等待條件檢查。

總結:

  • 線程必須持有實例的鎖才能執行上述方法(wait()、notify()、notifyAll())
  • wait()/notify() 方法只能在 synchronized 代碼塊內被調用,若是調用 wait()/notify() 方法時,當前線程沒有持有對象鎖,會拋出異常 java.lang.IllegalMonitorStateException

生產者/消費者模式應用

  • 生產者(Producer)生成數據的線程
  • 消費者(Consumer)使用數據的線程

生產者線程和消費者線程經過共享隊列進行協做,

生產者/消費者模式在生產者和消費者之間加入了一個橋樑角色,該橋樑角色用於消除線程間處理速度的差別。

Producer-Consumer

Channel 角色持有共享隊列 Data,對 Producer 角色和 Consumer 角色的訪問執行互斥處理,並隱藏多線程實現。

線程的中斷

線程正常結束於 run() 方法執行完畢,但在實際應用中多線程模式每每是死循環,考慮到存在特殊狀況須要取消/關閉線程。Java 使用中斷機制,經過協做方式傳遞信息,從而取消/關閉線程。

中斷的方法

public static boolean interrupted()
public boolean isInterrupted()
public void interrupt()
  • interrupt() 和 isInterrupted() 是實例方法,經過線程對象調用。
  • interrupted() 是靜態方法,由當前線程 Thread.currentThread() 實際執行。

線程存在 interrupted 中斷狀態標記,用於判斷線程是否中斷。

  • isInterrupted() 實例方法返回對應線程的中斷狀態。
  • interrupted() 靜態方法返回當前線程的中斷狀態,存在反作用清空中斷狀態。

不一樣線程狀態的中斷反應

  • NEWTERMINATED
    調用 interrupt() 方法不起任何做用
  • RUNNABLE
    調用 interrupt() 方法,線程正在運行,且與 I/O 操做無關,設置線程中斷狀態標記而已。若是線程等待 I/O 操做,則會進行特殊處理。
  • BLOCKED
    調用 interrupt() 方法沒法中斷正在 BLOCKED 狀態的線程
  • WAITINGTIMED_WAITING
    調用 interrupt() 方法設置線程中斷狀態標記,拋出 InterruptedException 異常。這是一個受檢異常,線程必須進行處理。

中斷的使用

對於提供線程服務的模塊,應該封裝取消/關閉線程方法對外提供接口,而不是交由調用者自行調用 interrupt() 方法。

線程狀態轉換綜合圖解

結合線程的方法(Thread 類 + Object 類)來看線程的狀態轉換:

注:

  • Thread t = new Thread(); Thread 類調用靜態方法,t 對象調用實例方法。
  • Object o = new Object(); Object 類調用靜態方法,o 對象調用實例方法。
  • Running 表示運行中狀態,並不是 Thread.State 枚舉類型。

Java線程狀態轉換綜合

附錄

Runnable 接口和 Callable 接口

  • Runnable 接口提供的 run() 方法返回值爲 void
  • Callable 接口提供的 call() 方法返回值爲泛型

Callable 接口經常使用與配合 Future、FutureTask 類獲取異步執行結果。

相關文章
相關標籤/搜索