Java線程詳解(深度好文)

Java線程:概念與原理

一、進程與線程

        進程是指一個內存中運行的應用程序,每個進程都有自己獨立的一塊內存空間,即進程空間或(虛空間)。進程不依賴於線程而獨立存在,一個進程中可以啓動多個線程。比如在Windows系統中,一個運行的exe就是一個進程。

        線程是指進程中的一個執行流程,一個進程中可以運行多個線程。比如Java.exe進程中可以運行很多線程。線程總是屬於某個進程,線程沒有自己的虛擬地址空間,與進程內的其他線程一起共享分配給該進程的所有資源。

        “同時”執行是人的感覺,在線程之間實際上輪換執行。

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

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

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

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

        線程有自己的堆棧和局部變量,但線程之間沒有單獨的地址空間,一個線程包含以下內容:

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

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

        簡而言之:

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

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

二、Java中的線程

        在Java中,“線程”指兩件不同的事情:

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

        2、線程的執行。

        在 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的子類應該重寫該方法。

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

        void run()

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

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

二、實例化線程

        1、如果是擴展java.lang.Thread類的線程,則直接new即可。

        2、如果是實現了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。

三、啓動線程

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

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

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

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

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

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

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

四、例子

        1、實現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  

        2、擴展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++);是用來模擬一個非常耗時的操作的。

五、一些常見問題

        1、線程的名字,一個運行中的線程總是有名字的,名字有兩個來源,一個是虛擬機自己給的名字,一個是你自己的定的名字。在沒有指定線程名字的情況下,虛擬機總會爲線程指定名字,並且主線程的名字總是mian,非主線程的名字不確定。

        2、線程都可以設置名字,也可以獲取線程的名字,連主線程也不例外。

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

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

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

        6、一旦線程啓動,它就永遠不能再重新啓動。只有一個新的線程可以被啓動,並且只能一次。一個可運行的線程或死線程可以被重新啓動。

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

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

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

        9、儘管我們沒有無法控制線程調度程序,但可以通過別的方式來影響線程調度的方式。

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

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

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

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

 

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

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

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

Java線程:線程狀態的轉換

一、線程狀態

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

        1、新狀態:線程對象已經創建,還沒有在其上調用start()方法。

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

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

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

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

二、阻止線程執行

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

        睡眠;

        等待;

        因爲需要一個對象的鎖定而被阻塞。

        1、睡眠

        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毫秒,睡眠了,其他的線程就有機會執行了。

        注意:

        1、線程睡眠是幫助所有線程獲得運行機會的最好方法。

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

        3、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毫秒!  

        2、線程的優先級和線程讓步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:分配給線程的默認優先級。  

        3、Thread.yield()方法

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

         yield()應該做的是讓當前運行線程回到可運行狀態,以允許具有相同優先級的其他線程獲得運行機會。因此,使用yield()的目的是讓相同優先級的線程之間能適當的輪轉執行。但是,實際中無法保證yield()達到讓步目的,因爲讓步的線程還有可能被線程調度程序再次選中。

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

        4、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種方法:

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

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

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

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

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

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

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

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

Java線程:線程的同步與鎖

一、同步問題提出

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

        例如:兩個線程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關鍵字同步方法或代碼。

二、同步和鎖定

        1、鎖的原理

        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.   }  

        效果是完全一樣的。

三、靜態方法同步

        要同步靜態方法,需要一個用於整個類對象的鎖,這個對象是就是這個類(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. }  

四、如果線程不能獲得鎖會怎麼樣

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

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

        1、調用同一個對象中非靜態同步方法的線程將彼此阻塞。如果是不同對象,則每個線程有自己的對象的鎖,線程間彼此互不干預。

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

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

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

五、何時需要同步

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

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

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

        如果需要在非靜態方法中使用靜態字段,或者在靜態字段中調用非靜態方法,問題將變得非常複雜。

六、線程安全類

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

        即使是線程安全類,也應該特別小心,因爲操作的線程之間仍然不一定安全。

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

        舉個例子:

[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. }  

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

七、線程死鎖

        死鎖對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的考試範圍。

八、線程同步小結

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

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

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

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

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

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

        7、死鎖是線程間相互等待鎖鎖造成的,在實際中發生的概率非常的小。真讓你寫個死鎖程序,不一定好使,呵呵。但是,一旦程序發生死鎖,程序將死掉。

Java線程:線程的交互

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

一、線程交互的基礎知識

        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()並不意味着這時該鎖變得可用。

二、多個線程在等待一個對象鎖時候使用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  
  8. 已經生產了10個產品,現倉儲量爲30  
  9. 已經生產了10個產品,現倉儲量爲40  
  10. 要生產的產品數量80超過剩餘庫存量60,暫時不能執行生產任務!  

        說明:

        對於本例,要說明的是當發現不能滿足生產或者消費條件的時候,調用對象的wait方法,wait方法的作用是釋放當前線程的所獲得的鎖,並調用對象的notifyAll()方法,通知(喚醒)該對象上其他等待線程,使得其繼續執行。這樣,整個生產者、消費者線程得以正確的協作執行。

        notifyAll() 方法,起到的是一個通知作用,不釋放鎖,也不獲取鎖。只是告訴該對象上等待的線程“可以競爭執行了,都醒來去執行吧”。

        本例僅僅是生產者消費者模型中最簡單的一種表示,本例中,如果消費者消費的倉儲量達不到滿足,而又沒有生產者,則程序會一直處於等待狀態,這當然是不對的。實際上可以將此例進行修改,修改爲,根據消費驅動生產,同時生產兼顧倉庫,如果倉不滿就生產,並對每次最大消費量做個限制,這樣就不存在此問題了,當然這樣的例子更復雜,更難以說明這樣一個簡單模型。

Java線程:併發協作-死鎖

        線程發生死鎖可能性很小,即使看似可能發生死鎖的代碼,在運行時發生死鎖的可能性也是小之又小。

        發生死鎖的原因一般是兩個對象的鎖相互等待造成的。

        在《Java線程:線程的同步與鎖》部分,簡述了死鎖的概念與簡單例子,但是所給的例子是不完整的,這裏給出一個完整的例子。

[java] view plain copy
  1. /** 
  2.  * Java線程:併發協作-死鎖 
  3.  */  
  4. public class Test {  
  5.     public static void main(String[] args) {  
  6.        DeadlockRisk dead = new DeadlockRisk();  
  7.        MyThread t1 = new MyThread(dead, 12);  
  8.        MyThread t2 = new MyThread(dead, 34);  
  9.        MyThread t3 = new MyThread(dead, 56);  
  10.        MyThread t4 = new MyThread(dead, 78);  
  11.        t1.start();  
  12.        t2.start();  
  13.        t3.start();  
  14.        t4.start();  
  15.     }  
  16. }  
  17.    
  18. class MyThread extends Thread {  
  19.     private DeadlockRisk dead;  
  20.     private int a, b;  
  21.    
  22.     MyThread(DeadlockRisk dead, int a, int b) {  
  23.        this.dead = dead;  
  24.        this.a = a;  
  25.        this.b = b;  
  26.     }  
  27.     @Override  
  28.     public void run() {  
  29.        dead.read();  
  30.        dead.write(a, b);  
  31.     }  
  32. }  
  33.    
  34. class DeadlockRisk {  
  35.     private static class Resource {  
  36.        public int value;  
  37.     }  
  38.     private Resource resourceA = new Resource();  
  39.     private Resource resourceB = new Resource();  
  40.     public int read() {  
  41.        synchronized (resourceA) {  
  42.            System.out.println("read():" + Thread.currentThread().getName()  
  43.                   + "獲取了resourceA的鎖!");  
  44.            synchronized (resourceB) {  
  45.               System.out.println("read():" + Thread.currentThread().getName()  
  46.                      + "獲取了resourceB的鎖!");  
  47.               return resourceB.value + resourceA.value;  
  48.            }  
  49.        }  
  50.     }  
  51.     public void write(int a, int b) {  
  52.        synchronized (resourceB) {  
  53.            System.out.println("write():" + Thread.currentThread().getName()  
  54.                   + "獲取了resourceA的鎖!");  
  55.            synchronized (resourceA) {  
  56.               System.out.println("write():"  
  57.                      + Thread.currentThread().getName() + "獲取了resourceB的鎖!");  
  58.               resourceA.value = a;  
  59.               resourceB.value = b;  
  60.            }  
  61.        }  
  62.     }  
  63. }  

        執行結果:

