Java線程詳解(深度好文)

Java線程:概念與原理

1、進程與線程

        進程是指一個內存中運行的應用程序,每一個進程都有本身獨立的一塊內存空間,即進程空間或(虛空間)。進程不依賴於線程而獨立存在,一個進程中能夠啓動多個線程。好比在Windows系統中,一個運行的exe就是一個進程。前端

        線程是指進程中的一個執行流程,一個進程中能夠運行多個線程。好比java.exe進程中能夠運行不少線程。線程老是屬於某個進程,線程沒有本身的虛擬地址空間,與進程內的其餘線程一塊兒共享分配給該進程的全部資源。java

        「同時」執行是人的感受,在線程之間實際上輪換執行。程序員

        進程在執行過程當中擁有獨立的內存單元,進程有獨立的地址空間,而多個線程共享內存,從而極大地提升了程序的運行效率。數據庫

        線程在執行過程當中與進程仍是有區別的。每一個獨立的線程有一個程序運行的入口、順序執行序列和程序的出口。可是線程不可以獨立執行,必須依存在應用程序中,由應用程序提供多個線程執行控制。編程

        進程是具備必定獨立功能的程序關於某個數據集合上的一次運行活動,進程是系統進行資源分配和調度的一個獨立單位安全

        線程是進程的一個實體,是CPU調度和分派的基本單位,它是比進程更小的能獨立運行的基本單位。線程本身基本上不擁有系統資源,只擁有一點在運行中必不可少的資源(如程序計數器,一組寄存器和棧),可是它可與同屬一個進程的其餘的線程共享進程所擁有的所有資源。多線程

        線程有本身的堆棧和局部變量,但線程之間沒有單獨的地址空間,一個線程包含如下內容:併發

  •  一個指向當前被執行指令的指令指針;
  • 一個棧;
  • 一個寄存器值的集合,定義了一部分描述正在執行線程的處理器狀態的值
  • 一個私有的數據區。

        咱們使用Join()方法掛起當前線程,直到調用Join()方法的線程執行完畢。該方法還存在包含參數的重載版本,其中的參數用於指定等待線程結束的最長時間(即超時)所花費的毫秒數。若是線程中的工做在規定的超時時段內結束,該版本的Join()方法將返回一個布爾量True。編程語言

        簡而言之:ide

  • 一個程序至少有一個進程,一個進程至少有一個線程。
  • 線程的劃分尺度小於進程,使得多進程程序的併發性高。
  • 另外,進程在執行過程當中擁有獨立的內存單元,而多個線程共享內存,從而極大地提升了程序的運行效率。
  • 線程在執行過程當中與進程仍是有區別的。每一個獨立的線程有一個程序運行的入口、順序執行序列和程序的出口。可是線程不可以獨立執行,必須依存在應用程序中,由應用程序提供多個線程執行控制。
  • 從邏輯角度來看,多線程的意義在於一個應用程序中,有多個執行部分能夠同時執行。但操做系統並無將多個線程看作多個獨立的應用,來實現進程的調度和管理以及資源分配。這就是進程和線程的重要區別。

         在Java中,每次程序運行至少啓動2個線程:一個是main線程,一個是垃圾收集線程。由於每當使用java命令執行一個類的時候,實際上都會啓動一個JVM,每個JVM實際上就是在操做系統中啓動了一個進程。

2、Java中的線程

        在Java中,「線程」指兩件不一樣的事情:

        一、java.lang.Thread類的一個實例;

        二、線程的執行。

        在 Java程序中,有兩種方法建立線程:

        一是對 Thread 類進行派生並覆蓋 run方法;

        二是經過實現Runnable接口建立。

        使用java.lang.Thread類或者java.lang.Runnable接口編寫代碼來定義、實例化和啓動新線程。

        一個Thread類實例只是一個對象,像Java中的任何其餘對象同樣,具備變量和方法,生死於堆上。

        Java中,每一個線程都有一個調用棧,即便不在程序中建立任何新的線程,線程也在後臺運行着。

        一個Java應用老是從main()方法開始運行,main()方法運行在一個線程內,他被稱爲主線程。

        一旦建立一個新的線程,就產生一個新的調用棧。

        線程整體分兩類:用戶線程和守候線程。

        當全部用戶線程執行完畢的時候,JVM自動關閉。可是守候線程卻不獨立於JVM,守候線程通常是由操做系統或者用戶本身建立的。

Java線程:建立與啓動

1、定義線程

        一、擴展java.lang.Thread類。

        此類中有個run()方法,應該注意其用法:public void run()

        若是該線程是使用獨立的Runnable運行對象構造的,則調用該Runnable對象的run方法;不然,該方法不執行任何操做並返回。

        Thread的子類應該重寫該方法。

二、實現java.lang.Runnable接口。

        void run()

        使用實現接口Runnable的對象建立一個線程時,啓動該線程將致使在獨立執行的線程中調用對象的run方法。

        方法run的常規協定是,它可能執行任何所需的操做。

2、實例化線程

        一、若是是擴展java.lang.Thread類的線程,則直接new便可。

        二、若是是實現了java.lang.Runnable接口的類,則用Thread的構造方法:

[java]  view plain  copy
  1. Thread(Runnabletarget)  
  2. Thread(Runnabletarget, String name)  
  3. Thread(ThreadGroupgroup, Runnable target)  
  4. Thread(ThreadGroupgroup, Runnable target, String name)  
  5. Thread(ThreadGroupgroup, Runnable target, String name, long stackSize)  

        其中:

        Runnable target:實現了Runnable接口的類的實例。 

  1. Thread類也實現了Runnable接口,所以,從Thread類繼承的類的實例也能夠做爲target傳入這個構造方法。
  2. 直接實現Runnable接口類的實例。
  3. 線程池創建多線程。

        String name:線程的名子。這個名子能夠在創建Thread實例後經過Thread類的setName方法設置。默認線程名:Thread-N,N是線程創建的順序,是一個不重複的正整數。

        ThreadGroup group:當前創建的線程所屬的線程組。若是不指定線程組,全部的線程都被加到一個默認的線程組中。

        long stackSize:線程棧的大小,這個值通常是CPU頁面的整數倍。如x86的頁面大小是4KB.在x86平臺下,默認的線程棧大小是12KB。

3、啓動線程

        在線程的Thread對象上調用start()方法,而不是run()或者別的方法。

        在調用start()方法以前:線程處於新狀態中,新狀態指有一個Thread對象,但尚未一個真正的線程。

        在調用start()方法以後:發生了一系列複雜的事情——

        啓動新的執行線程(具備新的調用棧);

        該線程重新狀態轉移到可運行狀態;

        當該線程得到機會執行時,其目標run()方法將運行。

        注意:對Java來講,run()方法沒有任何特別之處。像main()方法同樣,它只是新線程知道調用的方法名稱(和簽名)。所以,在Runnable上或者Thread上調用run方法是合法的。但並不啓動新的線程。

4、例子

        一、實現Runnable接口的多線程例子

[java]  view plain  copy
  1. /** 
  2.  * 實現Runnable接口的類 
  3.  */  
  4. public class RunnableImpl implements Runnable{  
  5.     private Stringname;  
  6.     public RunnableImpl(String name) {  
  7.        this.name = name;  
  8.     }  
  9.     @Override  
  10.     public void run() {  
  11.        for (int i = 0; i < 5; i++) {  
  12.            for(long k=0;k<100000000;k++);  
  13.            System.out.println(name+":"+i);  
  14.        }       
  15.     }  
  16. }  
  17.    
  18. /** 
  19.  * 測試Runnable類實現的多線程程序 
  20.  */  
  21. public class TestRunnable {  
  22.    
  23.     public static void main(String[] args) {  
  24.        RunnableImpl ri1=new RunnableImpl("李白");  
  25.        RunnableImpl ri2=new RunnableImpl("屈原");  
  26.        Thread t1=new Thread(ri1);  
  27.        Thread t2=new Thread(ri2);  
  28.        t1.start();  
  29.        t2.start();  
  30.     }  
  31. }  

        執行結果:

[java]  view plain  copy
  1. 屈原:0  
  2. 李白:0  
  3. 屈原:1  
  4. 李白:1  
  5. 屈原:2  
  6. 李白:2  
  7. 李白:3  
  8. 屈原:3  
  9. 李白:4  
  10. 屈原:4  

        二、擴展Thread類實現的多線程例子

[java]  view plain  copy
  1. /** 
  2.  * 測試擴展Thread類實現的多線程程序 
  3.  */  
  4. public class TestThread extends Thread {  
  5.     public TestThread(String name){  
  6.        super(name);  
  7.     }  
  8.     @Override  
  9.     public void run() {  
  10.        for(int i=0;i<5;i++){  
  11.            for(long k=0;k<100000000;k++);  
  12.            System.out.println(this.getName()+":"+i);  
  13.        }  
  14.     }  
  15.     public static void main(String[] args){  
  16.        Thread t1=new TestThread("李白");  
  17.        Thread t2=new TestThread("屈原");  
  18.        t1.start();  
  19.        t2.start();        
  20.     }  
  21. }  
        執行結果:

[java]  view plain  copy
  1. 屈原:0  
  2. 李白:0  
  3. 屈原:1  
  4. 李白:1  
  5. 屈原:2  
  6. 李白:2  
  7. 屈原:3  
  8. 屈原:4  
  9. 李白:3  
  10. 李白:4  

        對於上面的多線程程序代碼來講,輸出的結果是不肯定的。其中的一條語句for(long k=0;k<100000000;k++);是用來模擬一個很是耗時的操做的。

5、一些常見問題

        一、線程的名字,一個運行中的線程老是有名字的,名字有兩個來源,一個是虛擬機本身給的名字,一個是你本身的定的名字。在沒有指定線程名字的狀況下,虛擬機總會爲線程指定名字,而且主線程的名字老是mian,非主線程的名字不肯定。

        二、線程均可以設置名字,也能夠獲取線程的名字,連主線程也不例外。

        三、獲取當前線程的對象的方法是:Thread.currentThread();

        四、在上面的代碼中,只能保證:每一個線程都將啓動,每一個線程都將運行直到完成。一系列線程以某種順序啓動並不意味着將按該順序執行。對於任何一組啓動的線程來講,調度程序不能保證其執行次序,持續時間也沒法保證。

        五、當線程目標run()方法結束時該線程完成。

        六、一旦線程啓動,它就永遠不能再從新啓動。只有一個新的線程能夠被啓動,而且只能一次。一個可運行的線程或死線程能夠被從新啓動。

        七、線程的調度是JVM的一部分,在一個CPU的機器上上,實際上一次只能運行一個線程。一次只有一個線程棧執行。JVM線程調度程序決定實際運行哪一個處於可運行狀態的線程。

        衆多可運行線程中的某一個會被選中作爲當前線程。可運行線程被選擇運行的順序是沒有保障的。

        八、儘管一般採用隊列形式,但這是沒有保障的。隊列形式是指當一個線程完成「一輪」時,它移到可運行隊列的尾部等待,直到它最終排隊到該隊列的前端爲止,它才能被再次選中。事實上,咱們把它稱爲可運行池而不是一個可運行隊列,目的是幫助認識線程並不都是以某種有保障的順序排列而成一個一個隊列的事實。

        九、儘管咱們沒有沒法控制線程調度程序,但能夠經過別的方式來影響線程調度的方式。

Java線程:線程棧模型與線程的變量

        要理解線程調度的原理,以及線程執行過程,必須理解線程棧模型。

        線程棧是指某時刻時內存中線程調度的棧信息,當前調用的方法老是位於棧頂。線程棧的內容是隨着程序的運行動態變化的,所以研究線程棧必須選擇一個運行的時刻(實際上指代碼運行到什麼地方)。

        下面經過一個示例性的代碼說明線程(調用)棧的變化過程。

 

        這幅圖描述在代碼執行到兩個不一樣時刻一、2時候,虛擬機線程調用棧示意圖。

        當程序執行到t.start();時候,程序多出一個分支(增長了一個調用棧B),這樣,棧A、棧B並行執行。

        從這裏就能夠看出方法調用和線程啓動的區別了。

Java線程:線程狀態的轉換

1、線程狀態

        線程的狀態轉換是線程控制的基礎。線程狀態總的能夠分爲五大狀態。用一個圖來描述以下:

        一、新狀態:線程對象已經建立,尚未在其上調用start()方法。

        二、可運行狀態:當線程有資格運行,但調度程序尚未把它選定爲運行線程時線程所處的狀態。當start()方法調用時,線程首先進入可運行狀態。在線程運行以後或者從阻塞、等待或睡眠狀態回來後,也返回到可運行狀態。

        三、運行狀態:線程調度程序從可運行池中選擇一個線程做爲當前線程時線程所處的狀態。這也是線程進入運行狀態的惟一一種方式。

        四、等待/阻塞/睡眠狀態:這是線程有資格運行時它所處的狀態。實際上這個三狀態組合爲一種,其共同點是:線程仍舊是活的,可是當前沒有條件運行。換句話說,它是可運行的,可是若是某件事件出現,他可能返回到可運行狀態。

        五、死亡態:當線程的run()方法完成時就認爲它死去。這個線程對象也許是活的,可是,它已經不是一個單獨執行的線程。線程一旦死亡,就不能復生。若是在一個死去的線程上調用start()方法,會拋出java.lang.IllegalThreadStateException異常。

2、阻止線程執行

        對於線程的阻止,考慮一下三個方面,不考慮IO阻塞的狀況:

        睡眠;

        等待;

        由於須要一個對象的鎖定而被阻塞。

        一、睡眠

        Thread.sleep(longmillis)和Thread.sleep(long millis, int nanos)靜態方法強制當前正在執行的線程休眠(暫停執行),以「減慢線程」。當線程睡眠時,它入睡在某個地方,在甦醒以前不會返回到可運行狀態。當睡眠時間到期,則返回到可運行狀態。

        線程睡眠的緣由:線程執行太快,或者須要強制進入下一輪,由於Java規範不保證合理的輪換。

        睡眠的實現:調用靜態方法。       

[java]  view plain  copy
  1. try {  
  2.            Thread.sleep(123);  
  3.        } catch (InterruptedException e) {  
  4.            e.printStackTrace();   
  5.        }  

        睡眠的位置:爲了讓其餘線程有機會執行,能夠將Thread.sleep()的調用放線程run()以內。這樣才能保證該線程執行過程當中會睡眠。

        例如,在前面的例子中,將一個耗時的操做改成睡眠,以減慢線程的執行。能夠這麼寫:

[java]  view plain  copy
  1. for(int i=0;i<5;i++){  
  2.            // 很耗時的操做,用來減慢線程的執行  
  3.            //for(longk=0;k<100000000;k++);  
  4.            try {  
  5.                 Thread.sleep(3);  
  6.             } catch (InterruptedException e) {  
  7.                 e.printStackTrace();  
  8.             }  
  9.            System.out.println(this.getName()+":"+i);  
  10.     }  
        執行結果:

[java]  view plain  copy
  1. 李白:0  
  2. 李白:1  
  3. 屈原:0  
  4. 李白:2  
  5. 屈原:1  
  6. 李白:3  
  7. 屈原:2  
  8. 李白:4  
  9. 屈原:3  
  10. 屈原:4  

        這樣,線程在每次執行過程當中,總會睡眠3毫秒,睡眠了,其餘的線程就有機會執行了。

        注意:

        一、線程睡眠是幫助全部線程得到運行機會的最好方法。

        二、線程睡眠到期自動甦醒,並返回到可運行狀態,不是運行狀態。sleep()中指定的時間是線程不會運行的最短期。所以,sleep()方法不能保證該線程睡眠到期後就開始執行。

        三、sleep()是靜態方法,只能控制當前正在運行的線程。

        下面給個例子:

[java]  view plain  copy
  1. /** 
  2.  * 一個計數器,計數到100,在每一個數字之間暫停1秒,每隔10個數字輸出一個字符串 
  3.  */  
  4. public class CalcThread extends Thread {  
  5.     public void run(){  
  6.        for(int i=0;i<100;i++){  
  7.            if ((i)%10==0) {  
  8.               System.out.println("--------"+i);  
  9.            }  
  10.            System.out.print(i);  
  11.            try {  
  12.               Thread.sleep(1);  
  13.               System.out.print("    線程睡眠1毫秒!\n");  
  14.            } catch (InterruptedException e) {  
  15.               e.printStackTrace();  
  16.            }  
  17.        }  
  18.     }  
  19.    
  20.     public static void main(String[] args) {  
  21.        new CalcThread().start();  
  22.     }  
  23. }  

        執行結果:

