Java 多線程(創建,結束,生命周期,常見方法)


想要學習多線程,必須要先理解什么是並發什么是並行。

  並行:是指兩個或多個線程在同一時刻發生。

  並發:是指兩個或多個線程在同一時間段內發生。

為了方便理解多線程的概念,我們先舉一個例子:

     假如我們把公司看做是一個進程,那么人就是其中的線程。進程必須得有一個主線程,公司在創業初期往往可能出現一人打天下的現象,但是,至少得有一個人,公司才能運作。公司創業初期,業務還不算太多,往往就是老板一個人身兼數職,一天如果只有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()方法來結束該線程——該方法容易導致死鎖,通常不推薦使用。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM