進程,線程呜叫,任務
進程(Process)是程序運行實例侦香,比如一個正在運行的QQ程序就是一個進程。進程是程序向操作系統(tǒng)申請資源的基本單位弥奸。
線程(Thread)是進程中可獨立執(zhí)行的最小單位榨惠,比如QQ程序中的語音服務可以由多個線程完成。
一個進程可以包含多個進程盛霎,在同一個進程中的所有線程共享該進程的資源赠橙。
一個線程需要完成的計算稱為任務,特定的線程執(zhí)行特定的任務摩渺,比如QQ中的語音任務简烤。
線程的創(chuàng)建剂邮,啟動摇幻,運行
線程的任務處理邏輯可以在Thread類的run實例方法中直接實現或者通過該方法進行調用,因此run方法相當于線程的任務邏輯入口挥萌,它由Java虛擬機在運行相應線程時直接調用绰姻,不是由應用代碼進行調用。
要運行一個線程引瀑,實際上是執(zhí)行該線程的run方法狂芋。首先我們需要啟動線程,使用Thread類的start方法憨栽。調用start方法啟動線程實際上是請求Java虛擬機運行相應的線程帜矾,但是這個線程何時能夠運行是由線程調度器(Scheduler)決定的。因此使用start方法屑柔,并不表示該線程的已經開始運行屡萤,這個線程可以稍后運行,也可能永遠不會運行掸宛。
Thread類有兩個構造器:Thread() 與 Thread(Runnable target)死陆,因此創(chuàng)建線程也有兩種方式。一種是繼承Thread唧瘾,定義Thread的子類并重寫run方法措译,在run方法中實現線程任務的處理邏輯别凤;另一種則是實現 Runnable 接口,并在該實例的run方法中實現線程任務的處理邏輯领虹,將這個實例作為參數傳入Thread(Runnable target)構造其中规哪。
以下是創(chuàng)建線程的兩種示例
(1)繼承Thread類
public class Client {
public static void main(String[] args) {
Thread myThread = new MyThread();
myThread.start();//啟動線程
System.out.println("ThreadName is "+Thread.currentThread().getName());
}
}
class MyThread extends Thread{
//重寫run方法
@Override
public void run() {
System.out.println("ThreadName is "+ Thread.currentThread().getName() );
}
}
(2)實現 Runnable 接口
public class Client {
public static void main(String[] args) {
Thread myThread = new Thread(new MyThread());
myThread.start();//啟動線程
System.out.println("1ThreadName is "+Thread.currentThread().getName());
}
}
class MyThread implements Runnable{
//重寫run方法
@Override
public void run() {
System.out.println("2ThreadName is "+ Thread.currentThread().getName() );
}
}
除了這兩種外,還有一種是實現Callable接口塌衰,并與Future由缆、線程池結合使用,在這里不談及猾蒂。
輸出結果可能有兩種情況
第一種:
1ThreadName is main
2ThreadName is Thread-0
第二種:
2ThreadName is Thread-0
1ThreadName is main
這也驗證了均唉,即使是使用start方法,并不表示該線程的已經開始運行肚菠,這個線程可以稍后運行舔箭,也可能永遠不會運行。
但是不管使用哪種方式創(chuàng)建線程蚊逢,一旦線程的run方法執(zhí)行完畢层扶,相應的線程也就運行結束。
線程屬于一次性用品烙荷,我們不能在一個已經運行結束的線程上再次調用其start方法使其重新運行镜会,實際上一個線程的start方法也只能被調用一次,若多次調用則會拋出 java.lang.IllegalThreadStateException 異常终抽。
Java語言并不阻止我們直接調用run方法戳表,這是因為在Java平臺中,線程也是一個對象昼伴;其次run方法為一個public方法匾旭,但是即便如此,我們也要避免直接調用run方法圃郊。
public class Client {
public static void main(String[] args) {
Thread myThread = new Thread(new MyThread());
myThread.start();//啟動線程
myThread.run();//在main方法中直接調用run方法
System.out.println("1ThreadName is "+Thread.currentThread().getName());
}
}
----output-----
2ThreadName is main
1ThreadName is main
2ThreadName is Thread-0
輸出結果顯示价涝,run方法被調用了兩次,一次是Java虛擬機直接調用持舆,一次是應用代碼直接調用色瘩。前者是運行在自己的線程中,后者是運行在main線程中逸寓,這樣就違背了線程的意義居兆。
Thread類實際上是 Runnable 接口的一個實現類
public class Thread implements Runnable
在Thread類的run方法源碼如下:
public void run() { // target 的類型為 Runnable
if (target != null) {
target.run(); //調用 Runnable 接口中的 run 方法
}
}
根據上面代碼可以看出,run方法的代碼邏輯決定了創(chuàng)建線程的兩種方式:
一種是在Thread類的子類中實現席覆,這時候target為空史辙,run方法的具體邏輯在其子類中實現
另一種若target不為空,那么Thread的run方法將調用 Runnable 接口的實現類中的run方法
這兩種創(chuàng)建線程的區(qū)別
①Thread類的子類創(chuàng)建線程是一種基于繼承的方法,而Runnable 接口的實現類創(chuàng)建線程是一種基于組合的方式聊倔,后者靈活性更強晦毙,耦合性更低。
②Runnable 接口的實現類創(chuàng)建線程意味著多個線程實例可以共享同一個實例耙蔑。
③Runnable 接口的實現類創(chuàng)建線程比Thread類的子類創(chuàng)建線程成本更低见妒。
④線程池只能放入實現Runable或callable類線程,不能直接放入繼承Thread的類甸陌。
守護線程與用戶線程
按照線程是否會阻止Java虛擬機正常停止须揣,可以將線程劃分為守護線程與用戶線程,使用daemon屬性設置钱豁,true表示守護線程耻卡,該屬性默認與父類線程的屬性值相同。用戶線程會阻止Java虛擬機正常停止牲尺,因此Java虛擬機必須在所有用戶線程已經停止的情況下才會正常停止卵酪。守護線程不會影響Java虛擬機的正常停止。
一般情況下谤碳,子線程是否是守護線程取決于父線程溃卡,父線程是什么類型的線程,子線程就是什么類型的線程蜒简,也可以通過setDaemon設置瘸羡,線程的默認優(yōu)先級也會跟隨父線程,但是子線程的生命周期跟父線程生命周期沒什么必然聯(lián)系搓茬。
線程的生命周期
Java線程的狀態(tài)可以通過Thread.getState()來獲取犹赖,返回值是一個枚舉類,線程狀態(tài)包括以下幾種:
①NEW:一個已經創(chuàng)建而未啟動的線程(未調用start)垮兑,線程只能被啟動一次冷尉,因此一個線程只有一次處于該狀態(tài)的機會漱挎。
②RUNNABLE:該狀態(tài)包括兩個子狀態(tài):READY 和 RUNNING 系枪。READY狀態(tài)可以被線程調度器(Scheduler)進行調度從而變成 RUNNING 狀態(tài)。反之使用 Thread.yield() 轉換磕谅。RUNNING 狀態(tài)表示該線程的run方法正在被執(zhí)行私爷。
③BLOCKED:線程發(fā)起阻塞式I/O操作后,或者申請一個鎖資源時就會處于該狀態(tài)膊夹。
④WAITING:等待其他線程執(zhí)行的狀態(tài)衬浑,調用wait(),join()放刨,park()等方法就會處于該狀態(tài)
⑤TIMED_WAITING:與WAITING狀態(tài)類似工秩,差別在于該狀態(tài)是有時間限制的等待
⑥TERMINATED:已經執(zhí)行結束的線程處于該狀態(tài)。一個線程只有一次處于該狀態(tài)的機會。
串行助币,并行浪听,并發(fā)
串行:先做完事情A,再做完事情B眉菱,再做完事情C迹栓,依此類推。
并發(fā):先做事情A俭缓,在事情A還沒做完時克伊,開始做事情B,在事情B還沒做完時開始做事情C华坦,依次類推愿吹。
并行:事情A,B,C同時開始進行。
競態(tài)
定義:指計算的正確性依賴于相對時間順序或者線程的交錯。
競態(tài)往往會伴隨讀取臟數據的問題载弄。
產生的條件:訪問(讀取更新)同一組共享變量的多個線程所執(zhí)行的操作相互交錯耘拇。
注:局部變量不會導致競態(tài),因為不同線程訪問的是各自的那一份局部變量宇攻。
線程安全
如果一個類在單線程環(huán)境下能夠運行正常惫叛,且在多線程環(huán)境下,不必為其做任何更改的情況下也能運行正常逞刷,我們將該類其稱為具有線程安全嘉涌。
如果一個類是線程不安全的,那么在多線程下會導致線程安全問題夸浅,概括來講包括以下三個方面
①原子性:對于涉及共享變量訪問的操作仑最,若該操作從執(zhí)行線程以外的任意線程來看都是"不可分割"的,那么說該操作具有原子性帆喇。所謂"不可分割"指的是①該操作在外部線程看來要么已經完成警医,要么未發(fā)生,即其他線程是不會看到該操作的中間狀態(tài)坯钦,②訪問同一組共享變量的線程不能被交錯预皇。原子性的存在排除了一個線程在對一個共享變量進行讀寫操作時,另一個線程也對該變量進行讀寫或更新操作而帶來的干擾問題婉刀。線程不可能進行交錯吟温,因此也消除了競態(tài)的可能性。
注意點:原子操作針對的是訪問共享變量突颊,對局部變量的訪問無所謂原子性鲁豪;原子操作只有在多線程的情況下才有意義潘悼。原子操作+原子操作不等于原子操作。
原子性實現方式:①軟件上使用鎖(lock)②硬件上利用CAS指令
②可見性:一個線程對共享變量的更新的結果對于讀取該共享變量的線程是否可見稱為可見性爬橡。在Java平臺使用volatile關鍵字保證可見性挥等。Java語言規(guī)范保證父線程在啟動子線程之前對共享變量的更新對子線程來說是可見的,同樣保證一個線程終止后該線程對共享變量的更新對于調用該線程的join方法的線程是可見的堤尾。
③有序性:如果在本線程內觀察肝劲,所有操作都是有序的;如果在一個線程中觀察另一個線程郭宝,所有操作都是無序的辞槐。前半句是指“線程內表現為串行語義”,后半句是指“指令重排序”現象和“工作內存主主內存同步延遲”現象粘室。
有序性實現方式:①volatile 具有禁止指令重排序榄檬,在一定程度上具有有序性。②synchronized鎖
上下文切換
時間片:每個線程占用處理器的時間稱為時間片(Time Slice)衔统。
時間片決定了一個線程可以連續(xù)占用處理器運行的長度鹿榜,當這個線程的時間片用完或者被迫暫停時(切出),另一個線程就會被線程調度器選中并運行(切入)锦爵,這種方式叫做線程上下文切換舱殿。
處理器上連續(xù)運行的多線程實際上是每個線程以時間片斷斷續(xù)續(xù)的進行,線程的切出與切入都需要操作系統(tǒng)保存和恢復此線程的進度信息险掀,這個信息就叫做上下文沪袭。
一個線程的生命周期狀態(tài)在RUNNABLE,BLOCKED樟氢,WAITING冈绊,TIMED_WAITING這四個狀態(tài)之間切換的過程就是一個上下文切換的過程。
在三種情況下可能會發(fā)生上下文切換:中斷處理埠啃,多任務處理死宣,用戶態(tài)切換。在中斷處理中碴开,其他程序”打斷”了當前正在運行的程序毅该。當CPU接收到中斷請求時,會在正在運行的程序和發(fā)起中斷請求的程序之間進行一次上下文切換叹螟。在多任務處理中鹃骂,CPU會在不同程序之間來回切換,每個程序都有相應的處理時間片罢绽,CPU在兩個時間片的間隔中進行上下文切換。對于一些操作系統(tǒng)静盅,當進行用戶態(tài)切換時也會進行一次上下文切換良价,雖然這不是必須的寝殴。
多線程運行必須有上下文切換,但上下文切換會帶來性能消耗明垢,因此多線程不一定比單線程計算效率高蚣常。
線程活性故障
一般情況下,我們都希望線程一直處于RUNNABLE狀態(tài)痊银,但是事實并非如此抵蚊。線程有可能會因為上下文切換,程序自身的錯誤溯革,資源的有限等導致一個線程處于非RUNNABLE狀態(tài)贞绳,這種現象就稱為線程活性故障,常見的線程活性故障有以下幾種:
①死鎖(Deadlock):線程A持有a資源并等待線程B釋放b資源再運行致稀,但線程B雖然持有b資源卻也在等待線程A釋放a資源冈闭,因此兩線程永遠無法進行,導致線程一直處于非RUNNABLE狀態(tài)抖单。
②鎖死(Lockout):線程A持有a資源并等待線程B釋放b資源再運行萎攒,但是線程B因某種原因而被終止運行了,這樣導致線程A永遠無法運行矛绘。
③活鎖(Livelock):線程A一直處于RUNNABLE狀態(tài)耍休,但是線程A并沒有在執(zhí)行任務,而是在做無用功货矮。
④饑餓(Starvation):由于線程優(yōu)先級的原因羹应,導致某些低優(yōu)先級的線程永遠獲取資源而導致任務無法進行。
線程中斷
interrupt():用于中斷線程次屠,將調用該方法的線程的狀態(tài)設置為“中斷”狀態(tài)园匹。但是并不會停止線程運行。
isInterrupted():測試線程Thread對象是否已經是中斷狀態(tài)劫灶,但不清除狀態(tài)標志裸违。
interrupted():測試當前線程是否已經是中斷狀態(tài),執(zhí)行后具有狀態(tài)標志清除為false的功能本昏。
“中斷”狀態(tài)只是一個標志供汛,正常運行的程序不去檢測狀態(tài),就不會終止涌穆,而wait等阻塞方法會去檢查并拋出異常怔昨。
關于interrupted() 和 isInterrupted()的源碼如下
public static boolean interrupted() {
return currentThread().isInterrupted(true); //①
}
private native boolean isInterrupted(boolean ClearInterrupted); //② 參數代表是否要清除狀態(tài)位
public boolean isInterrupted() {
return isInterrupted(false); //③
}
nterrupted 是作用于當前線程,isInterrupted 是作用于調用該方法的線程對象所對應的線程宿稀。
interrupted()實際上是調用②中的isInterrupted(boolean ClearInterrupted)趁舀,默認值為true;isInterrupted()其實也是調用的②祝沸,默認值為false矮烹。
注意以下說明
InterruptedException - if any thread has interrupted the current thread. The interrupted status of the current thread is cleared when this exception is thrown.
當一個線程處于中斷狀態(tài)時越庇,如果再由wait、sleep以及jion三個方法引起的阻塞奉狈,那么JVM會將線程的中斷標志重新設置為false卤唉,并拋出一個InterruptedException異常
如何捕獲線程中的異常
背景:線程中的異常是不能拋出到調用該線程的外部方法中捕獲的,線程方法的異常(無論是checked還是unchecked exception)仁期,都應該在線程代碼邊界之內(run方法內)進行try catch并處理掉桑驱。
工具:Thread.UncaughtExceptionHandler 新接口,它允許我們在每一個Thread對象上添加一個異常處理器跛蛋。Thread.UncaughtExceptionHandler.uncaughtException()方法會在線程因未捕獲的異常而面臨死亡時被調用熬的。
使用:
自定義線程異常捕獲處理器
//定義這個線程異常捕獲的處理器
class ThreadExceptionhandler implements Thread.UncaughtExceptionHandler{
@Override
public void uncaughtException(Thread t, Throwable e) {
System.out.println("捕獲線程 "+t+" 異常:"+e);
}
}
有三種方式使用該線程的異常捕獲器
1 在創(chuàng)建線程時進行設置
Thread mythread = new MyThread();
mythread.setUncaughtExceptionHandler(new ThreadExceptionhandler());
mythread.start();
2 使用Executors創(chuàng)建線程時,還可以在TreadFactory中設置问芬。
TreadFactory:線程工廠類悦析,有默認實現,如果有自定義的需要則需要自己實現ThreadFactory接口并作為參數傳入此衅。
ExecutorService exec = Executors.newCachedThreadPool(new ThreadFactory(){
@Override
public Thread newThread(Runnable r) {
Thread thread = newThread(r);
thread.setUncaughtExceptionHandler(new ThreadExceptionhandler());
return thread;
}
});
exec.execute(new ExceptionThread());
3 設置默認的線程異常捕獲器
Thread.setDefaultUncaughtExceptionHandler(new ThreadExceptionhandler());