[java]  view plain  copy
  1. --------0  
  2. 0    線程睡眠1毫秒!  
  3. 1    線程睡眠1毫秒!  
  4. 2    線程睡眠1毫秒!  
  5. 3    線程睡眠1毫秒!  
  6. 4    線程睡眠1毫秒!  
  7. 5    線程睡眠1毫秒!  
  8. 6    線程睡眠1毫秒!  
  9. 7    線程睡眠1毫秒!  
  10. 8    線程睡眠1毫秒!  
  11. 9    線程睡眠1毫秒!  
  12. --------10  
  13. 10    線程睡眠1毫秒!  
  14. 11    線程睡眠1毫秒!  
  15. 12    線程睡眠1毫秒!  
  16. 13    線程睡眠1毫秒!  
  17. 14    線程睡眠1毫秒!  
  18. 15    線程睡眠1毫秒!  
  19. 16    線程睡眠1毫秒!  
  20. 17    線程睡眠1毫秒!  
  21. 18    線程睡眠1毫秒!  
  22. 19    線程睡眠1毫秒!  
  23. --------20  
  24. 20    線程睡眠1毫秒!  
  25. 21    線程睡眠1毫秒!  
  26. 22    線程睡眠1毫秒!  
  27. 23    線程睡眠1毫秒!  
  28. 24    線程睡眠1毫秒!  
  29. 25    線程睡眠1毫秒!  
  30. 26    線程睡眠1毫秒!  
  31. 27    線程睡眠1毫秒!  
  32. 28    線程睡眠1毫秒!  
  33. 29    線程睡眠1毫秒!  
  34. --------30  
  35. 30    線程睡眠1毫秒!  
  36. 31    線程睡眠1毫秒!  
  37. 32    線程睡眠1毫秒!  
  38. 33    線程睡眠1毫秒!  
  39. 34    線程睡眠1毫秒!  
  40. 35    線程睡眠1毫秒!  
  41. 36    線程睡眠1毫秒!  
  42. 37    線程睡眠1毫秒!  
  43. 38    線程睡眠1毫秒!  
  44. 39    線程睡眠1毫秒!  
  45. --------40  
  46. 40    線程睡眠1毫秒!  
  47. 41    線程睡眠1毫秒!  
  48. 42    線程睡眠1毫秒!  
  49. 43    線程睡眠1毫秒!  
  50. 44    線程睡眠1毫秒!  
  51. 45    線程睡眠1毫秒!  
  52. 46    線程睡眠1毫秒!  
  53. 47    線程睡眠1毫秒!  
  54. 48    線程睡眠1毫秒!  
  55. 49    線程睡眠1毫秒!  
  56. --------50  
  57. 50    線程睡眠1毫秒!  
  58. 51    線程睡眠1毫秒!  
  59. 52    線程睡眠1毫秒!  
  60. 53    線程睡眠1毫秒!  
  61. 54    線程睡眠1毫秒!  
  62. 55    線程睡眠1毫秒!  
  63. 56    線程睡眠1毫秒!  
  64. 57    線程睡眠1毫秒!  
  65. 58    線程睡眠1毫秒!  
  66. 59    線程睡眠1毫秒!  
  67. --------60  
  68. 60    線程睡眠1毫秒!  
  69. 61    線程睡眠1毫秒!  
  70. 62    線程睡眠1毫秒!  
  71. 63    線程睡眠1毫秒!  
  72. 64    線程睡眠1毫秒!  
  73. 65    線程睡眠1毫秒!  
  74. 66    線程睡眠1毫秒!  
  75. 67    線程睡眠1毫秒!  
  76. 68    線程睡眠1毫秒!  
  77. 69    線程睡眠1毫秒!  
  78. --------70  
  79. 70    線程睡眠1毫秒!  
  80. 71    線程睡眠1毫秒!  
  81. 72    線程睡眠1毫秒!  
  82. 73    線程睡眠1毫秒!  
  83. 74    線程睡眠1毫秒!  
  84. 75    線程睡眠1毫秒!  
  85. 76    線程睡眠1毫秒!  
  86. 77    線程睡眠1毫秒!  
  87. 78    線程睡眠1毫秒!  
  88. 79    線程睡眠1毫秒!  
  89. --------80  
  90. 80    線程睡眠1毫秒!  
  91. 81    線程睡眠1毫秒!  
  92. 82    線程睡眠1毫秒!  
  93. 83    線程睡眠1毫秒!  
  94. 84    線程睡眠1毫秒!  
  95. 85    線程睡眠1毫秒!  
  96. 86    線程睡眠1毫秒!  
  97. 87    線程睡眠1毫秒!  
  98. 88    線程睡眠1毫秒!  
  99. 89    線程睡眠1毫秒!  
  100. --------90  
  101. 90    線程睡眠1毫秒!  
  102. 91    線程睡眠1毫秒!  
  103. 92    線程睡眠1毫秒!  
  104. 93    線程睡眠1毫秒!  
  105. 94    線程睡眠1毫秒!  
  106. 95    線程睡眠1毫秒!  
  107. 96    線程睡眠1毫秒!  
  108. 97    線程睡眠1毫秒!  
  109. 98    線程睡眠1毫秒!  
  110. 99    線程睡眠1毫秒!  

        二、線程的優先級和線程讓步yield()

        線程的讓步是經過Thread.yield()來實現的。yield()方法的做用是:暫停當前正在執行的線程對象,並執行其餘線程。

        要理解yield(),必須瞭解線程的優先級的概念。線程老是存在優先級,優先級範圍在1~10之間。JVM線程調度程序是基於優先級的搶先調度機制。在大多數狀況下,當前運行的線程優先級將大於或等於線程池中任何線程的優先級。但這僅僅是大多數狀況。

        注意:當設計多線程應用程序的時候,必定不要依賴於線程的優先級。由於線程調度優先級操做是沒有保障的,只能把線程優先級做用做爲一種提升程序效率的方法,可是要保證程序不依賴這種操做。

        當線程池中線程都具備相同的優先級,調度程序的JVM實現自由選擇它喜歡的線程。這時候調度程序的操做有兩種可能:一是選擇一個線程運行,直到它阻塞或者運行完成爲止。二是時間分片,爲池內的每一個線程提供均等的運行機會。

        設置線程的優先級:線程默認的優先級是建立它的執行線程的優先級。能夠經過setPriority(int newPriority)更改線程的優先級。例如:

[java]  view plain  copy
  1. Thread t = new MyThread();  
  2. t.setPriority(8);  
  3. t.start();  

        線程優先級爲1~10之間的正整數,JVM從不會改變一個線程的優先級。然而,1~10之間的值是沒有保證的。一些JVM可能不能識別10個不一樣的值,而將這些優先級進行每兩個或多個合併,變成少於10個的優先級,則兩個或多個優先級的線程可能被映射爲一個優先級。

        線程默認優先級是5,Thread類中有三個常量,定義線程優先級範圍:

[java]  view plain  copy
  1. static intMAX_PRIORITY:線程能夠具備的最高優先級。  
  2. static intMIN_PRIORITY:線程能夠具備的最低優先級。  
  3. static intNORM_PRIORITY:分配給線程的默認優先級。  

        三、Thread.yield()方法

        Thread.yield()方法做用是:暫停當前正在執行的線程對象,並執行其餘線程。

         yield()應該作的是讓當前運行線程回到可運行狀態,以容許具備相同優先級的其餘線程得到運行機會。所以,使用yield()的目的是讓相同優先級的線程之間能適當的輪轉執行。可是,實際中沒法保證yield()達到讓步目的,由於讓步的線程還有可能被線程調度程序再次選中。

        結論:yield()從未致使線程轉到等待/睡眠/阻塞狀態。在大多數狀況下,yield()將致使線程從運行狀態轉到可運行狀態,但有可能沒有效果。

        四、join()方法

        Thread的非靜態方法join()讓一個線程B「加入」到另一個線程A的尾部。在A執行完畢以前,B不能工做。例如:

[java]  view plain  copy
  1. Thread t = new MyThread();  
  2. t.start();  
  3. t.join();  

        另外,join()方法還有帶超時限制的重載版本。例如t.join(5000);則讓線程等待5000毫秒,若是超過這個時間,則中止等待,變爲可運行狀態。

線程的加入join()對線程棧致使的結果是線程棧發生了變化,固然這些變化都是瞬時的。下面給示意圖:

        小結

        到目前位置,介紹了線程離開運行狀態的3種方法:

        一、調用Thread.sleep():使當前線程睡眠至少多少毫秒(儘管它可能在指定的時間以前被中斷)。

        二、調用Thread.yield():不能保障太多事情,儘管一般它會讓當前運行線程回到可運行性狀態,使得有相同優先級的線程有機會執行。

        三、調用join()方法:保證當前線程中止執行,直到該線程所加入的線程完成爲止。然而,若是它加入的線程沒有存活,則當前線程不須要中止。

        除了以上三種方式外,還有下面幾種特殊狀況可能使線程離開運行狀態:

        一、線程的run()方法完成。

        二、在對象上調用wait()方法(不是在線程上調用)。

        三、線程不能在對象上得到鎖定,它正試圖運行該對象的方法代碼。

        四、線程調度程序能夠決定將當前運行狀態移動到可運行狀態,以便讓另外一個線程得到運行機會,而不須要任何理由。

Java線程:線程的同步與鎖

1、同步問題提出

        線程的同步是爲了防止多個線程訪問一個數據對象時,對數據形成的破壞。

        例如:兩個線程ThreadA、ThreadB都操做同一個對象Foo對象,並修改Foo對象上的數據。

[java]  view plain  copy
  1. public class Foo {  
  2.     private int x = 100;  
  3.     public int getX() {  
  4.         return x;  
  5.     }  
  6.     public int fix(int y) {  
  7.         x = x - y;  
  8.         return x;  
  9.     }  
  10. }   
  11.    
  12. public class FooRunnable implements Runnable {  
  13.     private Foo foo =new Foo();  
  14.    
  15.     public static void main(String[] args) {  
  16.        FooRunnable r = new FooRunnable();  
  17.         Thread ta = new Thread(r,"Thread-A");  
  18.         Thread tb = new Thread(r,"Thread-B");  
  19.         ta.start();  
  20.         tb.start();  
  21.     }  
  22.    
  23.     @Override  
  24.     public void run() {  
  25.        for (int i = 0; i < 3; i++) {  
  26.             this.fix(30);  
  27.             try {  
  28.                 Thread.sleep(1);  
  29.             } catch (InterruptedException e) {  
  30.                 e.printStackTrace();  
  31.             }  
  32.             System.out.println(Thread.currentThread().getName()+ " :當前foo對象的x值= " + foo.getX());  
  33.         }  
  34.     }  
  35.    
  36.     public int fix(int y) {  
  37.        return foo.fix(y);  
  38.     }  
  39. }  

        執行結果:

[java]  view plain  copy
  1. Thread-B :當前foo對象的x值= 40  
  2. Thread-A :當前foo對象的x值= 10  
  3. Thread-B :當前foo對象的x值= -20  
  4. Thread-A :當前foo對象的x值= -50  
  5. Thread-B :當前foo對象的x值= -80  
  6. Thread-A :當前foo對象的x值= -80  

        從結果發現,這樣的輸出值明顯是不合理的,緣由是兩個線程不加控制的訪問Foo對象並修改其數據所致。

        若是要保持結果的合理性,只須要達到一個目的,就是將對Foo的訪問加以限制,每次只能有一個線程在訪問。這樣就能保證Foo對象中數據的合理性了。

        在具體的Java代碼中須要完成如下兩個操做:

        把競爭訪問的資源類Foo變量x標識爲private;

        同步修改變量的代碼,使用synchronized關鍵字同步方法或代碼。

2、同步和鎖定

        一、鎖的原理

        Java中每一個對象都有一個內置鎖。

        當程序運行到非靜態的synchronized同步方法上時,自動得到與正在執行代碼類的當前實例(this實例)有關的鎖。得到一個對象的鎖也稱爲獲取鎖、鎖定對象、在對象上鎖定或在對象上同步。

        當程序運行到synchronized同步方法或代碼塊時才該對象鎖才起做用。

        一個對象只有一個鎖。因此,若是一個線程得到該鎖,就沒有其餘線程能夠得到鎖,直到第一個線程釋放(或返回)鎖。這也意味着任何其餘線程都不能進入該對象上的synchronized方法或代碼塊,直到該鎖被釋放。

        釋放鎖是指持鎖線程退出了synchronized同步方法或代碼塊。

        關於鎖和同步,有一下幾個要點:

        1)只能同步方法,而不能同步變量和類;

        2)每一個對象只有一個鎖;當提到同步時,應該清楚在什麼上同步?也就是說,在哪一個對象上同步?

        3)沒必要同步類中全部的方法,類能夠同時擁有同步和非同步方法。

        4)若是兩個線程要執行一個類中的synchronized方法,而且兩個線程使用相同的實例來調用方法,那麼一次只能有一個線程可以執行方法,另外一個須要等待,直到鎖被釋放。也就是說:若是一個線程在對象上得到一個鎖,就沒有任何其餘線程能夠進入(該對象的)類中的任何一個同步方法。

        5)若是線程擁有同步和非同步方法,則非同步方法能夠被多個線程自由訪問而不受鎖的限制。

        6)線程睡眠時,它所持的任何鎖都不會釋放。

        7)線程能夠得到多個鎖。好比,在一個對象的同步方法裏面調用另一個對象的同步方法,則獲取了兩個對象的同步鎖。

        8)同步損害併發性,應該儘量縮小同步範圍。同步不但能夠同步整個方法,還能夠同步方法中一部分代碼塊。

        9)在使用同步代碼塊時候,應該指定在哪一個對象上同步,也就是說要獲取哪一個對象的鎖。例如:

[java]  view plain  copy
  1. public int fix(int y) {  
  2.        synchronized (this) {  
  3.            x = x - y;  
  4.        }  
  5.        return x;  
  6.    }  

        固然,同步方法也能夠改寫爲非同步方法,但功能徹底同樣的,例如:

[java]  view plain  copy
  1. public synchronized int getX() {  
  2.     return x++;  
  3. }  

        與  

[java]  view plain  copy
  1. public int getX() {  
  2.       synchronized (this) {  
  3.           return x;  
  4.       }  
  5.   }  

        效果是徹底同樣的。

3、靜態方法同步

        要同步靜態方法,須要一個用於整個類對象的鎖,這個對象是就是這個類(XXX.class)。

        例如:

[java]  view plain  copy
  1. public staticsynchronized int setName(String name){  
  2.       Xxx.name = name;  
  3. }  

        等價於

[java]  view plain  copy
  1. public static intsetName(String name){  
  2.       synchronized(Xxx.class){  
  3.             Xxx.name = name;  
  4.       }  
  5. }  

4、若是線程不能得到鎖會怎麼樣

        若是線程試圖進入同步方法,而其鎖已經被佔用,則線程在該對象上被阻塞。實質上,線程進入該對象的一種池中,必須在那裏等待,直到其鎖被釋放,該線程再次變爲可運行或運行爲止。

        當考慮阻塞時,必定要注意哪一個對象正被用於鎖定:

        一、調用同一個對象中非靜態同步方法的線程將彼此阻塞。若是是不一樣對象,則每一個線程有本身的對象的鎖,線程間彼此互不干預。

        二、調用同一個類中的靜態同步方法的線程將彼此阻塞,它們都是鎖定在相同的Class對象上。

        三、靜態同步方法和非靜態同步方法將永遠不會彼此阻塞,由於靜態方法鎖定在Class對象上,非靜態方法鎖定在該類的對象上。

        四、對於同步代碼塊,要看清楚什麼對象已經用於鎖定(synchronized後面括號的內容)。在同一個對象上進行同步的線程將彼此阻塞,在不一樣對象上鎖定的線程將永遠不會彼此阻塞。

5、什麼時候須要同步

        在多個線程同時訪問互斥(可交換)數據時,應該同步以保護數據,確保兩個線程不會同時修改更改它。

        對於非靜態字段中可更改的數據,一般使用非靜態方法訪問。

        對於靜態字段中可更改的數據,一般使用靜態方法訪問。

        若是須要在非靜態方法中使用靜態字段,或者在靜態字段中調用非靜態方法,問題將變得很是複雜。

6、線程安全類

        當一個類已經很好的同步以保護它的數據時,這個類就稱爲「線程安全的」。

        即便是線程安全類,也應該特別當心,由於操做的線程之間仍然不必定安全。

        舉個形象的例子,好比一個集合是線程安全的,有兩個線程在操做同一個集合對象,當第一個線程查詢集合非空後,刪除集合中全部元素的時候。第二個線程也來執行與第一個線程相同的操做,也許在第一個線程查詢後,第二個線程也查詢出集合非空,可是當第一個執行清除後,第二個再執行刪除顯然是不對的,由於此時集合已經爲空了。

        舉個例子:

[java]  view plain  copy
  1. public class NameList {  
  2.     private List nameList = Collections.synchronizedList(newLinkedList());  
  3.    
  4.     public void add(String name) {  
  5.         nameList.add(name);  
  6.     }  
  7.    
  8.     public String removeFirst() {  
  9.        if (nameList.size()>0) {  
  10.        return (String) nameList.remove(0);  
  11.        } else {  
  12.            return null;  
  13.        }  
  14.     }    
  15. }  
  16.    
  17. public class TestNameList {  
  18.     public static void main(String[] args) {  
  19.         final NameList nl =new NameList();  
  20.          nl.add("蘇東坡");  
  21.          class NameDropper extends Thread{  
  22.            @Override  
  23.            public void run() {  
  24.               String name = nl.removeFirst();  
  25.                 System.out.println(name);  
  26.            }          
  27.          }  
  28.          Thread t1=new NameDropper();  
  29.          Thread t2=new NameDropper();  
  30.          t1.start();  
  31.          t2.start();  
  32.     }  
  33. }  

        執行結果:

[java]  view plain  copy
  1. 蘇東坡  
  2. null  

        雖然集合對象

[java]  view plain  copy
  1. private List nameList =Collections.synchronizedList(new LinkedList());  

是同步的,可是程序還不是線程安全的。

        出現這種事件的緣由是,上例中一個線程操做列表過程當中沒法阻止另一個線程對列表的其餘操做。

        解決上面問題的辦法是,在操做集合對象的NameList上面作一個同步。改寫後的代碼以下:

[java]  view plain  copy
  1. public class NameList {  
  2.     private List nameList = Collections.synchronizedList(newLinkedList());  
  3.    
  4.     public synchronized void add(String name) {  
  5.         nameList.add(name);  
  6.     }  
  7.    
  8.     public synchronized StringremoveFirst() {  
  9.        if (nameList.size()>0) {  
  10.         return (String) nameList.remove(0);  
  11.        } else {  
  12.            return null;  
  13.        }  
  14.     }    
  15. }  

        這樣,當一個線程訪問其中一個同步方法時,其餘線程只有等待。

7、線程死鎖

        死鎖對Java程序來講,是很複雜的,也很難發現問題。當兩個線程被阻塞,每一個線程在等待另外一個線程時就發生死鎖。

        仍是看一個比較直觀的死鎖例子:

[java]  view plain  copy
  1. public class Deadlock {  
  2.     private static class Resource{  
  3.        public int value;  
  4.     }  
  5.     private Resource resourceA=new Resource();  
  6.     private Resource resourceB=new Resource();  
  7.     public int read(){  
  8.        synchronized (resourceA) {  
  9.            synchronized (resourceB) {  
  10.               return resourceB.value+resourceA.value;  
  11.            }  
  12.        }  
  13.     }  
  14.     public void write(int a,int b){  
  15.        synchronized(resourceB){  
  16.            synchronized (resourceA) {  
  17.               resourceA.value=a;  
  18.               resourceB.value=b;  
  19.            }  
  20.        }  
  21.     }  
  22. }  

        假設read()方法由一個線程啓動,write()方法由另一個線程啓動。讀線程將擁有resourceA鎖,寫線程將擁有resourceB鎖,二者都堅持等待的話就出現死鎖。

        實際上,上面這個例子發生死鎖的機率很小。由於在代碼內的某個點,CPU必須從讀線程切換到寫線程,因此,死鎖基本上不能發生。

        可是,不管代碼中發生死鎖的機率有多小,一旦發生死鎖,程序就死掉。有一些設計方法能幫助避免死鎖,包括始終按照預約義的順序獲取鎖這一策略。已經超出SCJP的考試範圍。

