Java多線程學習筆記
一、線程類
1、新建狀態(New):新創建了一個線程對象。
2、就緒狀態(Runnable):線程對象創建後,其他線程調用了該對象的start()方法。該狀態的線程位於可運行線程池中,變得可運行,等待獲取CPU的使用權。
3、運行狀態(Running):就緒狀態的線程獲取了CPU,執行程序代碼。
4、阻塞狀態(Blocked):阻塞狀態是線程因爲某種原因放棄CPU使用權,暫時停止運行。直到線程進入就緒狀態,纔有機會轉到運行狀態。阻塞的情況分三種:
(一)、等待阻塞:運行的線程執行wait()方法,JVM會把該線程放入等待池中。
(二)、同步阻塞:運行的線程在獲取對象的同步鎖時,若該同步鎖被別的線程佔用,則JVM會把該線程放入鎖池中。
(三)、其他阻塞:運行的線程執行sleep()或join()方法,或者發出了I/O請求時,JVM會把該線程置爲阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者 I/O處理完畢時,線程重新轉入就緒狀態。
5、死亡狀態(Dead):線程執行完了或者因異常退出了run()方法,該線程結束生命週期。
線程調度:
1、調整線程優先級:Java線程有優先級,優先級高的線程會獲得較多的運行機會。
Java線程的優先級用整數表示,取值範圍是1~10,Thread類有以下三個靜態常量:
static int MAX_PRIORITY
線程可以具有的最高優先級,取值爲10。
static int MIN_PRIORITY
線程可以具有的最低優先級,取值爲1。
static int NORM_PRIORITY
分配給線程的默認優先級,取值爲5。
Thread類的setPriority()和getPriority()方法分別用來設置和獲取線程的優先級。
每個線程都有默認的優先級。主線程的默認優先級爲Thread.NORM_PRIORITY。
線程的優先級有繼承關係,比如A線程中創建了B線程,那麼B將和A具有相同的優先級。
JVM提供了10個線程優先級,但與常見的操作系統都不能很好的映射。如果希望程序能移植到各個操作系統中,應該僅僅使用Thread類有以下三個靜態常量作爲優先級,這樣能保證同樣的優先級採用了同樣的調度方式。
2、線程睡眠:Thread.sleep(long millis)方法,使線程轉到阻塞狀態。millis參數設定睡眠的時間,以毫秒爲單位。當睡眠結束後,就轉爲就緒(Runnable)狀態。sleep()平臺移植性好。
3、線程等待:Object類中的wait()方法,導致當前的線程等待,直到其他線程調用此對象的 notify() 方法或 notifyAll() 喚醒方法。這個兩個喚醒方法也是Object類中的方法,行爲等價於調用 wait(0) 一樣。
4、線程讓步:Thread.yield() 方法,暫停當前正在執行的線程對象,把執行機會讓給相同或者更高優先級的線程。
5、線程加入:join()方法,等待其他線程終止。在當前線程中調用另一個線程的join()方法,則當前線程轉入阻塞狀態,直到另一個進程運行結束,當前線程再由阻塞轉爲就緒狀態。
6、線程喚醒:Object類中的notify()方法,喚醒在此對象監視器上等待的單個線程。如果所有線程都在此對象上等待,則會選擇喚醒其中一個線程。選擇是任意性的,並在對實現做出決定時發生。線程通過調用其中一個 wait 方法,在對象的監視器上等待。 直到當前的線程放棄此對象上的鎖定,才能繼續執行被喚醒的線程。被喚醒的線程將以常規方式與在該對象上主動同步的其他所有線程進行競爭;例如,喚醒的線程在作爲鎖定此對象的下一個線程方面沒有可靠的特權或劣勢。類似的方法還有一個notifyAll(),喚醒在此對象監視器上等待的所有線程。
注意:Thread中suspend()和resume()兩個方法在JDK1.5中已經廢除,不再介紹。因爲有死鎖傾向。
7、常見線程名詞解釋
主線程:JVM調用程序mian()所產生的線程。
當前線程:這個是容易混淆的概念。一般指通過Thread.currentThread()來獲取的進程。
後臺線程:指爲其他線程提供服務的線程,也稱爲守護線程。JVM的垃圾回收線程就是一個後臺線程。
前臺線程:是指接受後臺線程服務的線程,其實前臺後臺線程是聯繫在一起,就像傀儡和幕後操縱者一樣的關係。傀儡是前臺線程、幕後操縱者是後臺線程。由前臺線程創建的線程默認也是前臺線程。可以通過isDaemon()和setDaemon()方法來判斷和設置一個線程是否爲後臺線程。
Java是通過Java.lang.Thread類來實現多線程的,第個Thread對象描述了一個單獨的線程。要產生一個線程,有兩種方法:
1、需要從Java.lang.Thread類繼承一個新的線程類,重載它的run()方法;
2、通過Runnalbe接口實現一個從非線程類繼承來類的多線程,重載Runnalbe接口的run()方法。運行一個新的線程,只需要調用它的start()方法即可。如:
1 /**===================================================================== 2 * 文件:ThreadDemo_01.java 3 * 描述:產生一個新的線程 4 * ====================================================================== 5 */ 6 class ThreadDemo extends Thread{ 7 Threads() 8 { 9 } 10 11 Threads(String szName) 12 { 13 super(szName); 14 } 15 16 // 重載run函數 17 public void run() 18 { 19 for (int count = 1,row = 1; row < 20; row++,count++) 20 { 21 for (int i = 0; i < count; i++) 22 { 23 System.out.print('*'); 24 } 25 System.out.println(); 26 } 27 } 28 } 29 30 class ThreadMain{ 31 public static void main(String argv[]){ 32 ThreadDemo th = new ThreadDemo(); 33 // 調用start()方法執行一個新的線程 34 th.start(); 35 } 36 }
線程類的一些常用方法:
sleep(): 強迫一個線程睡眠N毫秒。
isAlive(): 判斷一個線程是否存活。
join(): 等待線程終止。
activeCount(): 程序中活躍的線程數。
enumerate(): 枚舉程序中的線程。
currentThread(): 得到當前線程。
isDaemon(): 一個線程是否爲守護線程。
setDaemon(): 設置一個線程爲守護線程。(用戶線程和守護線程的區別在於,是否等待主線程依賴於主線程結束而結束)
setName(): 爲線程設置一個名稱。
wait(): 強迫一個線程等待。
notify(): 通知一個線程繼續運行。
setPriority(): 設置一個線程的優先級。
二、等待一個線程的結束
有些時候我們需要等待一個線程終止後再運行我們的另一個線程,這時我們應該怎麼辦呢?請看下面的例子:
/**===================================================================== * 文件:ThreadDemo_02.java * 描述:等待一個線程的結束 * ====================================================================== */ class ThreadDemo extends Thread{ Threads() { } Threads(String szName) { super(szName); } // 重載run函數 public void run() { for (int count = 1,row = 1; row < 20; row++,count++) { for (int i = 0; i < count; i++) { System.out.print('*'); } System.out.println(); } } } class ThreadMain{ public static void main(String argv[]){ //產生兩個同樣的線程 ThreadDemo th1 = new ThreadDemo(); ThreadDemo th2 = new ThreadDemo(); // 我們的目的是先運行第一個線程,再運行第二個線程 th1.start(); th2.start(); } }
這裏我們的目標是要先運行第一個線程,等第一個線程終止後再運行第二個線程,而實際運行的結果是如何的呢?實際上我們運行的結果並不是兩個我們想要的直角三角形,而是一些亂七八糟的*號行,有的長,有的短。爲什麼會這樣呢?因爲線程並沒有按照我們的調用順序來執行,而是產生了線程賽跑現象。實際上Java並不能按我們的調用順序來執行線程,這也說明了線程是並行執行的單獨代碼。如果要想得到我們預期的結果,這裏我們就需要判斷第一個線程是否已經終止,如果已經終止,再來調用第二個線程。代碼如下:
/**===================================================================== * 文件:ThreadDemo_03.java * 描述:等待一個線程的結束的兩種方法 * ====================================================================== */ class ThreadDemo extends Thread{ Threads() { } Threads(String szName) { super(szName); } // 重載run函數 public void run() { for (int count = 1,row = 1; row < 20; row++,count++) { for (int i = 0; i < count; i++) { System.out.print('*'); } System.out.println(); } } } class ThreadMain{ public static void main(String argv[]){ ThreadMain test = new ThreadMain(); test.Method1(); // test.Method2(); } // 第一種方法:不斷查詢第一個線程是否已經終止,如果沒有,則讓主線程睡眠一直到它終止爲止 // 即:while/isAlive/sleep public void Method1(){ ThreadDemo th1 = new ThreadDemo(); ThreadDemo th2 = new ThreadDemo(); // 執行第一個線程 th1.start(); // 不斷查詢第一個線程的狀態 while(th1.isAlive()){ try{ Thread.sleep(100); }catch(InterruptedException e){ } } //第一個線程終止,運行第二個線程 th2.start(); } // 第二種方法:join() public void Method2(){ ThreadDemo th1 = new ThreadDemo(); ThreadDemo th2 = new ThreadDemo(); // 執行第一個線程 th1.start(); try{ th1.join(); }catch(InterruptedException e){ } // 執行第二個線程 th2.start(); }
三、線程的同步問題
有些時候,我們需要很多個線程共享一段代碼,比如一個私有成員或一個類中的靜態成員,但是由於線程賽跑的問題,所以我們得到的常常不是正確的輸出結果,而相反常常是張冠李戴,與我們預期的結果大不一樣。看下面的例子:
由於線程的賽跑問題,所以輸出的結果往往是Thread1對應“這是第 2 個線程”,這樣與我們要輸出的結果是不同的。爲了解決這種問題(錯誤),Java爲我們提供了“鎖”的機制來實現線程的同步。鎖的機制要求每個線程在進入共享代碼之前都要取得鎖,否則不能進入,而退出共享代碼之前則釋放該鎖,這樣就防止了幾個或多個線程競爭共享代碼的情況,從而解決了線程的不同步的問題。可以這樣說,在運行共享代碼時則是最多隻有一個線程進入,也就是和我們說的壟斷。鎖機制的實現方法,則是在共享代碼之前加入synchronized段,把共享代碼包含在synchronized段中。上述問題的解決方法爲:
由於過多的synchronized段將會影響程序的運行效率,因此引入了同步方法,同步方法的實現則是將共享代碼單獨寫在一個方法裏,在方法前加上synchronized關鍵字即可。
在線程同步時的兩個需要注意的問題:
1、無同步問題:即由於兩個或多個線程在進入共享代碼前,得到了不同的鎖而都進入共享代碼而造成。
2、死鎖問題:即由於兩個或多個線程都無法得到相應的鎖而造成的兩個線程都等待的現象。這種現象主要是因爲相互嵌套的synchronized代碼段而造成,因此,在程序中儘可能少用嵌套的synchronized代碼段是防止線程死鎖的好方法。
Java多線程學習筆記(二)
四、Java的等待通知機制
在有些時候,我們需要在幾個或多個線程中按照一定的秩序來共享一定的資源。例如生產者--消費者的關係,在這一對關係中實際情況總是先有生產者生產了產品後,消費者纔有可能消費;又如在父--子關係中,總是先有父親,然後纔能有兒子。然而在沒有引入等待通知機制前,我們得到的情況卻常常是錯誤的。這裏我引入《用線程獲得強大的功能》一文中的生產者--消費者的例子:
在以上的程序中,模擬了生產者和消費者的關係,生產者在一個循環中不斷生產了從A-Z的共享數據,而消費者則不斷地消費生產者生產的A-Z的共享數據。我們開始已經說過,在這一對關係中,必須先有生產者生產,纔能有消費者消費。但如果運行我們上面這個程序,結果卻出現了在生產者沒有生產之前,消費都就已經開始消費了或者是生產者生產了卻未能被消費者消費這種反常現象。爲了解決這一問題,引入了等待通知(wait/notify)機制如下:
1、在生產者沒有生產之前,通知消費者等待;在生產者生產之後,馬上通知消費者消費。
2、在消費者消費了之後,通知生產者已經消費完,需要生產。
下面修改以上的例子(源自《用線程獲得強大的功能》一文):
在以上程序中,設置了一個通知變量,每次在生產者生產和消費者消費之前,都測試通知變量,檢查是否可以生產或消費。最開始設置通知變量爲true,表示還未生產,在這時候,消費者需要消費,於時修改了通知變量,調用notify()發出通知。這時由於生產者得到通知,生產出第一個產品,修改通知變量,向消費者發出通知。這時如果生產者想要繼續生產,但因爲檢測到通知變量爲false,得知消費者還沒有生產,所以調用wait()進入等待狀態。因此,最後的結果,是生產者每生產一個,就通知消費者消費一個;消費者每消費一個,就通知生產者生產一個,所以不會出現未生產就消費或生產過剩的情況。
五、線程的中斷
在很多時候,我們需要在一個線程中調控另一個線程,這時我們就要用到線程的中斷。用最簡單的話也許可以說它就相當於播放機中的暫停一樣,當第一次按下暫停時,播放器停止播放,再一次按下暫停時,繼續從剛纔暫停的地方開始重新播放。而在Java中,這個暫停按鈕就是Interrupt()方法。在第一次調用interrupt()方法時,線程中斷;當再一次調用interrupt()方法時,線程繼續運行直到終止。這裏依然引用《用線程獲得強大功能》一文中的程序片斷,但爲了更方便看到中斷的過程,我在原程序的基礎上作了些改進,程序如下:
1 /* =================================================================================== 2 * 文件:ThreadDemo09.java 3 * 描述:線程的中斷 4 * =================================================================================== 5 */ 6 class ThreadA extends Thread{ 7 8 private Thread thdOther; 9 10 ThreadA(Thread thdOther){ 11 this.thdOther = thdOther; 12 } 13 14 public void run(){ 15 16 System.out.println(getName() + " 運行..."); 17 18 int sleepTime = (int)(Math.random() * 10000); 19 System.out.println(getName() + " 睡眠 " + sleepTime 20 + " 毫秒。"); 21 22 try{ 23 Thread.sleep(sleepTime); 24 }catch(InterruptedException e){} 25 26 System.out.println(getName() + " 覺醒,即將中斷線程 B。"); 27 // 中斷線程B,線程B暫停運行 28 thdOther.interrupt(); 29 } 30 } 31 32 class ThreadB extends Thread{ 33 int count = 0; 34 35 public void run(){ 36 37 System.out.println(getName() + " 運行..."); 38 39 while (!this.isInterrupted()){ 40 System.out.println(getName() + " 運行中 " + count++); 41 42 try{ 43 Thread.sleep(10); 44 }catch(InterruptedException e){ 45 int sleepTime = (int)(Math.random() * 10000); 46 System.out.println(getName() + " 睡眠" + sleepTime 47 + " 毫秒。覺醒後立即運行直到終止。"); 48 49 try{ 50 Thread.sleep(sleepTime); 51 }catch(InterruptedException m){} 52 53 System.out.println(getName() + " 已經覺醒,運行終止..."); 54 // 重新設置標記,繼續運行 55 this.interrupt(); 56 } 57 } 58 59 System.out.println(getName() + " 終止。"); 60 } 61 } 62 63 class Test{ 64 public static void main(String argv[]){ 65 ThreadB thdb = new ThreadB(); 66 thdb.setName("ThreadB"); 67 68 ThreadA thda = new ThreadA(thdb); 69 thda.setName("ThreadA"); 70 71 thdb.start(); 72 thda.start(); 73 } 74 } 75 運行以上程序,你可以清楚地看到中斷的過程。首先線程B開始運行,接着運行線程A,在線程A睡眠一段時間覺醒後,調用interrupt()方法中斷線程B,此是可能B正在睡眠,覺醒後掏出一個InterruptedException異常,執行其中的語句,爲了更清楚地看到線程的中斷恢復,我在InterruptedException異常後增加了一次睡眠,當睡眠結束後,線程B調用自身的interrupt()方法恢復中斷,這時測試isInterrupt()返回true,線程退出。 76 線程和進程(Threads and Processes) 77 第一個關鍵的系統級概念,究竟什麼是線程或者說究竟什麼是進程?她們其實就是操作系統內部的一種數據結構。 78 進程數據結構掌握着所有與內存相關的東西:全局地址空間、文件句柄等等諸如此類的東西。當一個進程放棄執行(準確的說是放棄佔有CPU),而被操作系統交換到硬盤上,使別的進程有機會運行的時候,在那個進程裏的所有數據也將被寫到硬盤上,甚至包括整個系統的核心(core memory)。可以這麼說,當你想到進程(process),就應該想到內存(memory) (進程 == 內存)。如上所述,切換進程的代價非常大,總有那麼一大堆的內存要移來移去。你必須用秒這個單位來計量進程切換(上下文切換),對於用戶來說秒意味着明顯的等待和硬盤燈的狂閃(對於作者的我,就意味着IBM龍騰3代的爛掉,5555555)。言歸正傳,對於Java而言,JVM就幾乎相當於一個進程(process),因爲只有進程才能擁有堆內存(heap,也就是我們平時用new操作符,分出來的內存空間)。 79 那麼線程是什麼呢?你可以把它看成“一段代碼的執行”---- 也就是一系列由JVM執行的二進制指令。這裏面沒有對象(Object)甚至沒有方法(Method)的概念。指令執行的序列可以重疊,並且並行的執行。後面,我會更加詳細的論述這個問題。但是請記住,線程是有序的指令,而不是方法(method)。 80 線程的數據結構,與進程相反,僅僅只包括執行這些指令的信息。它包含當前的運行上下文(context):如寄存器(register)的內容、當前指令的在運行引擎的指令流中的位置、保存方法(methods)本地參數和變量的運行時堆棧。如果發生線程切換,OS只需把寄存器的值壓進棧,然後把線程包含的數據結構放到某個類是列表(LIST)的地方;把另一個線程的數據從列表中取出,並且用棧裏的值重新設置寄存器。切換線程更加有效率,時間單位是毫秒。對於Java而言,一個線程可以看作是JVM的一個狀態。 81 運行時堆棧(也就是前面說的存儲本地變量和參數的地方)是線程數據結構一部分。這是因爲多個線程,每一個都有自己的運行時堆棧,也就是說存儲在這裏面的數據是絕對線程安全(後面將會詳細解釋這個概念)的。因爲可以肯定一個線程是無法修改另一個線程的系統級的數據結構的。也可以這麼說一個不訪問堆內存的(只讀寫堆棧內存)方法,是線程安全的(Thread Safe)。 82 線程安全和同步 83 線程安全,是指一個方法(method)可以在多線程的環境下安全的有效的訪問進程級的數據(這些數據是與其他線程共享的)。事實上,線程安全是個很難達到的目標。 84 線程安全的核心概念就是同步,它保證多個線程: 85 同時開始執行,並行運行 86 不同時訪問相同的對象實例 87 不同時執行同一段代碼 88 我將會在後面的章節,一一細訴這些問題。但現在還是讓我們來看看同步的一種經典的 89 實現方法——信號量。信號量是任何可以讓兩個線程爲了同步它們的操作而相互通信的對象。Java也是通過信號量來實現線程間通信的。 90 不要被微軟的文檔所暗示的信號量僅僅是Dijksta提出的計數型信號量所迷惑。信號量其實包含任何可以用來同步的對象。 91 如果沒有synchronized關鍵字,就無法用JAVA實現信號量,但是僅僅只依靠它也不足夠。我將會在後面爲大家演示一種用Java實現的信號量。 92 同步的代價很高喲! 93 同步(或者說信號量,隨你喜歡啦)的一個很讓人頭痛的問題就是代價。考慮一下,下面的代碼: 94 Listing 1.2: 95 import java.util.*; 96 import java.text.NumberFormat; 97 class Synch 98 { 99 private static long[ ] locking_time = new long[100]; 100 private static long[ ] not_locking_time = new long[100]; 101 private static final long ITERATIONS = 10000000; 102 synchronized long locking (long a, long b){return a + b;} 103 long not_locking (long a, long b){return a + b;} 104 105 private void test( int id ) 106 { 107 long start = System.currentTimeMillis(); 108 for(long i = ITERATIONS; --i >= 0 ;) 109 { locking(i,i); } 110 locking_time[id] = System.currentTimeMillis() - start; 111 start = System.currentTimeMillis(); 112 for(long i = ITERATIONS; --i >= 0 ;) 113 { not_locking(i,i); } 114 not_locking_time[id] = System.currentTimeMillis() - start; 115 } 116 static void print_results( int id ) 117 { NumberFormat compositor = NumberFormat.getInstance(); 118 compositor.setMaximumFractionDigits( 2 ); 119 double time_in_synchronization = locking_time[id] - not_locking_time[id]; 120 System.out.println( "Pass " + id + ": Time lost: " 121 + compositor.format( time_in_synchronization ) 122 + " ms. " 123 + compositor.format( ((double)locking_time[id]/not_locking_time[id])*100.0 ) 124 + "% increase" ); 125 } 126 static public void main(String[ ] args) throws InterruptedException 127 { 128 final Synch tester = new Synch(); 129 tester.test(0); print_results(0); 130 tester.test(1); print_results(1); 131 tester.test(2); print_results(2); 132 tester.test(3); print_results(3); 133 tester.test(4); print_results(4); 134 tester.test(5); print_results(5); 135 tester.test(6); print_results(6); 136 final Object start_gate = new Object(); 137 Thread t1 = new Thread() 138 { public void run() 139 { try{ synchronized(start_gate) { start_gate.wait(); } } 140 catch( InterruptedException e ){} 141 tester.test(7); 142 } 143 }; 144 Thread t2 = new Thread() 145 { public void run() 146 { try{ synchronized(start_gate) { start_gate.wait(); } } 147 catch( InterruptedException e ){} 148 tester.test(8); 149 } 150 }; 151 Thread.currentThread().setPriority( Thread.MIN_PRIORITY ); 152 t1.start(); 153 t2.start(); 154 synchronized(start_gate){ start_gate.notifyAll(); } 155 t1.join(); 156 t2.join(); 157 print_results( 7 ); 158 print_results( 8 ); 159 } 160 }
運行以上程序,你可以清楚地看到中斷的過程。首先線程B開始運行,接着運行線程A,在線程A睡眠一段時間覺醒後,調用interrupt()方法中斷線程B,此是可能B正在睡眠,覺醒後掏出一個InterruptedException異常,執行其中的語句,爲了更清楚地看到線程的中斷恢復,我在InterruptedException異常後增加了一次睡眠,當睡眠結束後,線程B調用自身的interrupt()方法恢復中斷,這時測試isInterrupt()返回true,線程退出。