編寫具有多線程能力的程序經常會用到的方法有:
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 方法中創建了兩個線程。你
可能會認為此程序的運行結果一定為:0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9。但你
錯了!這個程序中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 次這個程序,它的輸出結果也一定每次都是:0 1 2 3 4 5 6 7 8 9 0 1
2 3 4 5 6 7 8 9。因為這里的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 循環,此時t2
還沒有執行完第一個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()方法會拋出異常,應該提供捕獲代碼?;蛄艚oJDK 捕獲。
示例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()
{ 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"); 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 接口,例如當你編寫具有多線程能力的小應用程序的時候。
線程的調度:
一個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()了。
轉http://vegetable318.bokee.com
posted on 2007-05-24 11:08
cheng 閱讀(3602)
評論(3) 編輯 收藏 所屬分類:
J2SE