[java] view plain copy
  1. read():Thread-1獲取了resourceA的鎖!  
  2. read():Thread-1獲取了resourceB的鎖!  
  3. write():Thread-1獲取了resourceA的鎖!  
  4. write():Thread-1獲取了resourceB的鎖!  
  5. read():Thread-3獲取了resourceA的鎖!  
  6. read():Thread-3獲取了resourceB的鎖!  
  7. write():Thread-3獲取了resourceA的鎖!  
  8. write():Thread-3獲取了resourceB的鎖!  
  9. read():Thread-2獲取了resourceA的鎖!  
  10. read():Thread-2獲取了resourceB的鎖!  
  11. write():Thread-2獲取了resourceA的鎖!  
  12. write():Thread-2獲取了resourceB的鎖!  
  13. read():Thread-0獲取了resourceA的鎖!  
  14. read():Thread-0獲取了resourceB的鎖!  
  15. write():Thread-0獲取了resourceA的鎖!  
  16. write():Thread-0獲取了resourceB的鎖!  

Java線程:volatile關鍵字

        Java語言包含兩種內在的同步機制:同步塊(或方法)和 volatile變量。這兩種機制的提出都是爲了實現代碼線程的安全性。其中 Volatile變量的同步性較差(但有時它更簡單並且開銷更低),而且其使用也更容易出錯。

        談及到volatile關鍵字,不得不提的一篇文章是:《Java理論與實踐:正確使用 Volatile 變量》,這篇文章對volatile關鍵字的用法做了相當精闢的闡述。

        之所以要單獨提出volatile這個不常用的關鍵字原因是這個關鍵字在高性能的多線程程序中也有很重要的用途,只是這個關鍵字用不好會出很多問題。

        首先考慮一個問題,爲什麼變量需要volatile來修飾呢?

        要搞清楚這個問題,首先應該明白計算機內部都做什麼了。比如做了一個i++操作,計算機內部做了三次處理:讀取-修改-寫入。

        同樣,對於一個long型數據,做了個賦值操作,在32系統下需要經過兩步才能完成,先修改低32位,然後修改高32位。

        假想一下,當將以上的操作放到一個多線程環境下操作時候,有可能出現的問題,是這些步驟執行了一部分,而另外一個線程就已經引用了變量值,這樣就導致了讀取髒數據的問題。

        通過這個設想,就不難理解volatile關鍵字了。

        volatile可以用在任何變量前面,但不能用於final變量前面,因爲final型的變量是禁止修改的。也不存在線程安全的問題。

        更多的內容,請參看:《Java理論與實踐:正確使用 Volatile 變量》一文,寫得很好。

Java線程:新特徵-線程池

        Sun在Java5中,對Java線程的類庫做了大量的擴展,其中線程池就是Java5的新特徵之一,除了線程池之外,還有很多多線程相關的內容,爲多線程的編程帶來了極大便利。爲了編寫高效穩定可靠的多線程程序,線程部分的新增內容顯得尤爲重要。

        有關Java5線程新特徵的內容全部在java.util.concurrent下面,裏面包含數目衆多的接口和類,熟悉這部分API特徵是一項艱難的學習過程。目前有關這方面的資料和書籍都少之又少,大所屬介紹線程方面書籍還停留在java5之前的知識層面上。

        當然新特徵對做多線程程序沒有必須的關係,在java5之前通用可以寫出很優秀的多線程程序。只是代價不一樣而已。

        線程池的基本思想還是一種對象池的思想,開闢一塊內存空間,裏面存放了衆多(未死亡)的線程,池中線程執行調度由池管理器來處理。當有線程任務時,從池中取一個,執行完成後線程對象歸池,這樣可以避免反覆創建線程對象所帶來的性能開銷,節省了系統的資源。

        在Java5之前,要實現一個線程池是相當有難度的,現在Java5爲我們做好了一切,我們只需要按照提供的API來使用,即可享受線程池帶來的極大便利。

        Java5的線程池分好多種:固定尺寸的線程池、單任務線程池、可變尺寸連接池、延遲連接池、單任務延遲連接池、自定義線程池。

        在使用線程池之前,必須知道如何去創建一個線程池,在Java5中,需要了解的是java.util.concurrent.Executors類的API,這個類提供大量創建連接池的靜態方法,是必須掌握的。

一、固定大小的線程池

[java] view plain copy
  1. import java.util.concurrent.ExecutorService;  
  2. import java.util.concurrent.Executors;  
  3.    
  4. /** 
  5.  * Java線程:線程池 
  6.  */  
  7. public class Test {  
  8.     public static void main(String[] args) {  
  9.        //創建一個可重用固定線程數的線程池  
  10.        ExecutorService pool =Executors.newFixedThreadPool(2);  
  11.        //創建實現了Runnable接口對象,Thread對象當然也實現了Runnable接口  
  12.        Thread t1 = new MyThread();  
  13.        Thread t2 = new MyThread();  
  14.         Thread t3 = new MyThread();  
  15.         Thread t4 = new MyThread();  
  16.         Thread t5 = new MyThread();  
  17.         //將線程放入線程池中進行執行  
  18.         pool.execute(t1);  
  19.         pool.execute(t2);  
  20.         pool.execute(t3);  
  21.         pool.execute(t4);  
  22.         pool.execute(t5);  
  23.         //關閉線程池  
  24.         pool.shutdown();  
  25.     }  
  26. }  
  27. class MyThread extends Thread{  
  28.     public void run(){  
  29.        System.out.println(Thread.currentThread().getName()+"正在執行...");  
  30.     }  
  31. }  
        執行結果:

[java] view plain copy
  1. pool-1-thread-1正在執行...  
  2. pool-1-thread-2正在執行...  
  3. pool-1-thread-2正在執行...  
  4. pool-1-thread-2正在執行...  
  5. pool-1-thread-1正在執行...  

二、單任務線程池

        在上例的基礎上改一行創建pool對象的代碼爲:

[java] view plain copy
  1. <span style="white-space:pre">    </span>//創建一個使用單個 worker線程的 Executor,以無界隊列方式來運行該線程。  
  2.         ExecutorService pool=Executors.newSingleThreadExecutor();  

        執行結果:

[java] view plain copy
  1. pool-1-thread-1正在執行...  
  2. pool-1-thread-1正在執行...  
  3. pool-1-thread-1正在執行...  
  4. pool-1-thread-1正在執行...  
  5. pool-1-thread-1正在執行...  

        對於以上兩種連接池,大小都是固定的,當要加入的池的線程(或者任務)超過池最大尺寸時候,則入此線程池需要排隊等待。

        一旦池中有線程完畢,則排隊等待的某個線程會入池執行。

三、可變尺寸的線程池

        與上面的類似,只是改動下pool的創建方式:

[java] view plain copy
  1. <span style="white-space:pre">    </span>//創建一個可根據需要創建新線程的線程池,但是在以前構造的線程可用時將重用它們。  
  2.         ExecutorService pool=Executors.newCachedThreadPool();  

        執行結果:

[java] view plain copy
  1. pool-1-thread-2正在執行...  
  2. pool-1-thread-4正在執行...  
  3. pool-1-thread-1正在執行...  
  4. pool-1-thread-3正在執行...  
  5. pool-1-thread-5正在執行...  

四、延遲連接池

[java] view plain copy
  1. import java.util.concurrent.Executors;  
  2. import java.util.concurrent.ScheduledExecutorService;  
  3. import java.util.concurrent.TimeUnit;  
  4.    
  5. /** 
  6.  * Java線程:線程池 
  7.  */  
  8. public class Test {  
  9.     public static void main(String[] args) {  
  10.        //創建一個線程池,它可安排在給定延遲後運行命令或者定期地執行。  
  11.         ScheduledExecutorServicepool=Executors.newScheduledThreadPool(2);  
  12.        //創建實現了Runnable接口對象,Thread對象當然也實現了Runnable接口  
  13.        Threadt1 = new MyThread();  
  14.        Threadt2 = new MyThread();  
  15.         Thread t3 = new MyThread();  
  16.         Thread t4 = new MyThread();  
  17.         Thread t5 = new MyThread();  
  18.         //將線程放入線程池中進行執行  
  19.         pool.execute(t1);  
  20.         pool.execute(t2);  
  21.         pool.execute(t3);  
  22.         //使用延遲執行風格的方法  
  23.         pool.schedule(t4, 10, TimeUnit.MILLISECONDS);  
  24.         pool.schedule(t5, 10, TimeUnit.MILLISECONDS);  
  25.         //關閉線程池  
  26.         pool.shutdown();  
  27.     }  
  28. }  
  29. class MyThread extends Thread{  
  30.     public void run(){  
  31.        System.out.println(Thread.currentThread().getName()+"正在執行...");  
  32.     }  
  33. }  

        執行結果:

[java] view plain copy
  1. pool-1-thread-1正在執行...  
  2. pool-1-thread-1正在執行...  
  3. pool-1-thread-1正在執行...  
  4. pool-1-thread-1正在執行...  
  5. pool-1-thread-2正在執行...  

        五、單任務延遲連接池

        在四代碼基礎上,做改動