8、線程同步小結

        一、線程同步的目的是爲了保護多個線程反問一個資源時對資源的破壞。

        二、線程同步方法是經過鎖來實現,每一個對象都有切僅有一個鎖,這個鎖與一個特定的對象關聯,線程一旦獲取了對象鎖,其餘訪問該對象的線程就沒法再訪問該對象的其餘同步方法。

        三、對於靜態同步方法,鎖是針對這個類的,鎖對象是該類的Class對象。靜態和非靜態方法的鎖互不干預。一個線程得到鎖,當在一個同步方法中訪問另外對象上的同步方法時,會獲取這兩個對象鎖。

        四、對於同步,要時刻清醒在哪一個對象上同步,這是關鍵。

        五、編寫線程安全的類,須要時刻注意對多個線程競爭訪問資源的邏輯和安全作出正確的判斷,對「原子」操做作出分析,並保證原子操做期間別的線程沒法訪問競爭資源。

        六、當多個線程等待一個對象鎖時,沒有獲取到鎖的線程將發生阻塞。

        七、死鎖是線程間相互等待鎖鎖形成的,在實際中發生的機率很是的小。真讓你寫個死鎖程序,不必定好使,呵呵。可是,一旦程序發生死鎖,程序將死掉。

Java線程:線程的交互

        線程交互是比較複雜的問題,SCJP要求不很基礎:給定一個場景,編寫代碼來恰當使用等待、通知和通知全部線程。

1、線程交互的基礎知識

        SCJP所要求的線程交互知識點須要從java.lang.Object的類的三個方法來學習:

[java]  view plain  copy
  1. void notify()——喚醒在此對象監視器上等待的單個線程。  
  2. void notifyAll()——喚醒在此對象監視器上等待的全部線程。  
  3. void wait()——致使當前的線程等待,直到其餘線程調用此對象的 notify()方法或 notifyAll()方法。  

        固然,wait()還有另外兩個重載方法:

[java]  view plain  copy
  1. void wait(longtimeout)——致使當前的線程等待,直到其餘線程調用此對象的 notify()方法或 notifyAll()方法,或者超過指定的時間量。  
  2. void wait(longtimeout, int nanos)——致使當前的線程等待,直到其餘線程調用此對象的 notify()方法或 notifyAll()方法,或者其餘某個線程中斷當前線程,或者已超過某個實際時間量。  

        以上這些方法是幫助線程傳遞線程關心的時間狀態。

        關於等待/通知,要記住的關鍵點是:

        必須從同步環境內調用wait()、notify()、notifyAll()方法。線程不能調用對象上等待或通知的方法,除非它擁有那個對象的鎖。

        wait()、notify()、notifyAll()都是Object的實例方法。與每一個對象具備鎖同樣,每一個對象能夠有一個線程列表,他們等待來自該信號(通知)。線程經過執行對象上的wait()方法得到這個等待列表。從那時候起,它再也不執行任何其餘指令,直到調用對象的notify()方法爲止。若是多個線程在同一個對象上等待,則將只選擇一個線程(不保證以何種順序)繼續執行。若是沒有線程等待,則不採起任何特殊操做。

        下面看個例子就明白了:

[java]  view plain  copy
  1. /** 
  2.  * 計算輸出其餘線程鎖計算的數據 
  3.  */  
  4. public class ThreadA {  
  5.     public static void main(String[] args) {  
  6.        ThreadB b=new ThreadB();  
  7.        //啓動計算線程  
  8.        b.start();  
  9.        //線程A擁有b對象上的鎖。線程爲了調用wait()或notify()方法,該線程必須是那個對象鎖的擁有者  
  10.        synchronized (b) {  
  11.            try {  
  12.               System.out.println("等待對象b完成計算......");  
  13.               b.wait();  
  14.            } catch (InterruptedException e) {  
  15.               e.printStackTrace();  
  16.            }  
  17.            System.out.println("b對象計算的總和是:" + b.total);  
  18.        }  
  19.     }  
  20. }  
  21.    
  22. /** 
  23.  * 計算1+2+3+...+100的和 
  24.  */  
  25. public class ThreadB extends Thread {  
  26.     int total;  
  27.     public void run(){  
  28.        synchronized (this) {  
  29.            for (int i=0;i<101;i++){  
  30.               total+=i;  
  31.            }  
  32.            //(完成計算了)喚醒在此對象監視器上等待的單個線程,在本例中線程A被喚醒  
  33.            notify();  
  34.        }  
  35.     }  
  36. }  

        執行結果:

[java]  view plain  copy
  1. 等待對象b完成計算......  
  2. b對象計算的總和是:5050  

        千萬注意:

        當在對象上調用wait()方法時,執行該代碼的線程當即放棄它在對象上的鎖。然而調用notify()時,並不意味着這時線程會放棄其鎖。若是線程榮然在完成同步代碼,則線程在移出以前不會放棄鎖。所以,只要調用notify()並不意味着這時該鎖變得可用。

2、多個線程在等待一個對象鎖時候使用notifyAll()

        在多數狀況下,最好通知等待某個對象的全部線程。若是這樣作,能夠在對象上使用notifyAll()讓全部在此對象上等待的線程衝出等待區,返回到可運行狀態。

        舉個例子:

[java]  view plain  copy
  1. /** 
  2.  * 計算線程 
  3.  */  
  4. public class Calculator extends Thread {  
  5.     int total;  
  6.     @Override  
  7.     public void run() {  
  8.        synchronized (this) {  
  9.            for(int i=0;i<101;i++){  
  10.               total+=i;  
  11.            }  
  12.         }  
  13.        //通知全部在此對象上等待的線程  
  14.        notifyAll();  
  15.     }    
  16. }  
  17.    
  18. /** 
  19.  * 獲取計算結果並輸出 
  20.  */  
  21. public class ReaderResult extends Thread {  
  22.     Calculator c;  
  23.     public ReaderResult(Calculator c) {  
  24.        this.c = c;  
  25.     }  
  26.     public void run(){  
  27.        synchronized (c) {  
  28.            try {  
  29.               System.out.println(Thread.currentThread() + "等待計算結果......");  
  30.               c.wait();  
  31.            } catch (InterruptedException e) {  
  32.               e.printStackTrace();  
  33.            }  
  34.             System.out.println(Thread.currentThread()+ "計算結果爲:" + c.total);  
  35.        }  
  36.     }  
  37.     public static void main(String[] args) {  
  38.        Calculator calculator=new Calculator();  
  39.        //啓動三個線程,分別獲取計算結果  
  40.        new ReaderResult(calculator).start();  
  41.        new ReaderResult(calculator).start();  
  42.        new ReaderResult(calculator).start();  
  43.        //啓動計算線程  
  44.        calculator.start();  
  45.     }  
  46. }  

        執行結果:

[java]  view plain  copy
  1. Thread[Thread-1,5,main]等待計算結果......  
  2. Thread[Thread-2,5,main]等待計算結果......  
  3. Thread[Thread-3,5,main]等待計算結果......  
  4. Exception in thread"Thread-0" java.lang.IllegalMonitorStateException  
  5.     atjava.lang.Object.notifyAll(Native Method)  
  6.     attest.Calculator.run(Calculator.java:15)  
  7. Thread[Thread-3,5,main]計算結果爲:5050  
  8. Thread[Thread-2,5,main]計算結果爲:5050  
  9. Thread[Thread-1,5,main]計算結果爲:5050  

        運行結果代表,程序中有異常,而且屢次運行結果可能有多種輸出結果。這就是說明,這個多線程的交互程序還存在問題。到底是出了什麼問題,須要深刻的分析和思考,下面將作具體分析。

        實際上,上面這個代碼中,咱們指望的是讀取結果的線程在計算線程調用notifyAll()以前等待便可。可是,若是計算線程先執行,並在讀取結果線程等待以前調用了notify()方法,那麼又會發生什麼呢?這種狀況是可能發生的。由於沒法保證線程的不一樣部分將按照什麼順序來執行。幸運的是當讀取線程運行時,它只能立刻進入等待狀態----它沒有作任何事情來檢查等待的事件是否已經發生。 ----所以,若是計算線程已經調用了notifyAll()方法,那麼它就不會再次調用notifyAll(),----而且等待的讀取線程將永遠保持等待。這固然是開發者所不肯意看到的問題。

        所以,當等待的事件發生時,須要可以檢查notifyAll()通知事件是否已經發生。

        一般,解決上面問題的最佳方式是利用某種循環,該循環檢查某個條件表達式,只有當正在等待的事情尚未發生的狀況下,它才繼續等待。

Java線程:線程的調度-休眠

        Java線程調度是Java多線程的核心,只有良好的調度,才能充分發揮系統的性能,提升程序的執行效率。

        這裏要明確的一點,無論程序員怎麼編寫調度,只能最大限度的影響線程執行的次序,而不能作到精準控制。

        線程休眠的目的是使線程讓出CPU的最簡單的作法之一,線程休眠時候,會將CPU資源交給其餘線程,以便能輪換執行,當休眠必定時間後,線程會甦醒,進入準備狀態等待執行。

        線程休眠的方法是Thread.sleep(long millis)和Thread.sleep(long millis, int nanos),均爲靜態方法,那調用sleep休眠的哪一個線程呢?簡單說,哪一個線程調用sleep,就休眠哪一個線程。

