一. 初識線程
幾乎所有的操作系統都只支持同時運行多個任務,一個任務就是一個程序,每個運行中的程序就是一個進程。當一個程序運行時,內部可能包含了多個順序執行流,每個順序執行流就是一個線程。
1.1 進程與線程
進程是運行過程中的程序,具有一定的獨立功能,進程是系統進行資源分配和調度的一個獨立單位。一般而言,進程包括如下特征
- 獨立性:進程是系統中獨立存在的實體,擁有自己獨立的資源,每個進程都擁有自己私有的地址空間。
- 動態性:進程與程序的區別在於,程序只是一個靜態的指令集合,而進程是一個正在系統中活動的指令集合。
- 並發性:多個進程可以在多個處理器上並發執行,多個進程之間不會互相影響
並發性和並行性是兩個概念。並發指在同一時刻只能有一條指令執行,但多個進程指令被快速輪換執行,是的宏觀上具有多個進程同時執行的效果。
線程是進程的組成部分,一個進程可以擁有多個線程,一個線程必須有一個父進程。線程可以擁有自己的堆棧、自己的程序計數器和自己的局部變量。但不擁有系統資源。他和父進程的其他子線程共享進程所擁有的全部資源。線程和其他線程共同協作完成進程的任務。
1.2 線程的創建和啟動
1.21 繼承Thread類創建線程類。
步驟如下:
① 定義Thread的子類,重寫該類的run()方法
② 創建Thread子類的實例,創建子線程對象
③ 調用線程對象的start()方法
package com.gdut.thread; public class FirstThread extends Thread{ private int i; public void run(){ for (; i < 100; i++) { System.out.println(getName()+" "+i); } } public static void main(String[] args) { for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName()); if(i == 20){ new FirstThread().start(); new FirstThread().start(); } } } }
1.2.2 實現Runnable接口創建線程類
① 定義Runnable接口的實現類,並重寫該類的run()方法
② 創建Runnable接口實現類的實例,並以此實例作為Thread的target來創建Thread對象
③ 調用線程對象的start()方法
1.2.3 使用Callable和Future創建線程
Java 5開始,Java提供了Callable接口,該接口提供了一個call()方法可以作為線程執行體,但call()方法更加強大
- call()方法有返回值
- call()方法可以聲明拋出異常。
Java 5提供了Future接口來代表Callable接口里call()方法的返回值,並未Future接口提供了FutureTask實現類,該類實現了,Future接口和Runnable接口,可以作為Thread類的target。
在Future接口定義了如下公共方法來控制關聯的Callable任務
- boolean cancle(boolean mayInterruptRunning):試圖取消Future里關聯的Callable任務
- V get():返回Callable任務里call()方法的返回值
- V get(Long timeout,TimeUnit unit):返回Callable任務里call()方法的返回值。該方法讓程序最多阻塞timeout和unit指定的時間,如果經過指定時間后Callable任務依然沒有返回值,將會拋出TimeoutException。
- boolean isCancelled():如果Callable任務正常完成前被取消,則返回true。
- boolean isdone():如果Callable任務已完成,則返回true。
創建啟動有返回值的線程步驟如下:
① 創建Callable接口的實現類,並實現Call()方法,該Call()方法將作為線程執行體,且該Call方法有返回值,在創建Callable實現類的實例
② 使用FutureTask類來包裝Callable對象,該FutureTask對象封裝了該Callable對象的call()方法
③ 使用FutureTask對象作為Thread對象的Target創建並啟動線程
④ 調用FutureTask對象的get()方法來獲得子線程執行結束后的返回值
package com.gdut.thread; import java.util.concurrent.Callable; import java.util.concurrent.FutureTask; public class ThirdThread { public static void main(String[] args) { ThirdThread rt = new ThirdThread(); FutureTask task = new FutureTask((Callable<Integer>)() ->{ int i =0; for (; i <100 ; i++) { System.out.println(Thread.currentThread().getName()+"循環變量的值"+i); } return i; }); for (int i = 0; i < 100; i++) { System.out.println(Thread.currentThread().getName()+"循環變量的值"+i); if(i == 20){ new Thread(task,"有返回值的線程").start(); } } try{ System.out.println("子線程的返回值:"+task.get()); }catch(Exception ex){ ex.printStackTrace(); } } }
1.3 創建線程的三種方式對比
采用實現Runnable、Callable接口的方式創建多線程的優缺點
- 線程只是實現了Runnable接口或Callable接口,還可以繼承其他類
- 在這種方式下,多個線程可以共享同一個target對象,所以非常適合多個相同線程來處理同一份資源的情況,從而可以將CPU,代碼,數據分開,形成清晰的模型,較好體現面向對象的思想
- 劣勢是,編程稍微復雜,如果需要訪問當前線程,則必須使用Thread.currentThread()方法。
所以一般推薦使用實現Runnable、Callable接口的方式創建多線程。
1.4 線程的生命周期
線程的生命周期經過:新建,就緒,運行,阻塞和死亡5種狀態。使用new關鍵字創建線程,該線程就處於新建狀態。當線程對象調用了start()方法之后,該程序就處於就緒狀態,Java虛擬機回為其創建方法調用棧和程序計數器,處於這個狀態的程序還沒有開始運行,只是表示該線程可以運行了,至於何時開始運行,取決於JVM里線程調度器的調度。
處於就緒狀態的線程獲得了CPU,開始執行run()方法的線程執行體,則該線程處於運行狀態。
當一個線程開始運行后,他不可能一直處於運行狀態(除非它運行執行體非常短,一下子就執行完),線程在執行過程中需要被中斷,目的使其他線程獲得執行的機會,在選擇下一個線程,系統會考慮線程的優先級。
當一個線程調用了它的sleep()和yield()方法后主動放棄所占用的資源。
當發生如下情況,線程會進入阻塞狀態:
- 線程調用sleep()方法主動放棄所占用的處理器資源
- 線程調用了一個阻塞式的IO方法,在該方法返回前,該線程被阻塞
- 線程試圖獲得一個同步監視器,但該同步監視器正被其他線程所持有。
- 線程在等待某個通知
- 程序調用了線程的suspend()方法將該線程掛起。但這個方法容易導致死鎖,應該盡量避免使用該方法。
被阻塞線程在合適的時候重新進入就緒狀態。重新等待線程調度器調度它。
當發生特定的情況時可以解除上面的阻塞
- 調用sleep()方法的線程經過了指定時間
- 線程調用的阻塞式IO方法已經返回
- 線程成功的獲得了試圖取得同步監視器
- 線程正在等待某個通知時,其他線程發出了一個通知
- 處於掛起狀態的線程被調用了resume()回復方法
線程會以如下三種方式結束,進入死亡狀態
- run()或call()方法執行完成,線程正常結束
- 線程拋出一個為捕獲的Exception
- 直接調用該線程的stop()方法來結束該線程——這個方法容易導致死鎖,應該盡量避免使用該方法。
二. 控制線程
2.1 join線程
Thread提供了讓一個線程等待另一個線程完成的方法——join()方法。
當程序執行流中調用了其他線程的join()方法時,調用線程被阻塞,知道被join()方法加入的join線程執行完為止
join()方法通常由使用的線程的程序調用,將大問題划分許多小問題,每個小問題分配一個線程。當所有的小問題處理后,在調用主線程進一步處理。
package com.gdut.thread; public class JoinThread extends Thread{ public JoinThread(String name) { super(name); } @Override public void run() { for (int i = 0; i <100 ; i++) { System.out.println(getName()+" "+i); } } public static void main(String[] args) throws Exception { new JoinThread("新線程").start(); for (int i = 0; i <100 ; i++) { if(i == 20){ JoinThread jt = new JoinThread("被join的線程"); jt.start(); jt.join(); } System.out.println(Thread.currentThread().getName()+" "+i); } } }
2.2 后台線程
有一種線程,后台執行的,它的任務是為其他線程提供服務。這種線程被稱為后台線程,又稱守護線程或精靈線程。JVM的垃圾回收線程就是典型的后台線程
后台線程有個特征:如果所有的前台線程都死亡,后台線程會自動死亡。調用Thread對象的setDaemon(true)方法可將指定線程設置成后台線程。
2.3 線程睡眠:sleep
如果需要讓正在執行的線程暫停一段時間,並進入阻塞狀態,則可以通過Thread類的靜態方法sleep()來實現。
在調用了sleep()方法之后,線程進入阻塞狀態,在其睡眠時間內,該線程將不會獲得執行的機會,即使系統中沒有其他可執行的線程。
2.4 線程讓步
yield()方法是一個和sleep()方法有也是點相似的方法,它Thread類的提供的一個靜態方法,它可以讓當前正在執行的線程暫停,但它不會阻塞該線程,只是將線程轉入就緒狀態。yield()方法只是讓當前的線程暫停一下,讓系統的線程調度器重新調度一次,完全可能的情況是:當線程調用了yield()方法暫停之后,線程調度器又將其調度出來重新執行
關於sleep()方法和yield()方法的區別
- sleep()方法暫停當前線程后,會給其他線程執行機會,不會理會其他線程的優先級;但yield()方法只會給優先級相同,或優先級更高的線程執行機會
- sleep()方法會將線程轉入阻塞狀態,知道經過阻塞時間才會轉入就緒狀態;yield()方法只是強制將線程轉入就緒狀態
- sleep()方法聲明拋出了InterruptedException異常,所以調用sleep()方法要么捕捉異常,要么顯式聲明拋出異常;而yield()方法則沒有聲明拋出異常。
- sleep()方法比yield()方法有更好的可移植性,通常不建議使用yield()方法來控制並發線程的執行
2.5 改變線程優先級
每個線程執行時都具有一定的優先級,優先級高的線程得到更多執行的機會,而優先級低的線程則得到較少的執行機會。
每個線程默認的優先級都和創建它的父線程的優先級相同,默認情況下,main線程具有普通優先級,由main線程創建的子線程也具有普通優先級。
Thread類提供了setPriority(int new Priority)、getPriority()方法來設置和返回指定線程的優先級,其中setPriority(int new Priority)參數可以是一個整數,范圍1~10,也可以使用Thread類的如下三個常量:
- MAX_PRIORITY:其值是10
- MIN_PRIORITY:其值是1
- NORM_PRIORITY:其值是5
三. 線程同步和線程安全問題
3.1 線程安全
臟數據問題,例如多個線程同時操作公共數據引發的數據出錯問題,這里不再敘述
3.2 同步代碼塊
為了解決上面的問題,Java多線程支持引入了同步監視器來解決這個問題,使用同步監視器的通用方法就是同步代碼塊。同步代碼塊的語法格式如下:
synchronized(obj){ //此處代碼就是同步代碼塊 }
上面代碼的含義是:線程開始執行同步代碼塊之前,必須先獲得同步監視器的鎖定。
3.3 同步方法
與同步代碼塊對應,Java的多線程安全還支持同步方法,同步方法就是用synchronized關鍵字來修飾某個方法,則該方法稱為同步方法。對於synchronized修飾的方法(非static方法)而言,無須顯式指定同步監視器,同步方法的監視器是this,也就是調用該方法的對象
當執行了同步監視器的wait()方法,則當前線程暫停,並釋放同步監視器。
3.4 同步鎖
從Java 5開始,Java提供了更強大的線程同步機制——通過顯式定義同步鎖對象實現同步,在這種機制下,同步鎖由lock充當。
某些鎖可能允許對貢獻資源的訪問。如ReadWriteLock,Lock,ReentrantRock(可重入鎖)。為ReadWriteRock提供了ReentrantReadWriteLock實現類。
通常建議在Finally塊確保在必要時釋放鎖。
3.5 死鎖
當兩個線程相互等待對方釋放鎖是就會發生死鎖。一旦出現死鎖,整個程序將不會發生任何異常,也不會給出任何提示。只是所有的線程都處於阻塞狀態,無法繼續。