[java] view plain copy
  1. //創建一個單任務執行線程池,它可安排在給定延遲後運行命令或者定期地執行。  
  2.  ScheduledExecutorServicepool=Executors.newSingleThreadScheduledExecutor();  

        執行結果:

[java] view plain copy
  1. pool-1-thread-1正在執行...  
  2. pool-1-thread-1正在執行...  
  3. pool-1-thread-1正在執行...  
  4. pool-1-thread-1正在執行...  
  5. pool-1-thread-1正在執行...  

        六、自定義線程池

[java] view plain copy
  1. import java.util.concurrent.ArrayBlockingQueue;  
  2. import java.util.concurrent.BlockingQueue;  
  3. import java.util.concurrent.ThreadPoolExecutor;  
  4. import java.util.concurrent.TimeUnit;  
  5.    
  6. /** 
  7.  * Java線程:線程池-自定義線程池 
  8.  */  
  9. public class Test {  
  10.     public static void main(String[] args) {  
  11.        //創建等待隊列  
  12.        BlockingQueue<Runnable> bqueue=newArrayBlockingQueue<Runnable>(20);  
  13.        //創建一個單線程執行任務,它可安排在給定延遲後運行命令或者定期地執行。  
  14.         ThreadPoolExecutor pool=newThreadPoolExecutor(232, TimeUnit.MILLISECONDS, bqueue);  
  15.        //創建實現了Runnable接口對象,Thread對象當然也實現了Runnable接口  
  16.        Thread t1 = new MyThread();  
  17.        Thread t2 = new MyThread();  
  18.         Thread t3 = new MyThread();  
  19.         Thread t4 = new MyThread();  
  20.         Thread t5 = new MyThread();  
  21.         Thread t6 = new MyThread();  
  22.         Thread t7 = new MyThread();  
  23.         //將線程放入線程池中進行執行  
  24.         pool.execute(t1);  
  25.         pool.execute(t2);  
  26.         pool.execute(t3);  
  27.         pool.execute(t4);  
  28.         pool.execute(t5);  
  29.         pool.execute(t6);  
  30.         pool.execute(t7);  
  31.         //關閉線程池  
  32.         pool.shutdown();  
  33.     }  
  34. }  
  35. class MyThread extends Thread{  
  36.     public void run(){  
  37.        System.out.println(Thread.currentThread().getName()+"正在執行...");  
  38.        try {  
  39.            Thread.sleep(100L);  
  40.        } catch (InterruptedException e) {  
  41.            e.printStackTrace();  
  42.        }  
  43.     }  
  44. }  

        執行結果:

[java] view plain copy
  1. pool-1-thread-1正在執行...  
  2. pool-1-thread-2正在執行...  
  3. pool-1-thread-1正在執行...  
  4. pool-1-thread-2正在執行...  
  5. pool-1-thread-1正在執行...  
  6. pool-1-thread-2正在執行...  
  7. pool-1-thread-1正在執行...  

        創建自定義線程池的構造方法很多,本例中參數ThreadPoolExecutor的含義如下:

[java] view plain copy
  1. public ThreadPoolExecutor(int corePoolSize,  
  2.                          int maximumPoolSize,  
  3.                          long keepAliveTime,  
  4.                           TimeUnit unit,  
  5.                          BlockingQueue<Runnable> workQueue)  

        用給定的初始參數和默認的線程工廠及處理程序創建新的ThreadPoolExecutor。使用Executors工廠方法之一比使用此通用構造方法方便得多。

        參數:

[java] view plain copy
  1. corePoolSize -池中所保存的線程數,包括空閒線程。  
  2. maximumPoolSize -池中允許的最大線程數。  
  3. keepAliveTime -當線程數大於核心時,此爲終止前多餘的空閒線程等待新任務的最長時間。  
  4. unit -keepAliveTime參數的時間單位。  
  5. workQueue -執行前用於保持任務的隊列。此隊列僅保持由execute方法提交的Runnable任務。  

        拋出:

[java] view plain copy
  1. IllegalArgumentException-如果 corePoolSize或 keepAliveTime小於零,或者 maximumPoolSize小於或等於零,或者 corePoolSize大於 maximumPoolSize。  
  2. NullPointerException-如果workQueue爲 null  
  3. 自定義連接池稍微麻煩些,不過通過創建的ThreadPoolExecutor線程池對象,可以獲取到當前線程池的尺寸、正在執行任務的線程數、工作隊列等等。  

Java線程:新特徵-有返回值的線程

        在Java5之前,線程是沒有返回值的,常常爲了“有”返回值,破費周折,而且代碼很不好寫。或者乾脆繞過這道坎,走別的路了。現在Java終於有可返回值的任務(也可以叫做線程)了。

        可返回值的任務必須實現Callable接口,類似的,無返回值的任務必須Runnable接口。

        執行Callable任務後,可以獲取一個Future的對象,在該對象上調用get就可以獲取到Callable任務返回的Object了。

        下面是個很簡單的例子:

[java] view plain copy
  1. import java.util.concurrent.Callable;  
  2. import java.util.concurrent.ExecutionException;  
  3. import java.util.concurrent.ExecutorService;  
  4. import java.util.concurrent.Executors;  
  5. import java.util.concurrent.Future;  
  6.    
  7. /** 
  8.  * Java線程:線程池-有返回值的線程 
  9.  */  
  10. public class Test {  
  11.     public static void main(String[] args)throwsExecutionException,InterruptedException {  
  12.        //創建一個線程池  
  13.        ExecutorService pool = Executors.newFixedThreadPool(2);  
  14.        //創建兩個有返回值的任務  
  15.        Callable c1=new MyCallable("A");  
  16.        Callable c2=new MyCallable("B");  
  17.        //執行任務並獲取Future對象  
  18.        Future f1=pool.submit(c1);  
  19.        Future f2=pool.submit(c2);  
  20.        //從Future對象上獲取任務的返回值,並輸出到控制檯  
  21.        System.out.println(">>>"+f1.get().toString());  
  22.        System.out.println(">>>"+f2.get().toString());  
  23.         //關閉線程池  
  24.         pool.shutdown();  
  25.     }  
  26. }  
  27. class MyCallable implements Callable{  
  28.     private String oid;   
  29.     MyCallable(String oid) {  
  30.        this.oid = oid;  
  31.     }  
  32.     @Override  
  33.     public Object call() throws Exception {  
  34.        return oid+"任務返回的內容";  
  35.         
  36.     }    
  37. }  

        執行結果:

[java] view plain copy
  1. >>>A任務返回的內容  
  2. >>>B任務返回的內容  

Java線程:新特徵-鎖(上)

        在Java5中,專門提供了鎖對象,利用鎖可以方便的實現資源的封鎖,用來控制對競爭資源併發訪問的控制,這些內容主要集中在java.util.concurrent.locks包下面,裏面有三個重要的接口Condition、Lock、ReadWriteLock。

[java] view plain copy
  1. Condition將Object監視器方法(wait、notify和 notifyAll)分解成截然不同的對象,以便通過將這些對象與任意Lock實現組合使用,爲每個對象提供多個等待 set(wait-set)。  
  2. Lock實現提供了比使用synchronized方法和語句可獲得的更廣泛的鎖定操作。  
  3. ReadWriteLock維護了一對相關的鎖定,一個用於只讀操作,另一個用於寫入操作。  

        舉個例子:

[java] view plain copy
  1. import java.util.concurrent.ExecutorService;  
  2. import java.util.concurrent.Executors;  
  3. import java.util.concurrent.locks.Lock;  
  4. import java.util.concurrent.locks.ReentrantLock;  
  5.    
  6. /** 
  7.  * Java線程:線程池-鎖 
  8.  */  
  9. public class Test {  
  10.     public static void main(String[] args){  
  11.        //創建併發訪問的賬戶  
  12.        MyCount myCount=new MyCount("6215580000000000000",10000);  
  13.        //創建一個鎖對象  
  14.        Lock lock=new ReentrantLock();  
  15.        //創建一個線程池  
  16.        ExecutorService pool = Executors.newCachedThreadPool();  
  17.        //創建一些併發訪問用戶,一個信用卡,存的存,取的取  
  18.        User u1 = new User("張三", myCount, -4000, lock);  
  19.         User u2 = new User("張三他爹", myCount, 6000, lock);  
  20.         User u3 = new User("張三他弟", myCount, -8000, lock);  
  21.         User u4 = new User("張三", myCount, 800, lock);  
  22.         //在線程池中執行各個用戶的操作  
  23.         pool.execute(u1);  
  24.         pool.execute(u2);  
  25.         pool.execute(u3);  
  26.         pool.execute(u4);  
  27.         //關閉線程池  
  28.         pool.shutdown();  
  29.     }  
  30. }  
  31. //信用卡用戶  
  32. class User implements Runnable{  
  33.      private String name;                //用戶名  
  34.      private MyCount myCount;        //所要操作的賬戶  
  35.      private int iocash;                //操作的金額,當然有正負之分了  
  36.      private Lock myLock;                //執行操作所需的鎖對象  
  37.      User(String name, MyCount myCount, int iocash, LockmyLock) {  
  38.          this.name = name;  
  39.          this.myCount = myCount;  
  40.          this.iocash = iocash;  
  41.          this.myLock = myLock;  
  42.  }  
  43.     @Override  
  44.     public void run() {  
  45.        //獲取鎖  
  46.         myLock.lock();  
  47.         //執行現金業務  
  48.         System.out.println(name + "正在操作" + myCount +"賬戶,金額爲" + iocash +",當前金額爲" + myCount.getCash());  
  49.         myCount.setCash(myCount.getCash() + iocash);  
  50.         System.out.println(name + "操作" + myCount +"賬戶成功,金額爲" + iocash +",當前金額爲" + myCount.getCash());  
  51.         //釋放鎖,否則別的線程沒有機會執行了  
  52.         myLock.unlock();  
  53.     }    
  54. }  
  55. //信用卡賬戶,可隨意透支  
  56. class MyCount {  
  57.     private String oid;        //賬號  
  58.     private int cash;            //賬戶餘額  
  59.     MyCount(String oid, int cash) {  
  60.        this.oid = oid;  
  61.        this.cash = cash;  
  62.     }  
  63.     public String getOid() {  
  64.        return oid;  
  65.     }  
  66.     public void setOid(String oid) {  
  67.        this.oid = oid;  
  68.     }  
  69.     public int getCash() {  
  70.        return cash;  
  71.     }  
  72.     public void setCash(int cash) {  
  73.        this.cash = cash;  
  74.     }  
  75.     @Override  
  76.     public String toString() {  
  77.             return"MyCount{" +  
  78.                             "oid='" + oid + '\'' +  
  79.                             ", cash=" + cash +  
  80.                             '}';  
  81.     }  
  82. }  

        執行結果:

[java] view plain copy
  1. 張三他爹正在操作MyCount{oid='6215580000000000000', cash=10000}賬戶,金額爲6000,當前金額爲10000  
  2. 張三他爹操作MyCount{oid='6215580000000000000', cash=16000}賬戶成功,金額爲6000,當前金額爲16000  
  3. 張三正在操作MyCount{oid='6215580000000000000', cash=16000}賬戶,金額爲800,當前金額爲16000  
  4. 張三操作MyCount{oid='6215580000000000000',cash=16800}賬戶成功,金額爲800,當前金額爲16800  
  5. 張三正在操作MyCount{oid='6215580000000000000', cash=16800}賬戶,金額爲-4000,當前金額爲16800  
  6. 張三操作MyCount{oid='6215580000000000000',cash=12800}賬戶成功,金額爲-4000,當前金額爲12800  
  7. 張三他弟正在操作MyCount{oid='6215580000000000000', cash=12800}賬戶,金額爲-8000,當前金額爲12800  
  8. 張三他弟操作MyCount{oid='6215580000000000000', cash=4800}賬戶成功,金額爲-8000,當前金額爲4800  

        從上面的輸出可以看到,利用鎖對象太方便了,比直接在某個不知情的對象上用鎖清晰多了。

        但一定要注意的是,在獲取了鎖對象後,用完後應該儘快釋放鎖,以便別的等待該鎖的線程有機會去執行

Java線程:新特徵-鎖(下)

        在上文中提到了Lock接口以及對象,使用它,很優雅的控制了競爭資源的安全訪問,但是這種鎖不區分讀寫,稱這種鎖爲普通鎖。爲了提高性能,Java提供了讀寫鎖,在讀的地方使用讀鎖,在寫的地方使用寫鎖,靈活控制,在一定程度上提高了程序的執行效率。

        Java中讀寫鎖有個接口java.util.concurrent.locks.ReadWriteLock,也有具體的實現ReentrantReadWriteLock,詳細的API可以查看JavaAPI文檔。

        下面這個例子是在文例子的基礎上,將普通鎖改爲讀寫鎖,並添加賬戶餘額查詢的功能,代碼如下:

[java] view plain copy
  1. import java.util.concurrent.ExecutorService;  
  2. import java.util.concurrent.Executors;  
  3. import java.util.concurrent.locks.ReadWriteLock;  
  4. import java.util.concurrent.locks.ReentrantReadWriteLock;  
  5.    
  6. /** 
  7.  * Java線程:線程池-鎖 
  8.  */  
  9. public class Test {  
  10.     public static void main(String[] args){  
  11.        //創建併發訪問的賬戶  
  12.        MyCount myCount=new MyCount("6215580000000000000",10000);  
  13.        //創建一個鎖對象  
  14.        ReadWriteLock lock=new ReentrantReadWriteLock(false);  
  15.        //創建一個線程池  
  16.        ExecutorService pool = Executors.newCachedThreadPool();  
  17.        //創建一些併發訪問用戶,一個信用卡,存的存,取的取  
  18.        User u1 = new User("張三", myCount, -4000, lock, false);  
  19.         User u2 = new User("張三他爹", myCount, 6000, lock, false);  
  20.         User u3 = new User("張三他弟", myCount, -8000, lock, false);  
  21.         User u4 = new User("張三", myCount, 800, lock,false);  
  22.         User u5 = new User("張三他爹", myCount, 0, lock,true);  
  23.         //在線程池中執行各個用戶的操作  
  24.         pool.execute(u1);  
  25.         pool.execute(u2);  
  26.         pool.execute(u3);  
  27.         pool.execute(u4);  
  28.         pool.execute(u5);  
  29.         //關閉線程池  
  30.         pool.shutdown();  
  31.     }  
  32. }  
  33. //信用卡用戶  
  34. class User implements Runnable{  
  35.     private String name;                //用戶名  
  36.     private MyCount myCount;        //所要操作的賬戶  
  37.     private int iocash;                //操作的金額,當然有正負之分了  
  38.     private ReadWriteLock myLock;                //執行操作所需的鎖對象  
  39.     private boolean ischeck;       //是否查詢  
  40.     User(String name, MyCount myCount, int iocash,ReadWriteLock myLock,boolean ischeck) {  
  41.         this.name = name;  
  42.         this.myCount = myCount;  
  43.         this.iocash = iocash;  
  44.         this.myLock = myLock;  
  45.         this.ischeck = ischeck;  
  46. }  
  47.     @Override  
  48.     public void run() {  
  49.        if (ischeck) {  
  50.             //獲取讀鎖  
  51.             myLock.readLock().lock();  
  52.             System.out.println("讀:" + name +"正在查詢" + myCount +"賬戶,當前金額爲" + myCount.getCash());  
  53.             //釋放讀鎖  
  54.             myLock.readLock().unlock();  
  55.        } else {  
  56.             //獲取寫鎖  
  57.             myLock.writeLock().lock();  
  58.             //執行現金業務  
  59.             System.out.println("寫:" + name +"正在操作" + myCount +"賬戶,金額爲" + iocash +",當前金額爲" + myCount.getCash());  
  60.            myCount.setCash(myCount.getCash() + iocash);  
  61.             System.out.println("寫:" + name +"操作" + myCount +"賬戶成功,金額爲" + iocash +",當前金額爲" + myCount.getCash());  
  62.             //釋放寫鎖  
  63.             myLock.writeLock().unlock();  
  64.        }  
  65.     }    
  66. }  
  67. //信用卡賬戶,可隨意透支  
  68. class MyCount {  
  69.     private String oid;        //賬號  
  70.     private int cash;            //賬戶餘額  
  71.     MyCount(String oid, int cash) {  
  72.        this.oid = oid;  
  73.        this.cash = cash;  
  74.     }  
  75.     public String getOid() {  
  76.        return oid;  
  77.     }  
  78.     public void setOid(String oid) {  
  79.        this.oid = oid;  
  80.     }  
  81.     public int getCash() {  
  82.        return cash;  
  83.     }  
  84.     public void setCash(int cash) {  
  85.        this.cash = cash;  
  86.     }  
  87.     @Override  
  88.     public String toString() {  
  89.             return"MyCount{" +  
  90.                             "oid='" + oid + '\'' +  
  91.                             ",cash=" + cash +  
  92.                             '}';  
  93.     }  
  94. }  

        執行結果:

[java] view plain copy
  1. 寫:張三他爹正在操作MyCount{oid='6215580000000000000', cash=10000}賬戶,金額爲6000,當前金額爲10000  
  2. 寫:張三他爹操作MyCount{oid='6215580000000000000', cash=16000}賬戶成功,金額爲6000,當前金額爲16000  
  3. 寫:張三正在操作MyCount{oid='6215580000000000000', cash=16000}賬戶,金額爲-4000,當前金額爲16000  
  4. 寫:張三操作MyCount{oid='6215580000000000000', cash=12000}賬戶成功,金額爲-4000,當前金額爲12000  
  5. 寫:張三他弟正在操作MyCount{oid='6215580000000000000', cash=12000}賬戶,金額爲-8000,當前金額爲12000  
  6. 寫:張三他弟操作MyCount{oid='6215580000000000000', cash=4000}賬戶成功,金額爲-8000,當前金額爲4000  
  7. 讀:張三他爹正在查詢MyCount{oid='6215580000000000000', cash=4000}賬戶,當前金額爲4000  
  8. 寫:張三正在操作MyCount{oid='6215580000000000000', cash=4000}賬戶,金額爲800,當前金額爲4000  
  9. 寫:張三操作MyCount{oid='6215580000000000000', cash=4800}賬戶成功,金額爲800,當前金額爲4800  

        在實際開發中,最好在能用讀寫鎖的情況下使用讀寫鎖,而不要用普通鎖,以求更好的性能。

Java線程:新特徵-信號量

        Java的信號量實際上是一個功能完畢的計數器,對控制一定資源的消費與回收有着很重要的意義,信號量常常用於多線程的代碼中,並能監控有多少數目的線程等待獲取資源,並且通過信號量可以得知可用資源的數目等等,這裏總是在強調“數目”二字,但不能指出來有哪些在等待,哪些資源可用。

        因此,本人認爲,這個信號量類如果能返回數目,還能知道哪些對象在等待,哪些資源可使用,就非常完美了,僅僅拿到這些概括性的數字,對精確控制意義不是很大。目前還沒想到更好的用法。

        下面是一個簡單例子:

[java] view plain copy
  1. import java.util.concurrent.ExecutorService;  
  2. import java.util.concurrent.Executors;  
  3. import java.util.concurrent.Semaphore;  
  4.    
  5. /** 
  6.  * Java線程:線程池-信號量 
  7.  */  
  8. public class Test {  
  9.     public static void main(String[] args){  
  10.        MyPool myPool = new MyPool(20);  
  11.         //創建線程池  
  12.         ExecutorService threadPool = Executors.newFixedThreadPool(2);  
  13.         MyThread t1 = new MyThread("任務A", myPool, 3);  
  14.         MyThread t2 = new MyThread("任務B", myPool, 12);  
  15.         MyThread t3 = new MyThread("任務C", myPool, 7);  
  16.         //在線程池中執行任務  
  17.         threadPool.execute(t1);  
  18.         threadPool.execute(t2);  
  19.         threadPool.execute(t3);  
  20.         //關閉池  
  21.         threadPool.shutdown();  
  22.     }  
  23. }  
  24. class MyPool {  
  25.     private Semaphore sp;    //池相關的信號量  
  26.    
  27.     /** 
  28.      * 池的大小,這個大小會傳遞給信號量 
  29.      * @param size 池的大小 
  30.      */  
  31.     MyPool(int size) {  
  32.             this.sp =new Semaphore(size);  
  33.     }  
  34.    
  35.     public Semaphore getSp() {  
  36.             return sp;  
  37.     }  
  38.    
  39.     public void setSp(Semaphore sp) {  
  40.             this.sp = sp;  
  41.     }  
  42. }  
  43. class MyThread extends Thread {  
  44.     private String threadname;            //線程的名稱  
  45.     private MyPool pool;                 //自定義池  
  46.     private int x;                      //申請信號量的大小  
  47.    
  48.     MyThread(String threadname, MyPool pool, int x) {  
  49.             this.threadname = threadname;  
  50.             this.pool = pool;  
  51.             this.x = x;  
  52.     }  
  53.     public void run() {  
  54.             try {  
  55.                     //從此信號量獲取給定數目的許可  
  56.                     pool.getSp().acquire(x);  
  57.                     //todo:也許這裏可以做更復雜的業務  
  58.                     System.out.println(threadname + "成功獲取了" + x +"個許可!");  
  59.             } catch (InterruptedException e) {  
  60.                     e.printStackTrace();  
  61.             } finally {  
  62.                     //釋放給定數目的許可,將其返回到信號量。  
  63.                     pool.getSp().release(x);  
  64.                     System.out.println(threadname + "釋放了" + x +"個許可!");  
  65.             }  
  66.     }  
  67. }  

        執行結果:

[java] view plain copy
  1. 任務A成功獲取了3個許可!  
  2. 任務B成功獲取了12個許可!  
  3. 任務B釋放了12個許可!  
  4. 任務A釋放了3個許可!  
  5. 任務C成功獲取了7個許可!  
  6. 任務C釋放了7個許可!  

        從結果可以看出,信號量僅僅是對池資源進行監控,但不保證線程的安全,因此,在使用時候,應該自己控制線程的安全訪問池資源。

Java線程:新特徵-阻塞隊列

        阻塞隊列是Java5線程新特徵中的內容,Java定義了阻塞隊列的接口java.util.concurrent.BlockingQueue,阻塞隊列的概念是,一個指定長度的隊列,如果隊列滿了,添加新元素的操作會被阻塞等待,直到有空位爲止。同樣,當隊列爲空時候,請求隊列元素的操作同樣會阻塞等待,直到有可用元素爲止。 

        有了這樣的功能,就爲多線程的排隊等候的模型實現開闢了便捷通道,非常有用。 

        java.util.concurrent.BlockingQueue繼承了java.util.Queue接口,可以參看API文檔。 

        下面給出一個簡單應用的例子:

[java] view plain copy
  1. import java.util.concurrent.ArrayBlockingQueue;  
  2. import java.util.concurrent.BlockingQueue;  
  3.    
  4. /** 
  5.  * Java線程:線程池-阻塞隊列 
  6.  */  
  7. public class Test {  
  8.     public static void main(String[] args) throws InterruptedException{  
  9.        BlockingQueue bqueue = new ArrayBlockingQueue(20);  
  10.         for (int i = 0; i < 30; i++) {  
  11.                 //將指定元素添加到此隊列中,如果沒有可用空間,將一直等待(如果有必要)。  
  12.                 bqueue.put(i);  
  13.                 System.out.println("向阻塞隊列中添加了元素:" + i);  
  14.         }  
  15.         System.out.println("程序到此運行結束,即將退出----");  
  16.     }  
  17. }  

        執行結果:

[java] view plain copy
  1. 向阻塞隊列中添加了元素:0  
  2. 向阻塞隊列中添加了元素:1  
  3. 向阻塞隊列中添加了元素:2  
  4. 向阻塞隊列中添加了元素:3  
  5. 向阻塞隊列中添加了元素:4  
  6. 向阻塞隊列中添加了元素:5  
  7. 向阻塞隊列中添加了元素:6  
  8. 向阻塞隊列中添加了元素:7  
  9. 向阻塞隊列中添加了元素:8  
  10. 向阻塞隊列中添加了元素:9  
  11. 向阻塞隊列中添加了元素:10  
  12. 向阻塞隊列中添加了元素:11  
  13. 向阻塞隊列中添加了元素:12  
  14. 向阻塞隊列中添加了元素:13  
  15. 向阻塞隊列中添加了元素:14  
  16. 向阻塞隊列中添加了元素:15  
  17. 向阻塞隊列中添加了元素:16  
  18. 向阻塞隊列中添加了元素:17  
  19. 向阻塞隊列中添加了元素:18  
  20. 向阻塞隊列中添加了元素:19  

        可以看出,輸出到元素19時候,就一直處於等待狀態,因爲隊列滿了,程序阻塞了。

        這裏沒有用多線程來演示,沒有這個必要。

        另外,阻塞隊列還有更多實現類,用來滿足各種複雜的需求:ArrayBlockingQueue, DelayQueue,LinkedBlockingQueue, PriorityBlockingQueue, SynchronousQueue,具體的API差別也很小。

Java線程:新特徵-阻塞棧

        對於阻塞棧,與阻塞隊列相似。不同點在於棧是“後入先出”的結構,每次操作的是棧頂,而隊列是“先進先出”的結構,每次操作的是隊列頭。

        這裏要特別說明一點的是,阻塞棧是Java6的新特徵。

        Java爲阻塞棧定義了接口:java.util.concurrent.BlockingDeque,其實現類也比較多,具體可以查看JavaAPI文檔。

        下面看一個簡單例子:

[java] view plain copy
  1. import java.util.concurrent.BlockingDeque;  
  2. import java.util.concurrent.LinkedBlockingDeque;  
  3.    
  4. /** 
  5.  * Java線程:線程池-阻塞棧 
  6.  */  
  7. public class Test {  
  8.     public static void main(String[] args) throws InterruptedException{  
  9.        BlockingDeque bDeque = new LinkedBlockingDeque(20);  
  10.         for (int i = 0; i < 30; i++) {  
  11.         //將指定元素添加到此阻塞棧中,如果沒有可用空間,將一直等待(如果有必要)。  
  12.             bDeque.putFirst(i);  
  13.             System.out.println("向阻塞棧中添加了元素:" + i);  
  14.         }  
  15.         System.out.println("程序到此運行結束,即將退出----");  
  16.     }  
  17. }  

        執行結果:

[java] view plain copy
  1. 向阻塞棧中添加了元素:0  
  2. 向阻塞棧中添加了元素:1  
  3. 向阻塞棧中添加了元素:2  
  4. 向阻塞棧中添加了元素:3  
  5. 向阻塞棧中添加了元素:4  
  6. 向阻塞棧中添加了元素:5  
  7. 向阻塞棧中添加了元素:6  
  8. 向阻塞棧中添加了元素:7  
  9. 向阻塞棧中添加了元素:8  
  10. 向阻塞棧中添加了元素:9  
  11. 向阻塞棧中添加了元素:10  
  12. 向阻塞棧中添加了元素:11  
  13. 向阻塞棧中添加了元素:12  
  14. 向阻塞棧中添加了元素:13  
  15. 向阻塞棧中添加了元素:14  
  16. 向阻塞棧中添加了元素:15  
  17. 向阻塞棧中添加了元素:16  
  18. 向阻塞棧中添加了元素:17  
  19. 向阻塞棧中添加了元素:18  
  20. 向阻塞棧中添加了元素:19  
        從上面結果可以看到,程序並沒結束,而是阻塞住了,原因是棧已經滿了,後面追加元素的操作都被阻塞了。

Java線程:新特徵-條件變量

        條件變量是Java5線程中很重要的一個概念,顧名思義,條件變量就是表示條件的一種變量。但是必須說明,這裏的條件是沒有實際含義的,僅僅是個標記而已,並且條件的含義往往通過代碼來賦予其含義。

        這裏的條件和普通意義上的條件表達式有着天壤之別。

        條件變量都實現了java.util.concurrent.locks.Condition接口,條件變量的實例化是通過一個Lock對象上調用newCondition()方法來獲取的,這樣,條件就和一個鎖對象綁定起來了。因此,Java中的條件變量只能和鎖配合使用,來控制併發程序訪問競爭資源的安全。

        條件變量的出現是爲了更精細控制線程等待與喚醒,在Java5之前,線程的等待與喚醒依靠的是Object對象的wait()和notify()/notifyAll()方法,這樣的處理不夠精細。

        而在Java5中,一個鎖可以有多個條件,每個條件上可以有多個線程等待,通過調用await()方法,可以讓線程在該條件下等待。當調用signalAll()方法,又可以喚醒該條件下的等待的線程。有關Condition接口的API可以具體參考JavaAPI文檔。

        條件變量比較抽象,原因是他不是自然語言中的條件概念,而是程序控制的一種手段。

        下面以一個銀行存取款的模擬程序爲例來揭蓋Java多線程條件變量的神祕面紗:

        有一個賬戶,多個用戶(線程)在同時操作這個賬戶,有的存款有的取款,存款隨便存,取款有限制,不能透支,任何試圖透支的操作都將等待裏面有足夠存款才執行操作。

[java] view plain copy
  1. import java.util.concurrent.ExecutorService;  
  2. import java.util.concurrent.Executors;  
  3. import java.util.concurrent.locks.Condition;  
  4. import java.util.concurrent.locks.Lock;  
  5. import java.util.concurrent.locks.ReentrantLock;  
  6.    
  7. /** 
  8.  * Java線程:條件變量 
  9.  */  
  10. public class Test {  
  11.     public static void main(String[] args){  
  12.        //創建併發訪問的賬戶  
  13.        MyCount myCount=new MyCount("6215580000000000000",10000);  
  14.        //創建一個線程池  
  15.        ExecutorService pool = Executors.newFixedThreadPool(2);  
  16.        Thread t1 = new SaveThread("張三", myCount, 2000);  
  17.         Thread t2 = new SaveThread("李四", myCount, 3600);  
  18.         Thread t3 = new DrawThread("王五", myCount, 2700);  
  19.         Thread t4 = new SaveThread("老張", myCount, 600);  
  20.         Thread t5 = new DrawThread("老牛", myCount, 1300);  
  21.         Thread t6 = new DrawThread("胖子", myCount, 800);  
  22.         //執行各個線程  
  23.         pool.execute(t1);  
  24.         pool.execute(t2);  
  25.         pool.execute(t3);  
  26.         pool.execute(t4);  
  27.         pool.execute(t5);  
  28.         pool.execute(t6);  
  29.         //關閉線程池  
  30.         pool.shutdown();  
  31.     }  
  32. }  
  33. //存款線程類  
  34. class SaveThread extends Thread {  
  35.     private String name;            //操作人  
  36.     private MyCount myCount;        //賬戶  
  37.     private int x;                  //存款金額  
  38.      
  39.     SaveThread(String name, MyCount myCount, int x) {  
  40.             this.name = name;  
  41.             this.myCount = myCount;  
  42.             this.x = x;  
  43.     }  
  44.     public void run() {  
  45.             myCount.saving(x, name);  
  46.     }  
  47. }  
  48. //取款線程類  
  49. class DrawThread extends Thread {  
  50.     private String name;                //操作人  
  51.     private MyCount myCount;        //賬戶  
  52.     private int x;                            //存款金額  
  53.    
  54.     DrawThread(String name, MyCount myCount, int x) {  
  55.             this.name = name;  
  56.             this.myCount = myCount;  
  57.             this.x = x;  
  58.     }  
  59.     public void run() {  
  60.             myCount.drawing(x, name);  
  61.     }  
  62. }  
  63. //普通銀行賬戶,不可透支  
  64. class MyCount {  
  65.      private String oid;                        //賬號  
  66.      private int cash;                            //賬戶餘額  
  67.      private Lock lock =new ReentrantLock();         //賬戶鎖  
  68.      private Condition _save = lock.newCondition();    //存款條件  
  69.      private Condition _draw = lock.newCondition();    //取款條件  
  70.      MyCount(String oid, int cash) {       
  71.          this.oid = oid;  
  72.          this.cash = cash;  
  73.      }  
  74.      /** 
  75.       * 存款 
  76.       * @param x 存款金額 
  77.       * @param name 存款人 
  78.       */  
  79.      public void saving(int x,String name){  
  80.          lock.lock();                        //獲取鎖  
  81.        if (x > 0) {  
  82.            cash += x; // 存款  
  83.            System.out.println(name + "存款" + x + ",當前餘額爲" + cash);  
  84.        }  
  85.          _draw.signalAll();            //喚醒所有等待線程。  
  86.          lock.unlock();                    //釋放鎖  
  87.      }  
  88.      //取款  
  89.      public void drawing(int x, String name) {  
  90.      lock.lock();                                 //獲取鎖  
  91.        try {  
  92.            if (cash - x < 0) {  
  93.               _draw.await(); // 阻塞取款操作  
  94.            } else {  
  95.               cash -= x; // 取款  
  96.               System.out.println(name + "取款" + x + ",當前餘額爲" + cash);  
  97.            }  
  98.            _save.signalAll(); // 喚醒所有存款操作  
  99.        } catch (InterruptedException e) {  
  100.            e.printStackTrace();  
  101.        } finally {  
  102.            lock.unlock(); // 釋放鎖  
  103.        }  
  104.      }  
  105. }  

        執行結果:

[java] view plain copy
  1. 張三存款2000,當前餘額爲12000  
  2. 王五取款2700,當前餘額爲9300  
  3. 老張存款600,當前餘額爲9900  
  4. 老牛取款1300,當前餘額爲8600  
  5. 胖子取款800,當前餘額爲7800  
  6. 李四存款3600,當前餘額爲11400  

        假如我們不用鎖和條件變量,如何實現此功能呢?下面是實現代碼:

[java] view plain copy
  1. import java.util.concurrent.ExecutorService;  
  2. import java.util.concurrent.Executors;  
  3.    
  4. /** 
  5.  * Java線程:不用條件變量 
  6.  */  
  7. public class Test {  
  8.     public static void main(String[] args){  
  9.        //創建併發訪問的賬戶  
  10.        MyCount myCount=new MyCount("6215580000000000000",10000);  
  11.        //創建一個線程池  
  12.        ExecutorService pool = Executors.newFixedThreadPool(2);  
  13.        Thread t1 = new SaveThread("張三", myCount, 2000);  
  14.         Thread t2 = new SaveThread("李四", myCount, 3600);  
  15.         Thread t3 = new DrawThread("王五", myCount, 2700);  
  16.         Thread t4 = new SaveThread("老張", myCount, 600);  
  17.         Thread t5 = new DrawThread("老牛", myCount, 1300);  
  18.         Thread t6 = new DrawThread("胖子", myCount, 800);  
  19.         //執行各個線程  
  20.         pool.execute(t1);  
  21.         pool.execute(t2);  
  22.         pool.execute(t3);  
  23.         pool.execute(t4);  
  24.         pool.execute(t5);  
  25.         pool.execute(t6);  
  26.         //關閉線程池  
  27.         pool.shutdown();  
  28.     }  
  29. }  
  30. //存款線程類  
  31. class SaveThread extends Thread {  
  32.     private String name;            //操作人  
  33.     private MyCount myCount;        //賬戶  
  34.     private int x;                  //存款金額  
  35.      
  36.     SaveThread(String name, MyCount myCount, int x) {  
  37.             this.name = name;  
  38.             this.myCount = myCount;  
  39.             this.x = x;  
  40.     }  
  41.     public void run() {  
  42.             myCount.saving(x, name);  
  43.     }  
  44. }  
  45. //取款線程類  
  46. class DrawThread extends Thread {  
  47.     private String name;                //操作人  
  48.     private MyCount myCount;        //賬戶  
  49.     private int x;                            //存款金額  
  50.    
  51.     DrawThread(String name, MyCount myCount, int x) {  
  52.             this.name = name;  
  53.             this.myCount = myCount;  
  54.             this.x = x;  
  55.     }  
  56.     public void run() {  
  57.             myCount.drawing(x, name);  
  58.     }  
  59. }  
  60. //普通銀行賬戶,不可透支  
  61. class MyCount {  
  62.      private String oid;                        //賬號  
  63.      private int cash;                            //賬戶餘額  
  64.      MyCount(String oid, int cash) {       
  65.          this.oid = oid;  
  66.          this.cash = cash;  
  67.      }  
  68.      /** 
  69.       * 存款 
  70.       * @param x 存款金額 
  71.       * @param name 存款人 
  72.       */  
  73.      public synchronized void saving(int x,Stringname){         
  74.        if (x > 0) {  
  75.            cash += x; // 存款  
  76.            System.out.println(name + "存款" + x + ",當前餘額爲" + cash);  
  77.        }  
  78.        notifyAll(); //喚醒所有等待線程。  
  79.      }  
  80.      //取款  
  81.      public synchronized void drawing(int x, String name){       
  82.        if (cash - x < 0) {  
  83.            try {  
  84.               wait();  
  85.            } catch (InterruptedException e) {  
  86.               e.printStackTrace();  
  87.            }  
  88.        } else {  
  89.            cash -= x; // 取款  
  90.            System.out.println(name + "取款" + x + ",當前餘額爲" + cash);  
  91.        }  
  92.        notifyAll(); // 喚醒所有存款操作       
  93.      }  
  94. }  

        執行結果:

[java] view plain copy
  1. 張三存款2000,當前餘額爲12000  
  2. 王五取款2700,當前餘額爲9300  
  3. 李四存款3600,當前餘額爲12900  
  4. 老牛取款1300,當前餘額爲11600  
  5. 胖子取款800,當前餘額爲10800  
  6. 老張存款600,當前餘額爲11400  

        結合先前同步代碼知識,舉一反三,將此例改爲同步代碼塊來實現,代碼如下:

[java] view plain copy
  1. import java.util.concurrent.ExecutorService;  
  2. import java.util.concurrent.Executors;  
  3.    
  4. /** 
  5.  * Java線程:改用同步代碼塊 
  6.  */  
  7. public class Test {  
  8.     public static void main(String[] args){  
  9.        //創建併發訪問的賬戶  
  10.        MyCount myCount=new MyCount("6215580000000000000",10000);  
  11.        //創建一個線程池  
  12.        ExecutorService pool = Executors.newFixedThreadPool(2);  
  13.        Thread t1 = new SaveThread("張三", myCount, 2000);  
  14.         Thread t2 = new SaveThread("李四", myCount, 3600);  
  15.         Thread t3 = new DrawThread("王五", myCount, 2700);  
  16.         Thread t4 = new SaveThread("老張", myCount, 600);  
  17.         Thread t5 = new DrawThread("老牛", myCount, 1300);  
  18.         Thread t6 = new DrawThread("胖子", myCount, 800);  
  19.         //執行各個線程  
  20.         pool.execute(t1);  
  21.         pool.execute(t2);  
  22.         pool.execute(t3);  
  23.         pool.execute(t4);  
  24.         pool.execute(t5);  
  25.         pool.execute(t6);  
  26.         //關閉線程池  
  27.         pool.shutdown();  
  28.     }  
  29. }  
  30. //存款線程類  
  31. class SaveThread extends Thread {  
  32.     private String name;            //操作人  
  33.     private MyCount myCount;        //賬戶  
  34.     private int x;                  //存款金額  
  35.      
  36.     SaveThread(String name, MyCount myCount, int x) {  
  37.             this.name = name;  
  38.             this.myCount = myCount;  
  39.             this.x = x;  
  40.     }  
  41.     public void run() {  
  42.             myCount.saving(x, name);  
  43.     }  
  44. }  
  45. //取款線程類  
  46. class DrawThread extends Thread {  
  47.     private String name;                //操作人  
  48.     private MyCount myCount;        //賬戶  
  49.     private int x;                           //存款金額  
  50.    
  51.     DrawThread(String name, MyCount myCount, int x) {  
  52.             this.name = name;  
  53.             this.myCount = myCount;  
  54.             this.x = x;  
  55.     }  
  56.     public void run() {  
  57.             myCount.drawing(x, name);  
  58.     }  
  59. }  
  60. //普通銀行賬戶,不可透支  
  61. class MyCount {  
  62.      private String oid;                        //賬號  
  63.      private int cash;                            //賬戶餘額  
  64.      MyCount(String oid, int cash) {       
  65.          this.oid = oid;  
  66.          this.cash = cash;  
  67.      }  
  68.      /** 
  69.       * 存款 
  70.       * @param x 存款金額 
  71.       * @param name 存款人 
  72.       */  
  73.      public void saving(int x,String name){        
  74.        if (x > 0) {  
  75.            synchronized (this) {  
  76.               cash += x; // 存款  
  77.               System.out.println(name + "存款" + x + ",當前餘額爲" + cash);  
  78.               notifyAll(); //喚醒所有等待線程。  
  79.            }  
  80.        }       
  81.      }  
  82.      //取款  
  83.      public synchronized void drawing(int x, String name) {      
  84.      synchronized (this) {  
  85.         if (cash - x < 0) {  
  86.             try {  
  87.                wait();  
  88.             }catch (InterruptedException e) {  
  89.                e.printStackTrace();  
  90.             }  
  91.         } else {  
  92.             cash -= x; // 取款  
  93.             System.out.println(name + "取款" + x + ",當前餘額爲" + cash);  
  94.         }               
  95.      }  
  96.      notifyAll(); // 喚醒所有存款操作  
  97.      }  
  98. }  

        執行結果:

[java] view plain copy
  1. 李四存款3600,當前餘額爲13600  
  2. 王五取款2700,當前餘額爲10900  
  3. 老張存款600,當前餘額爲11500  
  4. 老牛取款1300,當前餘額爲10200  
  5. 胖子取款800,當前餘額爲9400  
  6. 張三存款2000,當前餘額爲11400  

        對比以上三種方式,從控制角度上講,第一種最靈活,第二種代碼最簡單,第三種容易犯錯。

Java線程:新特徵-原子量

        所謂的原子量即操作變量的操作是“原子的”,該操作不可再分,因此是線程安全的。

        爲何要使用原子變量呢,原因是多個線程對單個變量操作也會引起一些問題。在Java5之前,可以通過volatile、synchronized關鍵字來解決併發訪問的安全問題,但這樣太麻煩。

        Java5之後,專門提供了用來進行單變量多線程併發安全訪問的工具包java.util.concurrent.atomic,其中的類也很簡單。

        下面給出一個反面例子(切勿模仿):

