想要學習多線程,必須要先理解什么是並發什么是並行。
並行:是指兩個或多個線程在同一時刻發生。
並發:是指兩個或多個線程在同一時間段內發生。
為了方便理解多線程的概念,我們先舉一個例子:
假如我們把公司看做是一個進程,那么人就是其中的線程。進程必須得有一個主線程,公司在創業初期往往可能出現一人打天下的現象,但是,至少得有一個人,公司才能運作。公司創業初期,業務還不算太多,往往就是老板一個人身兼數職,一天如果只有1、2趟活兒,應該還是忙得過來的。時間長了,隨着業務的發展、口碑的建立,生意越來越興隆,一個人肯定就忙不過來了。假設一天有5個活兒,老板一個人必須搬完A家才能搬B家,搬到黃昏估計也就搬到C家,D和E家都還在焦急地等待着呢。老板一個人要充當搬運工、司機、業務聯系人、法人代表、出納等眾多角色,累死累活公司的規模也上不去,人手不夠制約了公司的發展。那么怎么辦,很簡單,增加人手,然后這些人各司其職,同時工作,很塊就處理完了公司的業務。而這些人手就是所謂的線程,開啟了的線程可以並行運行。
多線程的好處:多線程相當於在一個進程中有多條不同的執行路徑,在同一時刻進行執行,可以提高程序的執行效率。
多線程的應用場景:迅累多線程下載,數據庫連接池,分批發送敦信等。
同步:相當於單線程,代碼從上往下按照順序執行。
異步:相當於多線程,開啟一條新的路徑執行,多條執行路徑同時執行,互不影響。
一,進程和線程的概念
進程:是一個正在執行的程序。每一個進程都有一個執行順序,該順序是一個執行路徑,或者叫做執行單元。進程是資源分配的基本單位(內存,進程ID(PID))。
線程:是一條執行路徑,是進程內部的一個獨立的執行單元,每個線程互不影響,一個進程至少有一個線程,線程控制進程的執行。線程是資源調度的單位。
多線程:在一個進程中多個線程並發執行。
1.1進程和線程的區別
♦內存區別:進程是有獨立的內存空間,每一個進程之間是相互獨立的,互不干擾。
線程除了有私有的內存空間外,還有共有的內存空間。
♦安全性:進程是相互獨立的,一個進程的崩潰是不會影響到其他的進程,進程是安全的。
線程存在內存空間的共享,一個線程的崩潰可能會影響到其他線程,所以線程沒有進程安全性高。
1.2進程和線程的關系
進程是相互獨立的,一個進程下可以有一個或者多個線程。
Java中很少使用進程的概念,但可以使用下面代碼來創建進程:
Runtime runtime=Runtime.getRuntime();//創建進程的方法
1.3Java默認有幾個線程?
Java默認是有兩個線程:主線程和垃圾回收的線程(Main和GC)
1.4java本身能否啟動線程?
java本身是沒有辦法啟動線程的,線程啟動時需要調用底層操作系統的支持。Java通過調用本地方法,來調用c++編寫的動態函數庫,由c++去操作底層來啟動線程。所以Java是通過間接的調用來啟動線程的。
二,自定義線程的四種方式:1.繼續Thread類,2.實現Runable接口,3.實現Callable接口的方法,4.線程池管理方法,另外還可以使用匿名對象。
第一種:繼承Thread類
步驟: 1.定義類繼承Thread類
2.重寫Thread類中的run方法
為什么要重寫run()方法? 將新定義的線程所要執行的代碼存儲在run()方法中,因為Thread類是用來描述線程的,而用於存儲該線程運行時的代碼的功能,就是由run()方法實現的,所以一般將新建的線程所要執行的代碼,都放到run()方法中。注意主線程運行時的代碼是存儲在main()方法中的。(一般新建立的線程調用的start()方法,是由父類繼承下來的,而start()方法中調用的run()方法,是被Thread子類重寫過的方法。
3.調用線程的start()方法(該方法的作用:啟動線程,並調用run方法)
1 class Demo extends Thread//1.繼承Thread類 2 { 3 public void run() { //2.重寫run方法 4 for(int i=0;i<10;i++){ 5 System.out.println("run..."+i); 6 } 7 } 8 } 9 public class TreadDemo { 10 public static void main(String[] args) {//main函數也是一個線程 11 Demo d=new Demo();//創建一個線程 12 d.start();//3.調用start方法 13 for(int i=0;i<20;i++){ 14 System.out.println("main...."+i); 15 } 16 } 17 }
運行結果部分圖:
運行結果分析:
主線程(要執行代碼存儲在main()函數中)開始執行,當運行到這一句Demo d=new Demo(); 會新創建一個線程2,接着主線程調用新線程的start()方法,而start()方法內部又會區調用run()方法(這個新線程要執行的代碼存儲在run()方法中),此時新線程也開啟了。而這一塊打印結果為什么是main和run 的交替?因為當線程2開啟后,只能說明線程2具備了運行條件,不一定立馬就有cup的執行權,所以打印的結果先是main.....i ,這時線程2突然搶到了cup執行權,於是也進行了打印輸出run.....i ,接着cup執行權又被主線程強走,然后打印main.....i ,所以打印結果就是他們的交替。下圖是對上述代碼執行過程的分析,要注意的是一個進程在開始至少有一個線程,而對於上面這個代碼,剛開始的這個主線程就是由main()函數開啟的。而且只要進程中還有線程未執行完畢,該進程就不會結束。
說到這一塊可能就有人迷惑,那既然調用start()方法時,該方法會接着調用run()方法,那為什么不能直接調用run()方法來執行呢?要注意start()方法的作用不止調用run()方法,還用啟動線程的作用。
先舉一個直接調用run()方法的例子:
1 public static void main(String[] args) {//main函數也是一個線程 2 Demo d=new Demo();//創建一個線程 3 d.run();//3.調用start方法 4 for(int i=0;i<20;i++){ 5 System.out.println("main...."+i); 6 } 7 }
run()方法中的代碼與上個例子中的相同。
執行結果:
不論運行多少次,都會發現結果和上圖都是一樣的。這和線程的隨機性明顯不符,為什么呢??(線程的隨機性指的是多個線程的執行結果不唯一,誰搶到cup執行權誰執行)
因為當你直接調用run()方法時,雖然通過該句Demo d=new Demo()已創建新線程,但是並沒有啟動新線程!! 所以當主線程執行到這一句d.run(),因為新線程沒有開啟,所以run()方法中的內容是在主線程中執行了的,所以只有當run打印完,才會輪到main打印。
要注意,人們平時看到的多個線程在“同時”執行,其實在底層並不是多個線程同時一塊執行的,而是通過快速的交替使用cup來執行自己的任務,因為其交替的速度非常快,快到人眼是感覺不到的,所以使我們在表面上看去,以為是多個線程在同時執行。這也就是為什么當我們電腦打開的程序也多時,電腦就會越卡。
補充:可通過Thread的getName()方法獲得新線程的默認名字。 新線程的默認名字格式:Thread-0 編號從0開始。
//兩種獲得線程名字的方法
this.getName(); Thread.currentThread().getName();
Thread.currentThread()//可獲得當前線程對象
那如何給新線程自定義名字呢?通過查資料,我們得知Thread有一個帶參構造函數,所以我們可以直接在新建線程時,直接將名字賦給它。
Demo d=new Demo("one");//創建一個線程
要注意我們既然要使用它的帶參構造函數,那么我們在子類中就必須定義一個帶參構造函數。
1 class Demo extends Thread//1.繼承Thread類 2 { 3 Demo(String name){ //定義一個帶參構造函數。 4 super(name); 5 } 6 public void run() { //2.重寫run方法 7 for(int i=0;i<10;i++){ 8 System.out.println(this.getName()+"run..."+i); 9 } 10 } 11 } 12 public class TreadDemo { 13 public static void main(String[] args) {//main函數也是一個線程 14 Demo d=new Demo("one");//創建一個線程 15 d.start();//3.調用start方法d.run() 16 for(int i=0;i<20;i++){ 17 System.out.println("main...."+i); 18 } 19 } 20 }
第二種:實現Runable接口(其實Thread也是實現Runnable接口的)
步驟:1.創建類實現Runnable接口
2.實現Runnable接口中的run()方法
目的:將線程執行的代碼存儲在run()方法中
3.通過Thread類建立線程對象
4.將Runnable接口的子類對象作為實際參數傳遞給Thread類的構造函數
為什么要將Runnable接口的子類對象傳遞給Thread的構造函數?
因為自定義的run()方法所屬的對象是Runnable接口的子類對象,所以要讓線程去執行指定對象的run()方法,就必須明確該run(方法的所屬對象。
5.調用Thread類的start方法開啟線程並調用Runnable接口子類的run()方法
實現方式和繼承方式的區別:
1,實現方式避免了單繼承的局限性。(因為一個類只能繼承一個類,當繼承了Thread類就無法在繼承其他類,但因為實現多個接口,所以就可以繼承其他類和實現其他接口。)
2,實現方式更適合處理多線程共享數據的問題。(因為實現方式創建多個線程時,使用的資源都是一份共享的,而使用繼承方式創建多個線程時,因為是通過new的方式,所以每個線程使用的資源不是同一份,因此必須使用static變量修飾,以達到資源共享的目的。)
3,線程池只能放入實現Runnable或Callable接口的線程,不能直接放入繼承Thread的類。
4,繼承Thread:線程代碼存放在Thread子類的run()方法中
實現Runnable:線程代碼存放在Runnable接口子類的run()方法
1 class Demo implements Runnable{// 1.定義類實現Runnable接口 2 public void run() { //2.重寫run方法 3 for(int i=0;i<100;i++){ 4 System.out.println(Thread.currentThread().getName()+"run..."+i); 5 } 6 } 7 public class TreadDemo { 8 public static void main(String[] args) {//main函數也是一個線程 9 Demo d=new Demo(); 10 Thread t1=new Thread(d);//3.通過Thread類建立線程對象 4.將Runnable接口的子類對象作為實際參數傳遞給Thread類的構造函數 11 Thread t2=new Thread(d); 12 t1.start();//5.調用Thread類的start方法開啟線程並調用Runnable接口子類的run()方法 13 t2.start(); 14 for(int i=0;i<200;i++){ 15 System.out.println("main...."+i); 16 } 17 } 18 }
第三種:實現callable接口<泛型>
步驟:1.創建類(MyCallable)並實現Callable接口。
2.實現call()方法。(同run方法,但不同的是要拋異常)
3.創建FutureTask實例,並將MyCallable類的實例作為參數傳遞給FutureTask。
4.創建Thread實例,並將FutureTask的實例作為參數傳遞給Thread。
5.調用Thread實例的start方法來開啟線程
6.獲取並打印(MyCallable)的執行結果。FutureTask實例.get().
1 import java.util.concurrent.Callable; 2 import java.util.concurrent.ExecutionException; 3 import java.util.concurrent.FutureTask; 4 5 class MyCallable implements Callable {//1.定義類(MyCallable)去實現Callable接口。 6 @Override 7 public Object call() throws Exception {//2.實現call()方法。 8 for (int i = 0; i <100 ; i++) { 9 System.out.println("子線程"+i); 10 } 11 return "子線程執行完畢"; 12 } 13 } 14 public class MyCallableDemo { 15 public static void main(String[] args) { 16 FutureTask futureTask = new FutureTask(new MyCallable());//FutureTask類實現了Runnable接口 17 // 3.創建FutureTask實例,並創建MyCallable類的實例作為參數傳遞給FutureTask實例。 18 Thread thread=new Thread(futureTask);//4.創建Tread實例 19 thread.start();//5.調用Tread實例的start方法來開啟線程 20 for (int i = 0; i <100 ; i++) { 21 System.out.println("主線程"+i); 22 } 23 try { 24 System.out.println(futureTask.get());//獲取子線程的結果 25 } catch (InterruptedException e) { 26 e.printStackTrace(); 27 } catch (ExecutionException e) { 28 e.printStackTrace(); 29 } 30 } 31 }
為什么要創建FutureTask類的實例??
因為Thread接收Runnable類型的,但不接收Callable類型的,所以就需要一個轉換器,而FutureTask實現了Runnable接口,且構造方法支持Callable類型的。
FutureTask接口與Runnable接口的關系:
Future接口中常用的方法:
1.判斷任務是否完成:isDone()
2.能夠中斷任務:cance()
3.能夠獲取任務執行結果:get()
第四種:使用線程池
步驟:1.使用Executor獲取線程池對象
2.通過線程池對象獲取線程並執行MyRunnable()實例
1 import java.util.concurrent.ExecutorService; 2 import java.util.concurrent.Executors; 3 class MyRunnable implements Runnable{//創建MyRunnable()實例
4 5 @Override 6 public void run() { 7 for (int i = 0; i <10; i++) { 8 System.out.println("子線程。。。"+i); 9 } 10 } 11 } 12 public class TreadExtendDemo { 13 public static void main(String[] args) { 14 ExecutorService executorService=Executors.newFixedThreadPool(10);//1.使用Executor獲取線程池對象 15 executorService.execute(new MyRunnable());//2.通過線程池對象獲取線程並執行MyRunnable()實例
16 for (int i = 0; i <10 ; i++) { 17 System.out.println("main"+i); 18 } 19 } 20 }
第五種:使用匿名對象。
1 public class ThreadDemo { 2 public static void main(String[] args) { 3 Thread thread=new Thread(new Runnable() {//使用匿名方法創建線程 4 @Override 5 public void run() { 6 for (int i = 0; i <100 ; i++) { 7 System.out.println("子線程。。。。"+i); 8 } 9 } 10 }); 11 thread.start();//開啟線程 12 13 for (int i = 0; i <100 ; i++) { 14 System.out.println("主線程"+i); 15 } 16 } 17 }
面試題:Runnable和Callable接口比較
相同點:
- 兩者都是借口。
- 兩者都可用來編寫多線程程序。
- 兩者都需要調用Tread.start()方法啟動線程。
不同點:
- 實現Callable接口的線程能夠返回執行結果,而實現Runnable接口的線程不能返回結果;
- Callable接口的call()方法允許拋出異常,而Runnable接口的run()方法的不允許拋異常;
- 實現Callable接口的線程可以調用Future.cancel()取消執行,也可以調用isDone()來判斷線程是否執行完,也就是說可以實現對線程的監控,而實現Runnable接口的線程不能;
注意點:Callable接口支持返回執行結果,但需要調用FutureTask.get()方法實現,此方法會阻塞主線程直到獲取‘將來’結果;當不調用此方法時,主線程不會阻塞。
三,結束線程的方法--(♦使用通知方式)
1.stop()已過時
2.使用Interrupt方法中斷線程。
注意點:我一開始看到該方法的時候,認為interrupt會使線程停止運行,但事實上並非如此,調用一個線程的Interrupt方法會把線程的狀態改為中斷態。這其中又可以細分成兩個方面:
1)對於因執行了sleep、wait、join方法而休眠的線程:調用Interrupt方法會使他們不再休眠,同時會拋出 InterruptedException異常。比如一個線程A正在sleep中,這時候另外一個程序里去調用A的interrupt方法,這時就會迫使A停止休眠而拋出InterruptedException異常,從而提前使線程逃離阻塞狀態。
2)對於正在運行的線程,即沒有阻塞的線程,調用Interrupt方法就只是把線程A的狀態改為interruptted,也就是將中斷標志改為true,但是不會影響線程A的繼續執行。因為一個線程不應該由其他線程來強制中斷或自行停止,會造成線程不安全,所以說interrupt()並不能將真正的中斷線程,還需要被中斷的線程進行配合才行,也就是說一個有中斷需求的線程,在執行過程中會不斷的檢查自己的中斷標志位,如果被設置了中斷標志,就自行停止線程。
3.采用通知的方法(標志)。
當線程完成執行並結束后,就無法再次運行了。應該通過使用標志來指示run方法退出的方式來停止線程,即通知方式。該方式可通過線程以安全的方式結束運行。
【代碼演示】:在main方法中啟動兩個線程,第一個線程循環隨機打印100以內的整數,直到第二個線程從鍵盤中讀取了“over”命令。
1 import java.util.Random; 2 import java.util.Scanner; 3 class TraddDemo_1 implements Runnable{ 4 //第一個線程隨機打印100以內的整數 5 boolean flag=true; 6 @Override 7 public void run() { 8 while (flag){ 9 System.out.println(flag); 10 try{ 11 Thread.sleep(3000);//為搶占cpu執行權 12 }catch (Exception e){ } 13 Random in=new Random(); 14 int number=in.nextInt(100)+1;//隨機打印1-100之間的數字 15 System.out.println("隨機數字:"+number); 16 } 17 } 18 public void setFlag(boolean flag){ 19 this.flag=flag; 20 } 21 } 22 class TreadDemo_2 implements Runnable{//因為兩個線程做的事不同,所以要重寫一個線程 23 TraddDemo_1 t; 24 public TreadDemo_2(TraddDemo_1 t){ 25 this.t=t;//變量t引用的是TreadDemo_1的地址 26 } 27 @Override 28 public void run() { 29 Scanner in=new Scanner(System.in); 30 while (true){ 31 System.out.println("請輸入終止符:"); 32 String str=in.nextLine(); 33 if(str.equals("over")){ 34 t.setFlag(false); 35 break; 36 }else { 37 System.out.println("您輸入的終止符不正確!"); 38 } 39 } 40 } 41 } 42 public class ThreadStopDemo { 43 public static void main(String[] args) { 44 TraddDemo_1 traddDemo1=new TraddDemo_1(); 45 TreadDemo_2 treadDemo2=new TreadDemo_2(traddDemo1);//將引用傳遞給TreadDemo_2 46 new Thread(traddDemo1).start();//創建線程,並啟動 47 new Thread(treadDemo2).start(); 48 } 49 }
運行結果:
四,線程的常見方法
yield()方法:暫停當前正在執行的線程對象,會讓當前線程由“運行狀態”進入到“就緒狀態”,並且讓步於其他相同或優先級更高的線程執行,如果沒有優先級高於當前線程的線程,則當前線程會繼續執行。注意不能確定暫停的時間,且即使釋放CPU執行權也不會釋放鎖。
join()方法:若在當前線程中調用B線程的join方法,則當前線程會讓B線程插隊在自己面前執行,如果B線程搶到了cpu執行權,則B線程肯定會執行完,是一個串行的過程。注B線程有可能沒有也沒有搶到執行權。
join(long millis)方法:當前線程會讓B線程插隊在自己面前執行,但只等待millis秒。
join(long millis,int nanos)方法:同join(long millis)方法,因為在底層還是將nanos四舍五入為millis.
注意:調用join會釋放鎖,因為在join方法里調用了wait方法。
interrupt()方法:中斷操作。
1)對於因執行了sleep、wait、join方法而休眠的線程,調用Interrupt方法會使他們不再休眠,同時會拋出 InterruptedException異常。比如一個線程A正在sleep中,這時候在另外一個線程里去調用A的interrupt方法,這時就會迫使A停止休眠從而提前使線程逃離阻塞狀態,並且會拋出InterruptedException異常,。
2)對於正在運行的線程,即沒有阻塞的線程,調用Interrupt方法就只是把線程A的狀態改為interruptted,也就是將中斷標志改為true,但是不會影響線程A的繼續執行,直到調用了使線程進入阻塞狀態的sleep,join,wait方法時才會起到作用,先拋出InterruptedException異常,然后使線程立即跳出阻塞狀態。
isinterrupted()方法:判斷當前線程是否發生了中斷操作,true 已發生中斷,false 未發生中斷操作。
sleep()方法:線程休眠。哪個線程調用就讓哪個線程休眠。
sleep(long millis)
sleep(long millis,int nanos) 都是提供休眠操作。
sleep休眠期間,會讓出CPU使用權,但線程仍然持有鎖。
sleep休眠時間到了之后,不會立即執行,而是線程由“阻塞狀態”進入“就緒狀態”。
setDaemon(boolean n)方法:設置守護線程(true)或用戶線程(false即默認的)。
用戶線程(非守護線程):線程的任務體正常執行完。
后台線程(守護線程):服務於用戶線程,所有用戶線程執行結束,哪怕守護線程的任務體還沒有執行完畢,也會伴隨着結束,比如:垃圾回收機制。
守護線程的生命周期:
守護線程的生命周期是依賴於用戶線程,當有用戶線程存在,守護線程就會存在,當沒有用戶線程存在,那守護線程也會隨之消亡。需要注意的是:Java虛擬機在“用戶線程”都結束后是會退出。
Priority優先級:線程的優先級
線程的優先級:就是來指導線程都執行優先級。
方法介紹:
int getPriority() 獲取優先級
setPriority() 設置優先級
方法特點:
1.Java線程的優先級並不絕對,它所控制的是執行的機會,也就是說,優先級高的線程執行效率比較大,而優先級低的也並不是沒有機會,只是執行的概率相對低一些。
2.Java一共有10個優先級,分別為1-10,數值越大,表明優先級越高,一個普通的線程,其優先級為5;線程的優先級具有繼承性,如果一個線程B是在另一個線程A中創建的,則B叫做A的子線程,B的初始優先級與A保持一致。
3.優先級范圍:Java中的優先級的范圍是1-10。最小值為1,默認的是5,最大值為10。“優先級高的會優先於低的先執行”。
【代碼演示】:join方法。
1 class B extends Thread{ 2 @Override 3 public void run() { 4 for (int i = 0; i <4 ; i++) { 5 System.out.println("B正在使用電腦"); 6 } 7 } 8 } 9 public class ThreadJoinDemo { 10 public static void main(String[] args) { 11 B b=new B(); 12 b.start(); 13 for (int i = 0; i <5; i++) { 14 System.out.println("A正在使用電腦"); 15 if(i>=3){ 16 try { 17 b.join();//調用B的join方法后,如果B搶到了執行權則一定會運行完代碼。 18 } catch (Exception e) { 19 e.printStackTrace(); 20 } 21 } 22 } 23 } 24 }
運行結果:
【代碼演示】:對正在運行的代碼調用interrupt方法。
1 public class TestDemo { 2 public static void main(String[] args) { 3 Thread thread=new Thread(new Runnable() { 4 @Override 5 public void run() { 6 int n=0; 7 while (true){ 8 n++; 9 System.out.println("子線程"+n); 10 if(n==100){ 11 System.out.println("休眠"); 12 try { 13 Thread.sleep(100000); 14 } catch (InterruptedException e) { 15 e.printStackTrace(); 16 } 17 } 18 } 19 20 } 21 }); 22 thread.start(); 23 thread.interrupt(); 24 25 } 26 }
運行結果:
通過結果可知,當線程正在運行這時調用了interrupt方法代碼確實沒有什么變化,直到調用了sleep方法時,我們可以明顯發現線程並沒有進入睡眠狀態,而是繼續運行,並且報出來sleep interrupted異常。
【代碼演示】:setDaemon方法
1 class Waiter extends Thread{ 2 @Override 3 public void run() { 4 while(true) {//使用while語句,直到主線程運行結束,該線程才停止。 5 System.out.println("服務員正在工作"); 6 } 7 } 8 } 9 public class TreadSetDaemonDemo { 10 public static void main(String[] args) { 11 Waiter waiter=new Waiter(); 12 waiter.setDaemon(true); 13 waiter.start(); 14 for (int i = 0; i <200 ; i++) { 15 System.out.println("餐廳還在營業!"); 16 } 17 } 18 }
運行結果:
只有當餐廳停止營業,服務員才停止工作。這里之所以后面還打印了3個“服務員正在工作”,是因為打印是單線程的,當餐廳停止營業時,雖然服務員也停止服務了,但是打印里面還緩存了一部分未打印完。
1、 總結wait()和sleep()方法之間的區別和聯系
從表面上看,wait()和sleep()都可以使得當前線程進入阻塞狀態,但是兩者之間存在本質性的差別,下面總結兩者的區別和相似之處:
1) wait()和sleep()都可以使得線程進入阻塞狀態
2) wait()和sleep()都是可中斷方法,被中斷之后都會收到中斷異常
3) wait()是Object類中的方法,由於wait()調用必須在一個synchronized方法/塊中,調用之前需要先獲取對象的monitor,每個對象都有自己的monitor,讓當前線程等待當前對象的monitor,當然需要當前對象來操作,所以wait()方法就必須要定義在Object類中, 而sleep()是Thread特有的方法
4) wait()方法執行必須在同步方法/同步塊中,而sleep()不需要
5) 線程在同步方法中執行sleep()時,並不會釋放掉monitor的鎖,而wait()會釋放掉
6) Sleep()短暫休眠之后會主動退出阻塞,而wait()(沒有指定時間)則需要被其他線程中斷后/其他線程喚醒並獲取到當前對象的monitor后才能退出阻塞
2、 interrupted()和isInterrupted()有什么區別?寫一個簡單的示例來驗證
1) isIntereripted是Thread的一個成員方法,它主要判斷當前線程是否被中斷,該方法僅僅是對interrupt標識的一個判斷,並不影響標識發生任何改變。
2) interrupted是一個靜態方法,雖然其也用於判斷當前線程是否被中斷,但是調用該方法會直接擦除掉線程的interrupt標識,要注意的是,如果當前線程被打斷了,那么第一次調用interrupted方法會返回true,並且立即擦除了interrupt標識;第二次包括以后的調用永遠都會返回false。
3、 什么是守護線程?為什么會有守護線程?什么時候需要守護線程?
要回答這些問題,我們必須先搞清楚另外一個比較重要的問題:JVM程序在什么情況下會退出?
來自JDK官方文檔:The java virtual machine exits when the only threads running are all daemon threads.這句話指的是正常退出的情況,而不是調用了System.exit()方法,通過這句話的描述,我們不難發現,在正常情況下, 若JVM總沒有一個非守護線程,則JVM的進程會退出。
如果一個JVM進程中都是守護線程(即沒有一個非守護線程存在),那么JVM這一進程在某一程序結束的時候也會退出,也就是說守護線程具備自動結束生命周期的特性,而非守護線程則不具備這一特點。假設JVM進程的垃圾回收線程是非守護線程,那么某一程序完成工作結束,則JVM無法退出,因為垃圾回收線程還在正常的工作。
守護線程經常用做執行一些后台任務,因此有時也被稱之為后台線程,當你希望關閉某些線程的時候,或者退出JVM進程的時候,一些線程能夠自動關閉,此時就可以考慮使用守護線程為你完成這樣的工作。
五.線程的生命周期
當線程被創建並啟動以后,它既不是一啟動就進入了執行狀態,也不是一直處於執行狀態。在線程的生命周期中,它要經過新建(New)、就緒(Runnable)、運行(Running)、阻塞(Blocked)和死亡(Dead)5種狀態。尤其是當線程啟動以后,它不可能一直"霸占"着CPU獨自運行,所以CPU需要在多條線程之間切換,於是線程狀態也會多次在運行、阻塞之間切換。
要想實現多線程,必須在主線程中創建新的線程對象。Java語言使用Thread類及其子類的對象來表示線程,在它的一個完整的生命周期中通常要經歷如下的五種狀態:
1.新建:當一個Thread類或其子類的對象被聲明並創建時,新生的線程對象處於新建狀態。(jvm為線程分配內存,初始化成員變量)
2.就緒:處於新建狀態的線程被start()后,將進入線程隊列等待CPU時間片,此時它已具備運行條件。(jvm為線程創建方法棧和程序計數器,等待線程調度器調度)
3.運行:當就緒的線程被調度並獲得處理器資源時,便進入運行狀態,run()方法定義了線程的操作和功能。
4.阻塞:在某種特殊情況下,被人掛起或執行輸入輸出操作時,讓出cpu,並臨時終止自己的執行,進入阻塞狀態。
5.死亡:線程完成了它的全部工作或線程被提前強制性的終止。
1、新建和就緒狀態
當程序使用new關鍵字創建了一個線程之后,該線程就處於新建狀態,此時它和其他的Java對象一樣,僅僅由Java虛擬機為其分配內存,並初始化其成員變量的值。此時的線程對象沒有表現出任何線程的動態特征,程序也不會執行線程的線程執行體。當線程對象調用了start()方法之后,該線程處於就緒狀態。Java虛擬機會為其創建方法調用棧和程序計數器,處於這個狀態中的線程並沒有開始運行,只是表示該線程可以運行了。至於該線程何時開始運行,取決於JVM里線程調度器的調度。
調用線程對象的start()方法之后,該線程立即進入就緒狀態——就緒狀態相當於"等待執行",但該線程並未真正進入運行狀態。如果希望調用子線程的start()方法后子線程立即開始執行,程序可以使用Thread.sleep(1) 來讓當前運行的線程(主線程)睡眠1毫秒,1毫秒就夠了,因為在這1毫秒內CPU不會空閑,它會去執行另一個處於就緒狀態的線程,這樣就可以讓子線程立即開始執行。
2、運行和阻塞狀態
2.1 線程運行狀態
如果處於就緒狀態的線程獲得了CPU,開始執行run()方法的線程執行體,則該線程處於運行狀態,如果計算機只有一個CPU。那么在任何時刻只有一個線程處於運行狀態,當然在一個多處理器的機器上,將會有多個線程並行執行;當線程數大於處理器數時,依然會存在多個線程在同一個CPU上輪換的現象。
當一個線程開始運行后,它不可能一直處於運行狀態(除非它的線程執行體足夠短,瞬間就執行結束了)。線程在運行過程中需要被中斷,目的是使其他線程獲得執行的機會,線程調度的細節取決於底層平台所采用的策略。對於采用搶占式策略的系統而言,系統會給每個可執行的線程一個小時間段來處理任務;當該時間段用完后,系統就會剝奪該線程所占用的資源,讓其他線程獲得執行的機會。在選擇下一個線程時,系統會考慮線程的優先級。所有現代的桌面和服務器操作系統都采用搶占式調度策略,但一些小型設備如手機則可能采用協作式調度策略,在這樣的系統中,只有當一個線程調用了它的sleep()或yield()方法后才會放棄所占用的資源——也就是必須由該線程主動放棄所占用的資源。
2.2 線程阻塞狀態
當發生如下情況時,線程將會進入阻塞狀態
① 線程調用sleep()方法主動放棄所占用的處理器資源
② 線程調用了一個阻塞式IO方法,在該方法返回之前,該線程被阻塞
③ 線程試圖獲得一個同步監視器,但該同步監視器正被其他線程所持有。關於同步監視器的知識、后面將存更深入的介紹
④ 線程在等待某個通知(notify)
⑤ 程序調用了線程的suspend()方法將該線程掛起。但這個方法容易導致死鎖,所以應該盡量避免使用該方法
當前正在執行的線程被阻塞之后,其他線程就可以獲得執行的機會。被阻塞的線程會在合適的時候重新進入就緒狀態,注意是就緒狀態而不是運行狀態。也就是說,被阻塞線程的阻塞解除后,必須重新等待線程調度器再次調度它。
2.3 解除阻塞
針對上面幾種情況,當發生如下特定的情況時可以解除上面的阻塞,讓該線程重新進入就緒狀態:
① 調用sleep()方法的線程經過了指定時間。
② 線程調用的阻塞式IO方法已經返回。
③ 線程成功地獲得了試圖取得的同步監視器。
④ 線程正在等待某個通知時,其他線程發出了個通知。
⑤ 處於掛起狀態的線程被調甩了resdme()恢復方法。
3. 死亡狀態
線程會以如下3種方式結束,結束后就處於死亡狀態:
① run()或call()方法執行完成,線程正常結束。
② 線程拋出一個未捕獲的Exception或Error。
③ 直接調用該線程stop()方法來結束該線程——該方法容易導致死鎖,通常不推薦使用。