咱們在閱讀程序時,表面看來是在跟蹤程序的處理流程,實際上跟蹤的是線程的執行。
單線程程序java
在單線程程序中,在某個時間點執行的處理只有一個。數據庫
Java 程序執行時,至少會有一個線程在運行,這個運行的線程被稱爲主線程(Main Thread)。編程
Java 程序在主線程運行的同時,後臺線程也在運行,例如:垃圾回收線程、GUI 相關線程等。緩存
Java 程序的終止是指除守護線程(Daemon Thread)之外的線程所有終止。守護線程是執行後臺做業的線程,例如垃圾回收線程。咱們能夠經過 setDaemon() 方法把線程設置爲守護線程。服務器
多線程程序網絡
由多個線程組成的程序稱爲多線程程序(Multithreaded Program)。多個線程運行時,各個線程的運行軌跡將會交織在一塊兒,同一時間點執行的處理有多個。多線程
多線程應用場景:併發
P.S. 使用 java.nio 包中的類,有時即使不使用線程,也能夠執行兼具性能和可擴展性的 I/O 處理。異步
並行(parallel)與併發(concurrent)的區別ide
程序運行存在順序、並行與併發模式。
併發相對於順序和並行來講比較抽象。單個 CPU 併發處理即爲順序執行,多個 CPU 併發處理能夠並行執行。
若是是單個 CPU,即使多個線程同時運行,併發處理也只能順序執行,在線程之間不斷切換。
併發處理包括:併發處理的順序執行、併發處理的並行執行。
線程和進程的區別
當執行緊密關聯的多項工做時,一般線程比進程更加適合。
多線程程序的優勢和成本
優勢:
缺點(成本):
相對而言,如果存在耗時任務須要放入子線程中實際執行,線程使用成本能夠不計。
多線程編程的重要性
硬件條件知足多線程並行執行的條件以外,還須要程序邏輯可以保證多線程正確地運行,考慮到線程之間的互斥處理和同步處理。
建立與啓動線程的兩種方法:
線程的建立與啓動步驟——方法一:
Thread 實例和線程自己不是同一個東西,建立 Thread 實例,線程並未啓動,直到 start() 方法調用,一樣就算線程終止了,實例也不會消失。可是一個 Thread 實例只能建立一個線程,一旦調用 start() 方法,無論線程是否正常/異常結束,都沒法再次經過調用 start() 方法建立新的線程。而且重複調用 start() 方法會拋出 IllegalThreadStateException 異常。
Thread run( ) 方法 和 start() 方法:
線程的建立與啓動步驟——方法二:
Thread(Runnable target)
無論是利用 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
RUNNABLE
BLOCKED
WAITING
TIMED_WAITING
TERMINATED
Thread 類的靜態方法 currentThread() 返回當前正在執行的線程對象。
Thread 類的靜態方法 sleep() 可以暫停(休眠)當前線程(執行該語句的線程)運行,放棄佔用 CPU。線程休眠期間能夠被中斷,中斷將會拋出 InterruptedException
異常。sleep() 方法的參數以毫秒做爲單位,不過一般狀況下,JVM 沒法精確控制時間。
sleep() 方法調用須要放在 try catch 語句中,可能拋出 InterruptedException
異常。InterruptedException
異常可以取消線程處理,可使用 interrupt() 方法在中途喚醒休眠狀態的線程。
多線程示例程序中常用 sleep() 方法模擬耗時任務處理過程。
Thread 類的靜態方法 yield() 可以暫停當前線程(執行該語句的線程)運行,讓出 CPU 給其餘線程優先執行。若是沒有正在等待的線程,或是線程的優先級不高,當前線程可能繼續運行,即 yield() 方法沒法確保暫停當前線程。yield() 方法相似 sleep() 方法,可是不能指定暫停時間。
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++;
Java 語言的解決方式:
java.util.concurrent.atomic
包計算機結構中,CPU 負責執行指令,內存負責讀寫數據。CPU 執行速度遠超內存讀寫速度,緩解二者速度不一致引入了高速緩存。 預先拷貝內存數據的副本到緩存中,便於 CPU 直接快速使用。
所以計算機中除內存以外,數據還有可能保存在 CPU 寄存器和各級緩存當中。這樣一來,當訪問一個變量時,可能優先從緩存中獲取,而非內存;當修改一個變量時,可能先將修改寫到緩存中,稍後纔會同步更新到內存中。
對於單線程程序來講沒有太大問題,可是多線程程序並行執行時,內存中的數據將會不一致,最新修改可能還沒有同步到內存中。須要提供一種機制保證多線程對應的多核 CPU 緩存中的共享變量的副本彼此一致——緩存一致性協議。
Java 語言的解決方式:
若是隻是解決內存可見性問題,使用 synchronized 關鍵字成本較高,考慮使用 volatile 關鍵字更輕量級的方式。
有序性:即程序執行的順序嚴格按照代碼的前後順序執行。
Java 容許編譯器和處理器爲了提升效率對指令進行重排序,重排序過程不會影響到單線程程序的執行,卻會可能影響到多線程程序併發執行時候的正確性。
volatile 關鍵字細節
Java 使用 volatile
關鍵字修飾變量,保證可見性、有序性。
可是 volatile
關鍵字沒法保證對變量操做是原子性的。
每一個線程擁有獨立的程序計數器(指令執行行號)、棧(方法參數、局部變量等信息),多個線程共享堆(對象),這些區域對應 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 方法共用。
基於 synchronized 關鍵字保護的是對象原則,有以下推論:
synchronized 方法具備可重入性,即獲取鎖後能夠在一個 synchronized 方法,調用其餘須要一樣鎖的 synchronized 方法。
通常在保護實例變量時,將全部訪問該變量的方法設置爲 synchronized 同步方法。
若是隻是想讓方法中的某一部分由一個線程運行,而非整個方法,則可以使用 synchronized 代碼塊,精確控制互斥處理的執行範圍。
死鎖是指兩個或兩個以上的進程(線程)在執行過程當中,因爭奪資源而形成的一種互相等待的現象,若無外力做用,它們都將沒法推動下去。
死鎖產生的四個必要條件
- 互斥條件:指進程對所分配到的資源進行排它性使用,即在一段時間內某資源只由一個進程佔用。若是此時還有其它進程請求資源,則請求者只能等待,直至佔有資源的進程用畢釋放。
- 請求和保持條件:指進程已經保持至少一個資源,但又提出了新的資源請求,而該資源已被其它進程佔有,此時請求進程阻塞,但又對本身已得到的其它資源保持不放。
- 不剝奪條件:指進程已得到的資源,在未使用完以前,不能被剝奪,只能在使用完時由本身釋放。
- 循環等待條件:指在發生死鎖時,必然存在一個進程——資源的環形鏈,即進程集合 {P0,P1,P2,···,Pn} 中的 P0 正在等待一個 P1 佔用的資源;P1 正在等待 P2 佔用的資源,……,Pn 正在等待已被 P0 佔用的資源。
產生死鎖必須同時知足上述四個條件,只要其中任一條件不成立,死鎖可避免。
應該儘可能避免在持有一個鎖的同時,申請另外一個鎖。若是確實須要多個鎖,應該按照相同的順序獲取鎖。
多線程之間除了在競爭中作互斥處理,還須要相互協做。協做的前提是清楚共享的條件變量。
wait()、notify()、notifyAll() 都是 Object 類的實例方法,而不是 Thread 類中的方法。這三個方法與其說是針對線程的操做,倒不如說是針對實例的條件等待隊列的操做。
操做 obj 條件等待隊列中的線程(喚醒、等待):
每一個對象擁有一個鎖和鎖的等待隊列,另外還有一個表示條件的等待隊列,用於線程間的協做。調用 wait() 方法會將當前線程放入條件隊列等待,等待條件須要等待時間或者依靠其餘線程改變(notify()/notifyAll() )。等待期間一樣能夠被中斷,中斷將會拋出 InterruptedException
異常。
Object 類的 wait() 方法和 Thread 類的 sleep() 方法在控制線程上主要區別在於對象鎖是否釋放,從方法所屬類能夠看出 Object 類的 wait() 方法包含對象鎖管理機制。
WAITING
、TIMED_WAITING
狀態。等待時間或者被其餘線程喚醒(notify()/notifyAll() ),從條件隊列中移除等待線程。
RUNNABLE
狀態,從 wait() 方法返回,從新執行等待條件檢查。BLOCKED
狀態,加入對象鎖的等待隊列,繼續等待。notify() 和 notifyAll() 方法的區別
一般使用 notifyAll() 方法,相比於 notify() 方法代碼更具健壯性,可是喚醒多個線程速度慢些。
注意:調用 notify() 方法以後,喚醒條件隊列中等待的線程,並將其移除隊列。被喚醒的線程並不會當即運行,由於執行 notify() 方法的線程還持有着鎖,等待 notify() 方法所處的同步(synchronized)代碼塊執行結束才釋放鎖。隨後等待的線程得到鎖從 wait() 方法返回,從新執行等待條件檢查。
總結:
java.lang.IllegalMonitorStateException
。生產者線程和消費者線程經過共享隊列進行協做,
生產者/消費者模式在生產者和消費者之間加入了一個橋樑角色,該橋樑角色用於消除線程間處理速度的差別。
Channel 角色持有共享隊列 Data,對 Producer 角色和 Consumer 角色的訪問執行互斥處理,並隱藏多線程實現。
線程正常結束於 run() 方法執行完畢,但在實際應用中多線程模式每每是死循環,考慮到存在特殊狀況須要取消/關閉線程。Java 使用中斷機制,經過協做方式傳遞信息,從而取消/關閉線程。
public static boolean interrupted() public boolean isInterrupted() public void interrupt()
線程存在 interrupted 中斷狀態標記,用於判斷線程是否中斷。
NEW
、TERMINATED
RUNNABLE
BLOCKED
BLOCKED
狀態的線程WAITING
、TIMED_WAITING
InterruptedException
異常。這是一個受檢異常,線程必須進行處理。中斷的使用
對於提供線程服務的模塊,應該封裝取消/關閉線程方法對外提供接口,而不是交由調用者自行調用 interrupt() 方法。
結合線程的方法(Thread 類 + Object 類)來看線程的狀態轉換:
注:
Callable 接口經常使用與配合 Future、FutureTask 類獲取異步執行結果。