[java] view plain copy
  1. import java.util.concurrent.ExecutorService;  
  2. import java.util.concurrent.Executors;  
  3. import java.util.concurrent.atomic.AtomicLong;  
  4.    
  5. /** 
  6.  * Java線程:新特徵-原子量 
  7.  */  
  8. public class Test {  
  9.     public static void main(String[] args){  
  10.        //創建一個線程池  
  11.        ExecutorService pool = Executors.newFixedThreadPool(2);  
  12.        Runnable t1 = new MyRunnable("張三"2000);  
  13.        Runnable t2 = new MyRunnable("李四"3600);  
  14.        Runnable t3 = new MyRunnable("王五"2700);  
  15.        Runnable t4 = new MyRunnable("老張"600);  
  16.        Runnable t5 = new MyRunnable("老牛"1300);  
  17.        Runnable t6 = new MyRunnable("胖子"800);  
  18.         //執行各個線程  
  19.         pool.execute(t1);  
  20.         pool.execute(t2);  
  21.         pool.execute(t3);  
  22.         pool.execute(t4);  
  23.         pool.execute(t5);  
  24.         pool.execute(t6);  
  25.         //關閉線程池  
  26.         pool.shutdown();  
  27.     }  
  28. }  
  29. class MyRunnable implements Runnable{  
  30.     private static AtomicLong aLong =newAtomicLong(10000);        //原子量,每個線程都可以自由操作  
  31.     private String name;          //操作人  
  32.     private int x;                //操作數額  
  33.    
  34.     MyRunnable(String name, int x) {  
  35.             this.name = name;  
  36.             this.x = x;  
  37.     }  
  38.     public void run() {  
  39.             System.out.println(name + "執行了" + x +",當前餘額:" + aLong.addAndGet(x));  
  40.     }  
  41. }  

        多次執行結果:

[java] view plain copy
  1. 李四執行了3600,當前餘額:13600  
  2. 王五執行了2700,當前餘額:16300  
  3. 老張執行了600,當前餘額:16900  
  4. 老牛執行了1300,當前餘額:18200  
  5. 胖子執行了800,當前餘額:19000  
  6. 張三執行了2000,當前餘額:21000  
[java] view plain copy
  1. 張三執行了2000,當前餘額:12000  
  2. 王五執行了2700,當前餘額:14700  
  3. 老張執行了600,當前餘額:15300  
  4. 老牛執行了1300,當前餘額:20200  
  5. 李四執行了3600,當前餘額:18900  
  6. 胖子執行了800,當前餘額:21000  
[java] view plain copy
  1. 張三執行了2000,當前餘額:12000  
  2. 王五執行了2700,當前餘額:14700  
  3. 李四執行了3600,當前餘額:18300  
  4. 老牛執行了1300,當前餘額:20200  
  5. 胖子執行了800,當前餘額:21000  
  6. 老張執行了600,當前餘額:18900  

        從運行結果可以看出,雖然使用了原子量,但是程序併發訪問還是有問題,那究竟問題出在哪裏了?

        這裏要注意的一點是,原子量雖然可以保證單個變量在某一個操作過程的安全,但無法保證你整個代碼塊,或者整個程序的安全性。因此,通常還應該使用鎖等同步機制來控制整個程序的安全性。

        下面是對這個錯誤修正:

[java] view plain copy
  1. import java.util.concurrent.ExecutorService;  
  2. import java.util.concurrent.Executors;  
  3. import java.util.concurrent.atomic.AtomicLong;  
  4. import java.util.concurrent.locks.Lock;  
  5. import java.util.concurrent.locks.ReentrantLock;  
  6.    
  7. /** 
  8.  * Java線程:新特徵-原子量 
  9.  */  
  10. public class Test {  
  11.     public static void main(String[] args){  
  12.        //創建一個線程池  
  13.        ExecutorService pool = Executors.newFixedThreadPool(2);  
  14.        Lock lock=new ReentrantLock(false);  
  15.        Runnable t1 = new MyRunnable("張三"2000,lock);  
  16.        Runnable t2 = new MyRunnable("李四"3600,lock);  
  17.        Runnable t3 = new MyRunnable("王五"2700,lock);  
  18.        Runnable t4 = new MyRunnable("老張"600,lock);  
  19.        Runnable t5 = new MyRunnable("老牛"1300,lock);  
  20.        Runnable t6 = new MyRunnable("胖子"800,lock);  
  21.         //執行各個線程  
  22.         pool.execute(t1);  
  23.         pool.execute(t2);  
  24.         pool.execute(t3);  
  25.         pool.execute(t4);  
  26.         pool.execute(t5);  
  27.         pool.execute(t6);  
  28.         //關閉線程池  
  29.         pool.shutdown();  
  30.     }  
  31. }  
  32. class MyRunnable implements Runnable {  
  33.     private static AtomicLong aLong =newAtomicLong(10000);   //原子量,每個線程都可以自由操作  
  34.     private String name;          //操作人  
  35.     private int x;                //操作數額  
  36.     private Lock lock;  
  37.     MyRunnable(String name, int x,Lock lock) {  
  38.             this.name = name;  
  39.             this.x = x;  
  40.             this.lock=lock;  
  41.     }  
  42.     public void run() {  
  43.     lock.lock();  
  44.         System.out.println(name + "執行了" + x +",當前餘額:" + aLong.addAndGet(x));  
  45.         lock.unlock();  
  46.     }  
  47. }  

        執行結果:

[java] view plain copy
  1. 張三執行了2000,當前餘額:12000  
  2. 王五執行了2700,當前餘額:14700  
  3. 老張執行了600,當前餘額:15300  
  4. 老牛執行了1300,當前餘額:16600  
  5. 胖子執行了800,當前餘額:17400  
  6. 李四執行了3600,當前餘額:21000  

        這裏使用了一個對象鎖,來控制對併發代碼的訪問。不管運行多少次,執行次序如何,最終餘額均爲21000,這個結果是正確的。

        有關原子量的用法很簡單,關鍵是對原子量的認識,原子僅僅是保證變量操作的原子性,但整個程序還需要考慮線程安全的。

Java線程:新特徵-障礙器

        Java5中,添加了障礙器類,爲了適應一種新的設計需求,比如一個大型的任務,常常需要分配好多子任務去執行,只有當所有子任務都執行完成時候,才能執行主任務,這時候,就可以選擇障礙器了。

        障礙器是多線程併發控制的一種手段,用法很簡單。下面給個例子:

[java] view plain copy
  1. import java.util.concurrent.BrokenBarrierException;  
  2. import java.util.concurrent.CyclicBarrier;  
  3.    
  4. /** 
  5.  * Java線程:新特徵-障礙器 
  6.  */  
  7. public class Test {  
  8.     public static void main(String[] args){  
  9.        //創建障礙器,並設置MainTask爲所有定數量的線程都達到障礙點時候所要執行的任務(Runnable)  
  10.         CyclicBarrier cb = new CyclicBarrier(7,new MainTask());  
  11.         new SubTask("A", cb).start();  
  12.         new SubTask("B", cb).start();  
  13.         new SubTask("C", cb).start();  
  14.         new SubTask("D", cb).start();  
  15.         new SubTask("E", cb).start();  
  16.         new SubTask("F", cb).start();  
  17.         new SubTask("G", cb).start();  
  18.     }  
  19. }  
  20. //主任務  
  21. class MainTask implements Runnable {  
  22.     public void run() {  
  23.     System.out.println(">>>>主任務執行了!<<<<");  
  24.     }  
  25. }  
  26. //子任務  
  27. class SubTask extends Thread {  
  28.      private String name;  
  29.      private CyclicBarrier cb;  
  30.      SubTask(String name, CyclicBarrier cb) {  
  31.      this.name = name;  
  32.      this.cb = cb;  
  33.      }  
  34.      public void run(){  
  35.      System.out.println("[子任務"+name+"]開始執行了!");  
  36.      for(int i=0;i<999999;i++);//模擬耗時的任務  
  37.      System.out.println("[子任務" + name +"]開始執行完成了,並通知障礙器已經完成!");  
  38.      try {  
  39.              //通知障礙器已經完成  
  40.              cb.await();  
  41.          } catch(InterruptedException e) {  
  42.                 e.printStackTrace();  
  43.          } catch(BrokenBarrierException e) {  
  44.                  e.printStackTrace();  
  45.          }  
  46.      }  
  47. }  

        執行結果:

[java] view plain copy
  1. [子任務B]開始執行了!  
  2. [子任務A]開始執行了!  
  3. [子任務D]開始執行了!  
  4. [子任務A]開始執行完成了,並通知障礙器已經完成!  
  5. [子任務D]開始執行完成了,並通知障礙器已經完成!  
  6. [子任務C]開始執行了!  
  7. [子任務C]開始執行完成了,並通知障礙器已經完成!  
  8. [子任務F]開始執行了!  
  9. [子任務B]開始執行完成了,並通知障礙器已經完成!  
  10. [子任務E]開始執行了!  
  11. [子任務F]開始執行完成了,並通知障礙器已經完成!  
  12. [子任務G]開始執行了!  
  13. [子任務E]開始執行完成了,並通知障礙器已經完成!  
  14. [子任務G]開始執行完成了,並通知障礙器已經完成!  
  15. >>>>主任務執行了!<<<<  
        從執行結果可以看出,所有子任務完成的時候,主任務執行了,達到了控制的目標。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章