[java]  view plain  copy
  1. /** 
  2.  * Java線程:線程的調度-休眠 
  3.  */  
  4. public class TestSleep {  
  5.     public static void main(String[] args) {  
  6.        Thread t1=new MyThread1();  
  7.        Thread t2=new Thread(new MyRunnable());  
  8.        t1.start();  
  9.        t2.start();  
  10.     }  
  11. }  
  12. class MyThread1 extends Thread{  
  13.     @Override  
  14.     public void run() {  
  15.        for(int i=0;i<3;i++){  
  16.            System.out.println("線程1第"+i+"次執行!");  
  17.            try {  
  18.               Thread.sleep(50);  
  19.            } catch (InterruptedException e) {  
  20.               e.printStackTrace();  
  21.            }  
  22.        }  
  23.     }    
  24. }  
  25. class MyRunnable implements Runnable{  
  26.     @Override  
  27.     public void run() {       
  28.        for(int i=0;i<3;i++){  
  29.            System.out.println("線程2第"+i+"次執行!");  
  30.            try {  
  31.               Thread.sleep(50);  
  32.            } catch (InterruptedException e) {  
  33.               e.printStackTrace();  
  34.            }  
  35.        }  
  36.     }    
  37. }  

        執行結果:

[java]  view plain  copy
  1. 線程10次執行!  
  2. 線程20次執行!  
  3. 線程21次執行!  
  4. 線程11次執行!  
  5. 線程22次執行!  
  6. 線程12次執行!  

        從上面的結果輸出能夠看出,沒法精準保證線程執行次序。

Java線程:線程的調度-優先級

        與線程休眠相似,線程的優先級仍然沒法保障線程的執行次序。只不過,優先級高的線程獲取CPU資源的機率較大,優先級低的並不是沒機會執行。

        線程的優先級用1-10之間的整數表示,數值越大優先級越高,默認的優先級爲5。

        在一個線程中開啓另一個新線程,則新開線程稱爲該線程的子線程,子線程初始優先級與父線程相同。

[java]  view plain  copy
  1. /** 
  2.  * Java線程:線程的調度-優先級 
  3.  */  
  4. public class TestPriority {  
  5.     public static void main(String[] args) {  
  6.        Thread t1=new MyThread1();  
  7.        Thread t2=new Thread(new MyRunnable());  
  8.        t1.setPriority(10);  
  9.        t2.setPriority(1);  
  10.        t1.start();  
  11.        t2.start();  
  12.     }  
  13. }  
  14. class MyThread1 extends Thread{  
  15.     @Override  
  16.     public void run() {  
  17.        for(int i=0;i<10;i++){  
  18.            System.out.println("線程1第"+i+"次執行!");  
  19.            try {  
  20.               Thread.sleep(100);  
  21.            } catch (InterruptedException e) {  
  22.               e.printStackTrace();  
  23.            }  
  24.        }  
  25.     }    
  26. }  
  27. class MyRunnable implements Runnable{  
  28.     @Override  
  29.     public void run() {       
  30.        for(int i=0;i<10;i++){  
  31.            System.out.println("線程2第"+i+"次執行!");  
  32.            try {  
  33.               Thread.sleep(100);  
  34.            } catch (InterruptedException e) {  
  35.               e.printStackTrace();  
  36.            }  
  37.        }  
  38.     }    
  39. }  

        執行結果:

[java]  view plain  copy
  1. 線程10次執行!  
  2. 線程11次執行!  
  3. 線程12次執行!  
  4. 線程20次執行!  
  5. 線程13次執行!  
  6. 線程21次執行!  
  7. 線程14次執行!  
  8. 線程22次執行!  
  9. 線程15次執行!  
  10. 線程23次執行!  
  11. 線程16次執行!  
  12. 線程24次執行!  
  13. 線程17次執行!  
  14. 線程25次執行!  
  15. 線程18次執行!  
  16. 線程26次執行!  
  17. 線程19次執行!  
  18. 線程27次執行!  
  19. 線程28次執行!  
  20. 線程29次執行!  

Java線程:線程的調度-讓步

        線程的讓步含義就是使當前運行着線程讓出CPU資源,可是讓給誰不知道,僅僅是讓出,線程狀態回到可運行狀態

        線程的讓步使用Thread.yield()方法,yield()爲靜態方法,功能是暫停當前正在執行的線程對象,並執行其餘線程。

[java]  view plain  copy
  1. /** 
  2.  * Java線程:線程的調度-讓步 
  3.  */  
  4. public class Test {  
  5.     public static void main(String[] args) {  
  6.        Thread t1=new MyThread1();  
  7.        Thread t2=new Thread(new MyRunnable());  
  8.        t1.start();  
  9.        t2.start();  
  10.     }  
  11. }  
  12. class MyThread1 extends Thread{  
  13.     @Override  
  14.     public void run() {  
  15.        for(int i=0;i<10;i++){  
  16.            System.out.println("線程1第"+i+"次執行!");          
  17.        }  
  18.     }    
  19. }  
  20. class MyRunnable implements Runnable{  
  21.     @Override  
  22.     public void run() {       
  23.        for(int i=0;i<10;i++){  
  24.            System.out.println("線程2第"+i+"次執行!");  
  25.            Thread.yield();  
  26.        }  
  27.     }    
  28. }  

        執行結果:

[java]  view plain  copy
  1. 線程20次執行!  
  2. 線程10次執行!  
  3. 線程11次執行!  
  4. 線程12次執行!  
  5. 線程13次執行!  
  6. 線程14次執行!  
  7. 線程15次執行!  
  8. 線程16次執行!  
  9. 線程17次執行!  
  10. 線程18次執行!  
  11. 線程19次執行!  
  12. 線程21次執行!  
  13. 線程22次執行!  
  14. 線程23次執行!  
  15. 線程24次執行!  
  16. 線程25次執行!  
  17. 線程26次執行!  
  18. 線程27次執行!  
  19. 線程28次執行!  
  20. 線程29次執行!  

Java線程:線程的調度-合併

        線程的合併的含義就是將幾個並行線程的線程合併爲一個單線程執行,應用場景是當一個線程必須等待另外一個線程執行完畢才能執行時能夠使用join方法。

        join爲非靜態方法,定義以下:

[java]  view plain  copy
  1. void join()——等待該線程終止。     
  2. void join(longmillis)——等待該線程終止的時間最長爲 millis毫秒。     
  3. void join(longmillis,int nanos)——等待該線程終止的時間最長爲 millis毫秒 + nanos 納秒。  
[java]  view plain  copy
  1. /** 
  2.  * Java線程:線程的調度-合併 
  3.  */  
  4. public class Test {  
  5.     public static void main(String[] args) {  
  6.        Thread t1=new MyThread1();       
  7.        t1.start();  
  8.        for (int i = 0; i < 20; i++) {  
  9.            System.out.println("主線程第" + i +"次執行!");  
  10.            if (i>2) {  
  11.               try {  
  12.                   ///t1線程合併到主線程中,主線程中止執行過程,轉而執行t1線程,直到t1執行完畢後繼續。  
  13.                   t1.join();  
  14.               } catch (InterruptedException e) {  
  15.                   e.printStackTrace();  
  16.               }  
  17.            }  
  18.        }  
  19.     }  
  20. }  
  21. class MyThread1 extends Thread{  
  22.     @Override  
  23.     public void run() {  
  24.        for(int i=0;i<10;i++){  
  25.            System.out.println("線程1第"+i+"次執行!");          
  26.        }  
  27.     }    
  28. }  

        執行結果:

[java]  view plain  copy
  1. 主線程第0次執行!  
  2. 主線程第1次執行!  
  3. 主線程第2次執行!  
  4. 主線程第3次執行!  
  5. 線程10次執行!  
  6. 線程11次執行!  
  7. 線程12次執行!  
  8. 線程13次執行!  
  9. 線程14次執行!  
  10. 線程15次執行!  
  11. 線程16次執行!  
  12. 線程17次執行!  
  13. 線程18次執行!  
  14. 線程19次執行!  
  15. 主線程第4次執行!  
  16. 主線程第5次執行!  
  17. 主線程第6次執行!  
  18. 主線程第7次執行!  
  19. 主線程第8次執行!  
  20. 主線程第9次執行!  
  21. 主線程第10次執行!  
  22. 主線程第11次執行!  
  23. 主線程第12次執行!  
  24. 主線程第13次執行!  
  25. 主線程第14次執行!  
  26. 主線程第15次執行!  
  27. 主線程第16次執行!  
  28. 主線程第17次執行!  
  29. 主線程第18次執行!  
  30. 主線程第19次執行!  

Java線程:線程的調度-守護線程

        守護線程與普通線程寫法上基本麼啥區別,調用線程對象的方法setDaemon(true),則能夠將其設置爲守護線程。

        守護線程使用的狀況較少,但並不是無用,舉例來講,JVM的垃圾回收、內存管理等線程都是守護線程。還有就是在作數據庫應用時候,使用的數據庫鏈接池,鏈接池自己也包含着不少後臺線程,監控鏈接個數、超時時間、狀態等等。

        setDaemon方法的詳細說明:

[java]  view plain  copy
  1. public final void setDaemon(boolean on)將該線程標記爲守護線程或用戶線程。當正在運行的線程都是守護線程時,Java虛擬機退出。  
        該方法必須在啓動線程前調用。
        該方法首先調用該線程的 checkAccess方法,且不帶任何參數。這可能拋出 SecurityException(在當前線程中)。
          參數:on - 若是爲true,則將該線程標記爲守護線程。
          拋出:
        IllegalThreadStateException- 若是該線程處於活動狀態。
        SecurityException- 若是當前線程沒法修改該線程。
        另請參見:
        isDaemon(),checkAccess()

[java]  view plain  copy
  1. /** 
  2.  * Java線程:線程的調度-守護線程 
  3.  */  
  4. public class Test {  
  5.     public static void main(String[] args) {  
  6.        Thread t1=new MyCommon();  
  7.        Thread t2=new Thread(new MyDaemon());  
  8.        t2.setDaemon(true);//設置爲守護線程  
  9.        t2.start();  
  10.        t1.start();        
  11.     }  
  12. }  
  13. class MyCommon extends Thread{  
  14.     @Override  
  15.     public void run() {  
  16.        for(int i=0;i<5;i++){  
  17.            System.out.println("線程1第"+i+"次執行!");  
  18.            try {  
  19.               Thread.sleep(7);  
  20.            } catch (InterruptedException e) {  
  21.               e.printStackTrace();  
  22.            }  
  23.        }  
  24.     }    
  25. }  
  26. class MyDaemon implements Runnable{  
  27.     @Override  
  28.     public void run() {  
  29.        for (long i = 0; i < 9999999L; i++) {  
  30.            System.out.println("後臺線程第" + i +"次執行!");  
  31.            try {  
  32.               Thread.sleep(7);  
  33.            } catch (InterruptedException e) {  
  34.               e.printStackTrace();  
  35.            }  
  36.        }  
  37.     }    
  38. }  

        執行結果:

[java]  view plain  copy
  1. 線程10次執行!  
  2. 後臺線程第0次執行!  
  3. 後臺線程第1次執行!  
  4. 線程11次執行!  
  5. 後臺線程第2次執行!  
  6. 線程12次執行!  
  7. 後臺線程第3次執行!  
  8. 線程13次執行!  
  9. 後臺線程第4次執行!  
  10. 線程14次執行!  
  11. 後臺線程第5次執行!  
  12. 後臺線程第6次執行!  
  13. 後臺線程第7次執行!  
  14. 後臺線程第8次執行!  
  15. 後臺線程第9次執行!  
  16. 後臺線程第10次執行!  

        從上面的執行結果能夠看出:

        前臺線程是保證執行完畢的,後臺線程尚未執行完畢就退出了。

        實際上:JRE判斷程序是否執行結束的標準是全部的前臺執線程行完畢了,而無論後臺線程的狀態,所以,在使用後臺縣城時候必定要注意這個問題。

Java線程:線程的同步-同步方法

        線程的同步是保證多線程安全訪問競爭資源的一種手段。

        線程的同步是Java多線程編程的難點,每每開發者搞不清楚什麼是競爭資源、何時須要考慮同步,怎麼同步等等問題,固然,這些問題沒有很明確的答案,但有些原則問題須要考慮,是否有競爭資源被同時改動的問題?

        在本部分以前,請參閱《Java線程:線程的同步與鎖》部分,本部分是在此基礎上所寫的。

        對於同步,在具體的Java代碼中須要完成一下兩個操做:

        把競爭訪問的資源標識爲private;

        同步哪些修改變量的代碼,使用synchronized關鍵字同步方法或代碼。

        固然這不是惟一控制併發安全的途徑。

        synchronized關鍵字使用說明

        synchronized只能標記非抽象的方法,不能標識成員變量。

        爲了演示同步方法的使用,構建了一個信用卡帳戶,起初信用額爲100w,而後模擬透支、存款等多個操做。顯然銀行帳戶User對象是個競爭資源,而多個併發操做的是帳戶方法oper(int x),固然應該在此方法上加上同步,並將帳戶的餘額設爲私有變量,禁止直接訪問。

[java]  view plain  copy
  1. /** 
  2.  * Java線程:線程的同步 
  3.  */  
  4. public class Test {  
  5.     public static void main(String[] args) {  
  6.        User u = new User("張三"100);  
  7.        MyThread t1 = new MyThread("線程A", u, 20);  
  8.        MyThread t2 = new MyThread("線程B", u, -60);  
  9.        MyThread t3 = new MyThread("線程C", u, -80);  
  10.        MyThread t4 = new MyThread("線程D", u, -30);  
  11.        MyThread t5 = new MyThread("線程E", u, 32);  
  12.        MyThread t6 = new MyThread("線程F", u, 21);  
  13.        t1.start();  
  14.        t2.start();  
  15.        t3.start();  
  16.        t4.start();  
  17.        t5.start();  
  18.        t6.start();  
  19.     }  
  20. }  
  21.    
  22. class MyThread extends Thread {  
  23.     private User u;  
  24.     private int y = 0;  
  25.    
  26.     MyThread(String name, User u, int y) {  
  27.        super(name);  
  28.        this.u = u;  
  29.        this.y = y;  
  30.     }  
  31.     public void run() {  
  32.        u.oper(y);  
  33.     }  
  34. }  
  35.    
  36. class User {  
  37.     private String code;  
  38.     private int cash;  
  39.     User(String code, int cash) {  
  40.        this.code = code;  
  41.        this.cash = cash;  
  42.     }  
  43.     public String getCode() {  
  44.        return code;  
  45.     }  
  46.     public void setCode(String code) {  
  47.        this.code = code;  
  48.     }  
  49.    
  50.     /** 
  51.      * 業務方法 
  52.      * @param x  添加x萬元 
  53.      */  
  54.     public synchronized void oper(int x) {  
  55.        try {  
  56.            Thread.sleep(10L);  
  57.            this.cash += x;  
  58.            System.out.println(Thread.currentThread().getName() + "運行結束,增長「"  
  59.                   + x + "」,當前用戶帳戶餘額爲:" + cash);  
  60.            Thread.sleep(10L);  
  61.        } catch (InterruptedException e) {  
  62.            e.printStackTrace();  
  63.        }  
  64.     }  
  65.     @Override  
  66.     public String toString() {  
  67.        return "User{" + "code='" + code + '\'' + ",cash=" + cash + '}';  
  68.     }  
  69. }  

        執行結果:

[java]  view plain  copy
  1. 線程A運行結束,增長「20」,當前用戶帳戶餘額爲:120  
  2. 線程F運行結束,增長「21」,當前用戶帳戶餘額爲:141  
  3. 線程D運行結束,增長「-30」,當前用戶帳戶餘額爲:111  
  4. 線程B運行結束,增長「-60」,當前用戶帳戶餘額爲:51  
  5. 線程E運行結束,增長「32」,當前用戶帳戶餘額爲:83  
  6. 線程C運行結束,增長「-80」,當前用戶帳戶餘額爲:3  

        反面教材,不一樣步的狀況,也就是去掉oper(int x)方法的synchronized修飾符,而後運行程序,結果以下:

[java]  view plain  copy
  1. 線程F運行結束,增長「21」,當前用戶帳戶餘額爲:121  
  2. 線程D運行結束,增長「-30」,當前用戶帳戶餘額爲:91  
  3. 線程B運行結束,增長「-60」,當前用戶帳戶餘額爲:31  
  4. 線程E運行結束,增長「32」,當前用戶帳戶餘額爲:63  
  5. 線程A運行結束,增長「20」,當前用戶帳戶餘額爲:3  
  6. 線程C運行結束,增長「-80」,當前用戶帳戶餘額爲:-17  
        很顯然,上面的結果是錯誤的,致使錯誤的緣由是多個線程併發訪問了競爭資源u,並對u的屬性作了改動。

        可見同步的重要性。

        注意:

        經過前文可知,線程退出同步方法時將釋放掉方法所屬對象的鎖,但還應該注意的是,同步方法中還能夠使用特定的方法對線程進行調度。這些方法來自於java.lang.Object類。

 

[java]  view plain  copy
  1. void notify()      
  2.                     喚醒在此對象監視器上等待的單個線程。      
  3. void notifyAll()      
  4.                     喚醒在此對象監視器上等待的全部線程。      
  5. void wait()      
  6.                     致使當前的線程等待,直到其餘線程調用此對象的 notify()方法或 notifyAll()方法。      
  7. void wait(long timeout)      
  8.                     致使當前的線程等待,直到其餘線程調用此對象的 notify()方法或 notifyAll()方法,或者超過指定的時間量。      
  9. void wait(long timeout,int nanos)      
  10.                     致使當前的線程等待,直到其餘線程調用此對象的 notify()方法或 notifyAll()方法,或者其餘某個線程中斷當前線程,或者已超過某個實際時間量。  

        結合以上方法,處理多線程同步與互斥問題很是重要,著名的生產者-消費者例子就是一個經典的例子,任何語言多線程必學的例子。

