Java 線程綜合述 | |
run(), start(), wait(), notify(), notifyAll(), sleep(), yield(), join() 還有一個重要的關鍵字:synchronized 本文將對以上內容進行講解。 一:run() 和start() 示例1: public class ThreadTest extends Thread { public void run() { for (int i = 0; i < 10; i++) { System.out.print(" " + i); } } public static void main(String[] args) { new ThreadTest().start(); new ThreadTest().start(); } } 這是個簡單的多線程程序。run() 和start() 是大家都很熟悉的兩個方法。把希望並行處理的代碼都放在run() 中;stat() 用於自動調用run(), 這是JAVA的內在機制規定的。並且run() 的訪問控制符必須是public,返回值必須是void(這種說法不準確,run() 沒有返回值),run() 不帶參數。 這些規定想必大家都早已知道了,但你是否清楚爲什麼run方法必須聲明成這樣的形式?這涉及到JAVA的方法覆蓋和重載的規定。這些內容很重要, 請讀者參考相關資料。 二:關鍵字synchronized 有了synchronized關鍵字,多線程程序的運行結果將變得可以控制。synchronized關鍵字用於保護共享數據。請大家注意 "共享數據", 你一定要分清哪些數據是共享數據,JAVA是面向對象的程序設計語言,所以初學者在編寫多線程程序時,容易分不清哪些數據是共享數據。請看下面的例子: 示例2: public class ThreadTest implements Runnable { public synchronized void run() { for (int i = 0; i < 10; i++) { System.out.print(" " + i); } } public static void main(String[] args) { Runnable r1 = new ThreadTest(); Runnable r2 = new ThreadTest(); Thread t1 = new Thread(r1); Thread t2 = new Thread(r2); t1.start(); t2.start(); } } 在這個程序中,run() 被加上了synchronized關鍵字。在main方法中創建了兩個線程。你可能會認爲此程序的運行結果一定爲:0123456789 0123456789。但你錯了!這個程序中synchronized關鍵字保護的不是共享數據( 其實在這個程序中synchronized關鍵字沒有起到任何作用,此程序的運行結果是不可預先確定的)。這個程序中的t1, t2是兩個對象(r1, r2)的線程。JAVA是面向對象的程序設計語言,不同的對象的數據是不同的,r1, r2有各自的run() 方法,而synchronized使同一個對象的多個線程, 在某個時刻只有其中的一個線程可以訪問這個對象的synchronized數據。每個對象都有一個 "鎖標誌", 當這個對象的一個線程訪問這個對象的某個synchronized數據時,這個對象的所有被synchronized修飾的數據將被上鎖(因爲 "鎖標誌" 被當前線程拿走了),只有當前線程訪問完它要訪問的synchronized數據時,當前線程纔會釋放 "鎖標誌", 這樣同一個對象的其它線程纔有機會訪問synchronized數據。 示例3: public class ThreadTest implements Runnable { public synchronized void run() { for (int i = 0; i < 10; i++) { System.out.print(" " + i); } } public static void main(String[] args) { Runnable r = new ThreadTest(); Thread t1 = new Thread(r); Thread t2 = new Thread(r); t1.start(); t2.start(); } } 如果你運行1000次這個程序,它的輸出結果也一定每次都是:01234567890123456789。因爲這裏的synchronized保護的是共享數據。 t1, t2是同一個對象(r)的兩個線程,當其中的一個線程(例如:t1)開始執行run() 方法時,由於run() 受synchronized保護,所以同一個對象的其他線程( t2)無法訪問synchronized方法(run方法)。只有當t1執行完後t2纔有機會執行。 示例4: public class ThreadTest implements Runnable { public void run() { synchronized (this) { for (int i = 0; i < 10; i++) { System.out.print(" " + i); } } } public static void main(String[] args) { Runnable r = new ThreadTest(); Thread t1 = new Thread(r); Thread t2 = new Thread(r); t1.start(); t2.start(); } } 這個程序與示例3的運行結果一樣。在可能的情況下,應該把保護範圍縮到最小,可以用示例4的形式,this代表 "這個對象"。沒有必要把整個run() 保護起來, run() 中的代碼只有一個for循環,所以只要保護for循環就可以了。 示例5: public class ThreadTest implements Runnable { public void run() { for (int k = 0; k < 5; k++) { System.out.println(Thread.currentThread().getName() + " : for loop : " + k); } synchronized (this) { for (int k = 0; k < 5; k++) { System.out.println(Thread.currentThread().getName() + " : synchronized for loop : " + k); } } } public static void main(String[] args) { Runnable r = new ThreadTest(); Thread t1 = new Thread(r, "t1_name"); Thread t2 = new Thread(r, "t2_name"); t1.start(); t2.start(); } } 運行結果:t1_name : for loop : 0 t1_name : for loop : 1 t1_name : for loop : 2 t2_name : for loop : 0 t1_name : for loop : 3 t2_name : for loop : 1 t1_name : for loop : 4 t2_name : for loop : 2 t1_name : synchronized for loop : 0 t2_name : for loop : 3 t1_name : synchronized for loop : 1 t2_name : for loop : 4 t1_name : synchronized for loop : 2 t1_name : synchronized for loop : 3 t1_name : synchronized for loop : 4 t2_name : synchronized for loop : 0 t2_name : synchronized for loop : 1 t2_name : synchronized for loop : 2 t2_name : synchronized for loop : 3 t2_name : synchronized for loop : 4 第一個for循環沒有受synchronized保護。對於第一個for循環,t1, t2可以同時訪問。運行結果表明t1執行到了k = 2時,t2開始執行了。t1首先執行完了第一個for循環,此時還沒有執行完第一個for循環( t2剛執行到k = 2)。t1開始執行第二個for循環,當t1的第二個for循環執行到k = 1時,t2的第一個for循環執行完了。 t2想開始執行第二個for循環,但由於t1首先執行了第二個for循環,這個對象的鎖標誌自然在t1手中( synchronized方法的執行權也就落到了t1手中),在t1沒執行完第二個for循環的時候,它是不會釋放鎖標誌的。 所以t2必須等到t1執行完第二個for循環後,它纔可以執行第二個for循環 三:sleep() 示例6: public class ThreadTest implements Runnable { public void run() { for (int k = 0; k < 5; k++) { if (k == 2) { try { Thread.currentThread().sleep(5000); } catch (Exception e) {} } System.out.print(" " + k); } } public static void main(String[] args) { Runnable r = new ThreadTest(); Thread t = new Thread(r); t.start(); } } sleep方法會使當前的線程暫停執行一定時間(給其它線程運行機會)。讀者可以運行示例6,看看結果就明白了。sleep方法會拋出異常,必須提供捕獲代碼。 示例7: public class ThreadTest implements Runnable { public void run() { for (int k = 0; k < 5; k++) { if (k == 2) { try { Thread.currentThread().sleep(5000); } catch (Exception e) {} } System.out.println(Thread.currentThread().getName() + " : " + k); } } public static void main(String[] args) { Runnable r = new ThreadTest(); Thread t1 = new Thread(r, "t1_name"); Thread t2 = new Thread(r, "t2_name"); t1.setPriority(Thread.MAX_PRIORITY); t2.setPriority(Thread.MIN_PRIORITY); t1.start(); t2.start(); } } t1被設置了最高的優先級,t2被設置了最低的優先級。t1不執行完,t2就沒有機會執行。但由於t1在執行的中途休息了5秒中,這使得t2就有機會執行了。 讀者可以運行這個程序試試看。 示例8: public class ThreadTest implements Runnable { public synchronized void run() { for (int k = 0; k < 5; k++) { if (k == 2) { try { Thread.currentThread().sleep(5000); } catch (Exception e) {} } System.out.println(Thread.currentThread().getName() + " : " + k); } } public static void main(String[] args) { Runnable r = new ThreadTest(); Thread t1 = new Thread(r, "t1_name"); Thread t2 = new Thread(r, "t2_name"); t1.start(); t2.start(); } } 請讀者首先運行示例8程序,從運行結果上看:一個線程在sleep的時候,並不會釋放這個對象的鎖標誌。 四:join() 示例9: public class ThreadTest implements Runnable { public static int a = 0; public void run() { for (int k = 0; k < 5; k++) { a = a + 1; } } public static void main(String[] args) { Runnable r = new ThreadTest(); Thread t = new Thread(r); t.start(); System.out.println(a); } } 請問程序的輸出結果是5嗎?答案是:有可能。其實你很難遇到輸出5的時候,通常情況下都不是5。這裏不講解爲什麼輸出結果不是5,我要講的是: 怎樣才能讓輸出結果爲5!其實很簡單,join() 方法提供了這種功能。join() 方法,它能夠使調用該方法的線程在此之前執行完畢。 把示例9的main() 方法該成如下這樣: public static void main(String[] args) throws Exception { Runnable r = new ThreadTest(); Thread t = new Thread(r); t.start(); t.join(); System.out.println(a); } 這時,輸出結果肯定是5!join() 方法會拋出異常,應該提供捕獲代碼。或留給JDK捕獲。 示例10: public class ThreadTest implements Runnable { public void run() { for (int k = 0; k < 10; k++) { System.out.print(" " + k); } } public static void main(String[] args) throws Exception { Runnable r = new ThreadTest(); Thread t1 = new Thread(r); Thread t2 = new Thread(r); t1.start(); t1.join(); t2.start(); } } 運行這個程序,看看結果是否與示例3一樣 五:yield() yield() 方法與sleep() 方法相似,只是它不能由用戶指定線程暫停多長時間。按照SUN的說法: sleep方法可以使低優先級的線程得到執行的機會,當然也可以讓同優先級和高優先級的線程有執行的機會。而yield() 方法只能使同優先級的線程有執行的機會。 示例11: public class ThreadTest implements Runnable { public void run() { 8 for (int k = 0; k < 10; k++) { if (k == 5 && Thread.currentThread().getName().equals("t1")) { Thread.yield(); } System.out.println(Thread.currentThread().getName() + " : " + k); } } public static void main(String[] args) { Runnable r = new ThreadTest(); Thread t1 = new Thread(r, "t1"); Thread t2 = new Thread(r, "t2"); t1.setPriority(Thread.MAX_PRIORITY); t2.setPriority(Thread.MIN_PRIORITY); t1.start(); t2.start(); } } 輸出結果: t1 : 0 t1 : 1 t1 : 2 t1 : 3 t1 : 4 t1 : 5 t1 : 6 t1 : 7 t1 : 8 t1 : 9 t2 : 0 t2 : 1 t2 : 2 t2 : 3 t2 : 4 t2 : 5 t2 : 6 t2 : 7 t2 : 8 t2 : 9 多次運行這個程序,輸出也是一樣。這說明:yield() 方法不會使不同優先級的線程有執行的機會。 六:wait(), notify(), notifyAll() 首先說明:wait(), notify(), notifyAll() 這些方法由java.lang.Object類提供,而上面講到的方法都是由java.lang.Thread類提供( Thread類實現了Runnable接口)。 wait(), notify(), notifyAll() 這三個方法用於協調多個線程對共享數據的存取,所以必須在synchronized語句塊內使用這三個方法。先看下面了例子: 示例12: public class ThreadTest implements Runnable { public static int shareVar = 0; public synchronized void run() { if (shareVar == 0) { for (int i = 0; i < 10; i++) { shareVar++; if (shareVar == 5) { try { this.wait(); } catch (Exception e) {} } } } if (shareVar != 0) { System.out.print(Thread.currentThread().getName()); System.out.println(" shareVar = " + shareVar); this.notify(); } } public static void main(String[] args) { Runnable r = new ThreadTest(); Thread t1 = new Thread(r, "t1"); 10 Thread t2 = new Thread(r, "t2"); t1.start(); t2.start(); } } 運行結果: t2 shareVar = 5 t1 shareVar = 10 t1線程最先執行。由於初始狀態下shareVar爲0,t1將使shareVar連續加1,當shareVar的值爲5時,t1調用wait() 方法, t1將處於休息狀態,同時釋放鎖標誌。這時t2得到了鎖標誌開始執行,shareVar的值已經變爲5,所以t2直接輸出shareVar的值, 然後再調用notify() 方法喚醒t1。t1接着上次休息前的進度繼續執行,把shareVar的值一直加到10,由於此刻shareVar的值不爲0, 所以t1將輸出此刻shareVar的值,然後再調用notify() 方法,由於此刻已經沒有等待鎖標誌的線程,所以此調用語句不起任何作用。 這個程序簡單的示範了wait(), notify() 的用法,讀者還需要在實踐中繼續摸索。 七:關於線程的補充 編寫一個具有多線程能力的程序可以繼承Thread類,也可以實現Runnable接口。在這兩個方法中如何選擇呢?從面向對象的角度考慮, 作者建議你實現Runnable接口。有時你也必須實現Runnable接口,例如當你編寫具有多線程能力的小應用程序的時候。 線程的調度:NewRunningRunnableOtherwise BlockedDeadBlocked in object`sit() poolBlocked in object`slock poolnotify() Schedulercompletesrun() start() sleep() or join() sleep() timeout or thread join() s or interupt() Lockavailablesynchronized() Thread states terupt() 一個Thread對象在它的生命週期中會處於各種不同的狀態,上圖形象地說明了這點。wa in 調用start() 方法使線程處於可運行狀態,這意味着它可以由JVM調度並執行。這並不意味着線程就會立即運行。 實際上,程序中的多個線程並不是同時執行的。除非線程正在真正的多CPU計算機系統上執行,否則線程使用單CPU必須輪流執行。但是,由於這發生的很快, 我們常常認爲這些線程是同時執行的。 JAVA運行時系統的計劃調度程序是搶佔性的。如果計劃調度程序正在運行一個線程並且來了另一個優先級更高的線程, 那麼當前正在執行的線程就被暫時終止而讓更高優先級的線程執行。 JAVA計劃調度程序不會爲與當前線程具有同樣優先級的另一個線程去搶佔當前的線程。但是,儘管計劃調度程序本身沒有時間片( 即它沒有給相同優先級的線程以執行用的時間片),但以Thread類爲基礎的線程的系統實現可能會支持時間片分配。這依賴具體的操作系統, Windows與UNIX在這個問題上的支持不會完全一樣。 由於你不能肯定小應用程序將運行在什麼操作系統上,因此你不應該編寫出依賴時間片分配的程序。就是說, 應該使用yield方法以允許相同優先級的線程有機會執行而不是希望每一個線程都自動得到一段CPU時間片。 Thread類提供給你與系統無關的處理線程的機制。但是,線程的實際實現取決於JAVA運行所在的操作系統。因此, 線程化的程序確實是利用了支持線程的操作系統。 當創建線程時,可以賦予它優先級。它的優先級越高,它就越能影響運行系統。 JAVA運行系統使用一個負責在所有執行JAVA程序內運行所有存在的計劃調度程序。 該計劃調度程序實際上使用一個固定優先級的算法來保證每個程序中的最高優先級的線程得到CPU--允許最高優先級的線程在其它線程之前執行。 對於在一個程序中有幾個相同優先級的線程等待執行的情況,該計劃調度程序循環地選擇它們,當進行下一次選擇時選擇前面沒有執行的線程, 具有相同優先級的所有的線程都受到平等的對待。較低優先級的線程在較高優先級的線程已經死亡或者進入不可執行狀態之後才能執行。 繼續討論wait(), notify(), notifyAll(): 當線程執行了對一個特定對象的wait() 調用時,那個線程被放到與那個對象相關的等待池中。此外,調用wait() 的線程自動釋放對象的鎖標誌。 可以調用不同的wait():wait() 或wait(long timeout) 對一個特定對象執行notify() 調用時,將從對象的等待池中移走一個任意的線程,並放到鎖標誌等待池中,那裏的線程一直在等待, 直到可以獲得對象的鎖標誌。notifyAll() 方法將從對象等待池中移走所有等待那個對象的線程並放到鎖標誌等待池中。 只有鎖標誌等待池中的線程能獲取對象的鎖標誌,鎖標誌允許線程從上次因調用wait() 而中斷的地方開始繼續運行。 在許多實現了wait() / notify() 機制的系統中,醒來的線程必定是那個等待時間最長的線程。然而,在Java技術中,並不保證這點。 注意,不管是否有線程在等待,都可以調用notify()。如果對一個對象調用notify() 方法,而在這個對象的鎖標誌等待池中並沒有線程, 那麼notify() 調用將不起任何作用。 在JAVA中,多線程是一個神奇的主題。之所以說它 "神奇",是因爲多線程程序的運行結果不可預測,但我們又可以通過某些方法控制多線程程序的執行。 要想靈活使用多線程,讀者還需要大量實踐。 另外,從JDK 1.2開始,SUN就不建議使用resume(), stop(), suspend() 了 |