一提到線程好像是件很麻煩很複雜的事,事實上確實如此,涉及到線程的編程是很講究技巧的。這就需要我們變換思維方式,瞭解線程機制的比較通用的技巧,寫出高效的、不依賴於某個JVM實現的程序來。畢竟僅僅就Java而言,各個虛擬機的實現是不同的。學習線程時,最令我印象深刻的就是那種不確定性、沒有保障性,各個線程的運行完全是以不可預料的方式和速度推進,有的一個程序運行了N次,其結果差異性很大。
1、什麼是線程?線程是彼此互相獨立的、能獨立運行的子任務,並且每個線程都有自己的調用棧。所謂的多任務是通過週期性地將CPU時間片切換到不同的子任務,雖然從微觀上看來,單核的CPU上同時只運行一個子任務,但是從宏觀來看,每個子任務似乎是同時連續運行的。(但是JAVA的線程不是按時間片分配的,在本文的最後引用了一段網友翻譯的JAVA原著中對線程的理解。)
2、在java中,線程指兩個不同的內容:一是java.lang.Thread類的一個對象;另外也可以指線程的執行。線程對象和其他的對象一樣,在堆上創建、運行、死亡。但不同之處是線程的執行是一個輕量級的進程,有它自己的調用棧。可以這樣想,每個調用棧都對應一個線程,每個線程又對應一個調用棧。我們運行java程序時有一個入口函數main()函數,它對應的線程被稱爲主線程。一個新線程一旦被創建,就產生一個新調用棧,從原主線程中脫離,也就是與主線程併發執行。
3、當提到線程時,很少是有保障的。我們必須瞭解到什麼是有保障的操作,什麼是無保障的操作,以便設計的程序在各種jvm上都能很好地工作。比如,在某些jvm實現中,把java線程映射爲本地操作系統的線程。這是java核心的一部分。
4、線程的創建。
創建線程有兩種方式:
A、繼承 java.lang.Thread類。
public class TestTh extends Thread {
public void run(String s){
System.out.println ("string in run is " + s);
}
@Override
public void run() {
super.run();
System.out.println ("someting run here!");
}
/**
* @Description: TODO
* @Title: main
* @Throws
*/
public static void main(String[] args) {
TestTh tt = new TestTh();
tt.start();
tt.run("it won't auto run!");
}
}
輸出的結果比較有趣:
string in run is it won’t auto run!
someting run here!
注意輸出的順序:好像與我們想象的順序相反了!爲什麼呢?
一旦調用start()方法,必須給JVM點時間,讓它配置進程。而在它配置完成之前,重載的run(String s)方法被調用了,結果反而先輸出了“string in run is it won’t auto run!”,這時tt線程完成了配置,輸出了“someting run here!”。
這個結論是比較容易驗證的:
修改上面的程序,在tt.start();後面加上語句for (int i = 0; i<10000; i++); 這樣主線程開始執行運算量比較大的for循環了,只有執行完for循環才能運行後面的tt.run(“it won’t auto run!”);語句。此時,tt線程和主線程並行執行了,已經有足夠的時間完成線程的配置!因此先到一步!修改後的程序運行結果如下:
someting run here!
string in run is it won’t auto run!
注意:這種輸出結果的順序是沒有保障的!不要依賴這種結論!
沒有參數的run()方法是自動被調用的,而帶參數的run()是被重載的,必須顯式調用。
這種方式的限制是:這種方式很簡單,但不是個好的方案。如果繼承了Thread類,那麼就不能繼承其他的類了,java是單繼承結構的,應該把繼承的機會留給別的類。除非因爲你有線程特有的更多的操作。
Thread類中有許多管理線程的方法,包括創建、啓動和暫停它們。所有的操作都是從run()方法開始,並且在run()方法內編寫需要在獨立線程內執行的代碼。run()方法可以調用其他方法,但是執行的線程總是通過調用run()。
B、實現java.lang.Runnable接口。
class ThreadTest implements Runnable {
@Override
public void run() {
super.run();
System.out.println ("someting run here");
}
public static void main (String[] args) {
ThreadTest tt = new ThreadTest();
Thread t1 = new Thread(tt);
Thread t2 = new Thread(tt);
t1.start();
t2.start();
//new Thread(tt).start();
}
}
比第一種方法複雜一點,爲了使代碼被獨立的線程運行,還需要一個Thread對象。這樣就把線程相關的代碼和線程要執行的代碼分離開來。
另一種方式是:參數形式的匿名內部類創建方式,也是比較常見的。
class ThreadTest{
public static void main (String[] args) {
Thread t = new Thread(new Runnable(){
public void run(){
System.out.println ("anonymous thread");
}
});
t.start();
}
}
如果你對此方式的聲明不感冒,請參看本人總結的內部類。
第一種方式使用無參構造函數創建線程,則當線程開始工作時,它將調用自己的run()方法。
第二種方式使用帶參數的構造函數創建線程,因爲你要告訴這個新線程使用你的run()方法,而不是它自己的。
如上例,可以把一個目標賦給多個線程,這意味着幾個執行線程將運行完全相同的作業。
5、什麼時候線程是活的?
在調用start()方法開始執行線程之前,線程的狀態還不是活的。測試程序如下:
class ThreadTest implements Runnable {
public void run() {
System.out.println ("someting run here");
}
public static void main (String[] args) {
ThreadTest tt = new ThreadTest();
Thread t1 = new Thread(tt);
System.out.println (t1.isAlive());
t1.start();
System.out.println (t1.isAlive());
}
}
結果輸出:
false
true
isAlive方法是確定一個線程是否已經啓動,而且還沒完成run()方法內代碼的最好方法。
6、啓動新線程。
線程的啓動要調用start()方法,只有這樣才能創建新的調用棧。而直接調用run()方法的話,就不會創建新的調用棧,也就不會創建新的線程,run()方法就與普通的方法沒什麼兩樣了!
7、給線程起個有意義的名字。
沒有該線程命名的話,線程會有一個默認的名字,格式是:“Thread-”加上線程的序號,如:Thread-0
這看起來可讀性不好,不能從名字分辨出該線程具有什麼功能。下面是給線程命名的方式。
第一種:用setName()函數
第二種:選用帶線程命名的構造器
class ThreadTest implements Runnable{
public void run(){
System.out.println (Thread.currentThread().getName());
}
public static void main (String[] args) {
ThreadTest tt = new ThreadTest();
//Thread t = new Thread (tt,"eat apple");
Thread t = new Thread (tt);
t.setName("eat apple");
t.start();
}
}
class ThreadTest implements Runnable{
public void run(){
System.out.println (Thread.currentThread().getName());
}
public static void main (String[] args) {
ThreadTest tt = new ThreadTest();
Thread[] ts =new Thread[10];
for (int i =0; i < ts.length; i++)
ts[i] = new Thread(tt);
for (Thread t : ts)
t.start();
}
}
在我的電腦上運行的結果是:
Thread-0
Thread-1
Thread-3
Thread-5
Thread-2
Thread-7
Thread-4
Thread-9
Thread-6
Thread-8
而且每次運行的結果都是不同的!繼續引用前面的話,一旦涉及到線程,其運行多半是沒有保障。這個保障是指線程的運行完全是由調度程序控制的,我們沒法控制它的執行順序,持續時間也沒有保障,有着不可預料的結果。
9、線程的狀態。
A、新狀態。
實例化Thread對象,但沒有調用start()方法時的狀態。
ThreadTest tt = new ThreadTest();
或者Thread t = new Thread (tt);
此時雖然創建了Thread對象,如前所述,但是它們不是活的,不能通過isAlive()測試。
B、就緒狀態。
線程有資格運行,但調度程序還沒有把它選爲運行線程所處的狀態。也就是具備了運行的條件,一旦被選中馬上就能運行。
也是調用start()方法後但沒運行的狀態。此時雖然沒在運行,但是被認爲是活的,能通過isAlive()測試。而且在線程運行之後、或者被阻塞、等待或者睡眠狀態回來之後,線程首先進入就緒狀態。
C、運行狀態。
從就緒狀態池(注意不是隊列,是池)中選擇一個爲當前執行進程時,該線程所處的狀態。
D、等待、阻塞、睡眠狀態。
這三種狀態有一個共同點:線程依然是活的,但是缺少運行的條件,一旦具備了條就就可以轉爲就緒狀態(不能直接轉爲運行狀態)。另外,suspend()和stop()方法已經被廢棄了,比較危險,不要再用了。
E、死亡狀態。
一個線程的run()方法運行結束,那麼該線程完成其歷史使命,它的棧結構將解散,也就是死亡了。但是它仍然是一個Thread對象,我們仍可以引用它,就像其他對象一樣!它也不會被垃圾回收器回收了,因爲對該對象的引用仍然存在。
如此說來,即使run()方法運行結束線程也沒有死啊!事實是,一旦線程死去,它就永遠不能重新啓動了,也就是說,不能再用start()方法讓它運行起來!如果強來的話會拋出IllegalThreadStateException異常。如:
t.start();
t.start();
放棄吧,人工呼吸或者心臟起搏器都無濟於事……線程也屬於一次性用品。
10、阻止線程運行。
A、睡眠。sleep()方法
讓線程睡眠的理由很多,比如:認爲該線程運行得太快,需要減緩一下,以便和其他線程協調;查詢當時的股票價格,每睡5分鐘查詢一次,可以節省帶寬,而且即時性要求也不那麼高。
用Thread的靜態方法可以實現Thread.sleep(5*60*1000); 睡上5分鐘吧。sleep的參數是毫秒。但是要注意sleep()方法會拋出檢查異常InterruptedException,對於檢查異常,我們要麼聲明,要麼使用處理程序。
try {
Thread.sleep(20000);
}
catch (InterruptedException ie) {
ie.printStackTrace();
}
既然有了sleep()方法,我們是不是可以控制線程的執行順序了!每個線程執行完畢都睡上一覺?這樣就能控制線程的運行順序了,下面是書上的一個例子:
class ThreadTest implements Runnable{
public void run(){
for (int i = 1; i<4; i++){
System.out.println (Thread.currentThread().getName());
try {
Thread.sleep(1000);
} catch (InterruptedException ie) { }
}
}
public static void main (String[] args) {
ThreadTest tt = new ThreadTest();
Thread t0 = new Thread(tt,"Thread 0");
Thread t1 = new Thread(tt,"Thread 1");
Thread t2 = new Thread(tt,"Thread 2");
t0.start();
t1.start();
t2.start();
}
}
並且給出了結果:
Thread 0
Thread 1
Thread 2
Thread 0
Thread 1
Thread 2
Thread 0
Thread 1
Thread 2
也就是Thread 0 Thread 1 Thread 2 按照這個順序交替出現,作者指出雖然結果和我們預料的似乎相同,但是這個結果是不可靠的。果然被我的雙核電腦驗證了:
Thread 0
Thread 1
Thread 2
Thread 2
Thread 0
Thread 1
Thread 1
Thread 0
Thread 2
看來線程真的很不可靠啊。但是儘管如此,sleep()方法仍然是保證所有線程都有運行機會的最好方法。至少它保證了一個線程進入運行之後不會一直到運行完位置。
時間的精確性。再強調一下,線程醒來之後不會進入運行狀態,而是進入就緒狀態。因此sleep()中指定的時間不是線程不運行的精確時間!不能依賴sleep()方法提供十分精確的定時。我們可以看到很多應用程序用sleep()作爲定時器,而且沒什麼不好的,確實如此,但是我們一定要知道sleep()不能保證線程醒來就能馬上進入運行狀態,是不精確的。
sleep()方法是一個靜態的方法,它所指的是當前正在執行的線程休眠一個毫秒數。看到某些書上的Thread.currentThread().sleep(1000); ,其實是不必要的。Thread.sleep(1000);就可以了。類似於getName()方法不是靜態方法,它必須針對具體某個線程對象,這時用取得當前線程的方法Thread.currentThread().getName();
B、線程優先級和讓步。
線程的優先級。在大多數jvm實現中調度程序使用基於線程優先級的搶先調度機制。如果一個線程進入可運行狀態,並且它比池中的任何其他線程和當前運行的進程的具有更高的優先級,則優先級較低的線程進入可運行狀態,最高優先級的線程被選擇去執行。
於是就有了這樣的結論:當前運行線程的優先級通常不會比池中任何線程的優先級低。但是並不是所有的jvm的調度都這樣,因此一定不能依賴於線程優先級來保證程序的正確操作,這仍然是沒有保障的,要把線程優先級用作一種提高程序效率的方法,並且這種方法也不能依賴優先級的操作。
另外一個沒有保障的操作是:當前運行的線程與池中的線程,或者池中的線程具有相同的優先級時,JVM的調度實現會選擇它喜歡的線程。也許是選擇一個去運行,直至其完成;或者用分配時間片的方式,爲每個線程提供均等的機會。
優先級用正整數設置,通常爲1-10,JVM從不會改變一個線程的優先級。默認情況下,優先級是5。Thread類具有三個定義線程優先級範圍的靜態最終常量:Thread.MIN_PRIORITY (爲1) Thread.NORM_PRIORITY (爲5) Thread.MAX_PRIORITY (爲10)
靜態Thread.yield()方法。
它的作用是讓當前運行的線程回到可運行狀態,以便讓具有同等優先級的其他線程運行。用yield()方法的目的是讓同等優先級的線程能適當地輪轉。但是,並不能保證達到此效果!因爲,即使當前變成可運行狀態,可是還有可能再次被JVM選中!也就是連任。
非靜態join()方法。
讓一個線程加入到另一個線程的尾部。讓B線程加入A線程,意味着在A線程運行完成之前,B線程不會進入可運行狀態。
Thread t = new Thread();
t.start();
t.join;
這段代碼的意思是取得當前的線程,把它加入到t線程的尾部,等t線程運行完畢之後,原線程繼續運行。書中的例子在我的電腦裏效果很糟糕,看不出什麼效果來。也許是CPU太快了,而且是雙核的;也許是JDK1.6的原因?
11、沒總結完。線程這部分很重要,內容也很多,看太快容易消化不良,偶要慢慢地消化掉……
在java技術中,線程通常是搶佔式的而不需要時間片分配進程(分配給每個線程相等的cpu時間的進程)。一個經常犯的錯誤是認爲“搶佔”就是“分配時間片”。
在Solaris平臺上的運行環境中,相同優先級的線程不能相互搶佔對方的cpu時間。但是,在使用時間片的windows平臺運行環境中,可以搶佔相同甚至更高優先級的線程的cpu時間。搶佔並不是絕對的,可是大多數的JVM的實現結果在行爲上表現出了嚴格的搶佔。縱觀JVM的實現,並沒有絕對的搶佔或是時間片,而是依賴於編碼者對wait和sleep這兩個方法的使用。
搶佔式調度模型就是許多線程屬於可以運行狀態(等待狀態),但實際上只有一個線程在運行。該線程一直運行到它終止進入可運行狀態(等待狀態)或是另一個具有更高優先級的線程變成可運行狀態。在後一種情況下,底優先級的線程被高優先級的線程搶佔,高優先級的線程獲得運行的機會。
線程可以因爲各種各樣的原因終止並進入可運行狀態(因爲堵塞)。例如,線程的代碼可以在適當時候執行Thread.sleep()方法,故意讓線程中止;線程可能爲了訪問資源而不得不等待直到該資源可用爲止。
所有可運行的線程根據優先級保持在不同的池中。一旦被堵塞的線程進入可運行狀態,它將會被放回適當的可運行池中。非空最高優先級的池中的線程將獲得cpu時間。
最後一個句子是不精確的,因爲:
(1)在大多數的JVM實現中,雖然不能保證說優先級有任何意義,但優先級看起來象是用搶佔方式工作。
(2)微軟windows的評價影響線程的行爲,以至儘管一個處於可運行狀態的優先級爲5的java線程正在等待cpu時間,但是一個優先級爲4的java線程卻可能正在運行。
實際上,許多JVM用隊列來實現池,但沒有保證行爲。