Java線程:線程的同步-同步塊

        對於同步,除了同步方法外,還能夠使用同步代碼塊,有時候同步代碼塊會帶來比同步方法更好的效果。

        追其同步的根本的目的,是控制競爭資源的正確的訪問,所以只要在訪問競爭資源的時候保證同一時刻只能一個線程訪問便可,所以Java引入了同步代碼快的策略,以提升性能。

        在上個例子的基礎上,對oper方法作了改動,由同步方法改成同步代碼塊模式,程序的執行邏輯並無問題。

[java]  view plain  copy
  1. /** 
  2.  * Java線程:線程的同步-同步代碼塊 
  3.  */  
  4. public class Test {  
  5.     public static void main(String[] args) {  
  6.        User u = new User("張三"100);  
  7.        MyThread t1 = new MyThread("線程A", u, 20);  
  8.        MyThread t2 = new MyThread("線程B", u, -60);  
  9.        MyThread t3 = new MyThread("線程C", u, -80);  
  10.        MyThread t4 = new MyThread("線程D", u, -30);  
  11.        MyThread t5 = new MyThread("線程E", u, 32);  
  12.        MyThread t6 = new MyThread("線程F", u, 21);  
  13.        t1.start();  
  14.        t2.start();  
  15.        t3.start();  
  16.        t4.start();  
  17.        t5.start();  
  18.        t6.start();  
  19.     }  
  20. }  
  21.    
  22. class MyThread extends Thread{  
  23.     private User u;  
  24.     private int y = 0;  
  25.    
  26.     MyThread(String name, User u, int y) {  
  27.        super(name);  
  28.        this.u = u;  
  29.        this.y = y;  
  30.     }  
  31.     public void run() {  
  32.        u.oper(y);  
  33.     }  
  34. }  
  35.    
  36. class User {  
  37.     private String code;  
  38.     private int cash;  
  39.     User(String code, int cash) {  
  40.        this.code = code;  
  41.        this.cash = cash;  
  42.     }  
  43.     public String getCode() {  
  44.        return code;  
  45.     }  
  46.     public void setCode(String code) {  
  47.        this.code = code;  
  48.     }  
  49.    
  50.     /** 
  51.      * 業務方法 
  52.      * @param x  添加x萬元 
  53.      */  
  54.     public void oper(int x) {  
  55.        try {  
  56.            Thread.sleep(10L);  
  57.            synchronized (this) {  
  58.               this.cash += x;  
  59.               System.out.println(Thread.currentThread().getName() + "運行結束,增長「"  
  60.                      + x + "」,當前用戶帳戶餘額爲:" + cash);  
  61.            }           
  62.            Thread.sleep(10L);  
  63.        } catch (InterruptedException e) {  
  64.            e.printStackTrace();  
  65.        }  
  66.     }  
  67.     @Override  
  68.     public String toString() {  
  69.        return "User{" + "code='" + code + '\'' + ",cash=" + cash + '}';  
  70.     }  
  71. }  

        執行結果:

[java]  view plain  copy
  1. 線程B運行結束,增長「-60」,當前用戶帳戶餘額爲:40  
  2. 線程D運行結束,增長「-30」,當前用戶帳戶餘額爲:10  
  3. 線程F運行結束,增長「21」,當前用戶帳戶餘額爲:31  
  4. 線程E運行結束,增長「32」,當前用戶帳戶餘額爲:63  
  5. 線程C運行結束,增長「-80」,當前用戶帳戶餘額爲:-17  
  6. 線程A運行結束,增長「20」,當前用戶帳戶餘額爲:3  

        注意:

        在使用synchronized關鍵字時候,應該儘量避免在synchronized方法或synchronized塊中使用sleep或者yield方法,由於synchronized程序塊佔有着對象鎖,你休息那麼其餘的線程只能一邊等着你醒來執行完了才能執行。不但嚴重影響效率,也不合邏輯。

        一樣,在同步程序塊內調用yeild方法讓出CPU資源也沒有意義,由於你佔用着鎖,其餘互斥線程仍是沒法訪問同步程序塊。固然與同步程序塊無關的線程能夠得到更多的執行時間。

Java線程:併發協做-生產者消費者模型

        對於多線程程序來講,無論任何編程語言,生產者和消費者模型都是最經典的。就像學習每一門編程語言同樣,Hello World!都是最經典的例子。

        實際上,準確說應該是「生產者-消費者-倉儲」模型,離開了倉儲,生產者消費者模型就顯得沒有說服力了。

        對於此模型,應該明確一下幾點:

  1. 生產者僅僅在倉儲未滿時候生產,倉滿則中止生產。
  2. 消費者僅僅在倉儲有產品時候才能消費,倉空則等待。
  3. 當消費者發現倉儲沒產品可消費時候會通知生產者生產。
  4. 生產者在生產出可消費產品時候,應該通知等待的消費者去消費。

        此模型將要結合java.lang.Object的wait與notify、notifyAll方法來實現以上的需求。這是很是重要的。

[java]  view plain  copy
  1. /** 
  2.  * Java線程:併發協做-生產者消費者模型 
  3.  */  
  4. public class Test {  
  5.     public static void main(String[] args) {  
  6.        Godown godown=new Godown(30);  
  7.        Consumer c1=new Consumer(50,godown);  
  8.        Consumer c2=new Consumer(20,godown);  
  9.        Consumer c3=new Consumer(30,godown);  
  10.        Producer p1=new Producer(10,godown);  
  11.        Producer p2=new Producer(10,godown);  
  12.        Producer p3=new Producer(10,godown);  
  13.        Producer p4=new Producer(10,godown);  
  14.        Producer p5=new Producer(10,godown);  
  15.        Producer p6=new Producer(10,godown);  
  16.        Producer p7=new Producer(80,godown);  
  17.        c1.start();  
  18.        c2.start();  
  19.        c3.start();  
  20.        p1.start();  
  21.        p2.start();  
  22.        p3.start();  
  23.        p4.start();  
  24.        p5.start();  
  25.        p6.start();  
  26.        p7.start();  
  27.     }  
  28. }  
  29. /** 
  30.  * 倉庫 
  31.  */  
  32. class Godown{  
  33.     public static final int max_size=100;//最大庫存量  
  34.     public int curnum;//當前庫存量  
  35.     Godown() {  
  36.     }  
  37.     Godown(int curnum){  
  38.        this.curnum=curnum;  
  39.     }  
  40.     /** 
  41.      * 生產指定數量的產品 
  42.      */  
  43.     public synchronized void produce(int neednum){  
  44.        //測試是否須要生產  
  45.        while(neednum+curnum>max_size){  
  46.            System.out.println("要生產的產品數量" + neednum +"超過剩餘庫存量" + (max_size - curnum) +",暫時不能執行生產任務!");  
  47.            try {  
  48.               //當前的生產線程等待  
  49.               wait();  
  50.            } catch (InterruptedException e) {  
  51.               e.printStackTrace();  
  52.            }  
  53.        }  
  54.        //知足生產條件,則進行生產,這裏簡單的更改當前庫存量  
  55.        curnum+=neednum;  
  56.        System.out.println("已經生產了"+neednum+"個產品,現倉儲量爲"+curnum);  
  57.        //喚醒在此對象監視器上等待的全部線程  
  58.        notifyAll();  
  59.     }  
  60.     /** 
  61.      * 消費指定數量的產品 
  62.      */  
  63.     public synchronized void consume(int neednum){  
  64.        //測試是否能夠消費  
  65.        while(curnum<neednum){  
  66.            try {  
  67.               //當前的生產線程等待  
  68.               wait();  
  69.            } catch (InterruptedException e) {  
  70.               e.printStackTrace();  
  71.            }  
  72.        }  
  73.        //知足消費條件,則進行消費,這裏簡單的更改當前庫存  
  74.        curnum-=neednum;  
  75.        System.out.println("已經消費了" + neednum +"個產品,現倉儲量爲" + curnum);  
  76.        //喚醒在此對象監視器上等待的全部線程  
  77.        notifyAll();  
  78.     }  
  79. }  
  80. //生產者  
  81. class Producer extends Thread{  
  82.     private int neednum;//生產產品的數量  
  83.     private Godown godown;//倉庫  
  84.     Producer(int neednum, Godown godown) {  
  85.        this.neednum = neednum;  
  86.        this.godown = godown;  
  87.     }  
  88.     public void run(){  
  89.        //生產指定數量的產品  
  90.        godown.produce(neednum);  
  91.     }  
  92. }  
  93. //消費者  
  94. class Consumer extends Thread{  
  95.     private int neednum;//消費產品的數量  
  96.     private Godown godown;//倉庫  
  97.     Consumer(int neednum, Godown godown) {  
  98.        this.neednum = neednum;  
  99.        this.godown = godown;  
  100.     }  
  101.     public void run(){  
  102.        //消費指定數量的產品  
  103.        godown.consume(neednum);  
  104.     }  
  105. }  

        執行結果:

[java]  view plain  copy
  1. 已經消費了20個產品,現倉儲量爲10  
  2. 已經生產了10個產品,現倉儲量爲20  
  3. 已經生產了10個產品,現倉儲量爲30  
  4. 已經生產了10個產品,現倉儲量爲40  
  5. 要生產的產品數量80超過剩餘庫存量60,暫時不能執行生產任務!  
  6. 已經消費了30個產品,現倉儲量爲10  
  7. 已經生產了10個產品,現倉儲量爲20
相關文章
相關標籤/搜索