一提到線程好像是件很麻煩很複雜的事,事實上确實如此,涉及到線程的程式設計是很講究技巧的。這就需要我們變換思維方式,了解線程機制的比較通用的技巧,寫出高效的、不依賴于某個jvm實作的程式來。畢竟僅僅就java而言,各個虛拟機的實作是不同的。學習線程時,最令我印象深刻的就是那種不确定性、沒有保障性,各個線程的運作完全是以不可預料的方式和速度推進,有的一個程式運作了n次,其結果差異性很大。
1、什麼是線程?線程是彼此互相獨立的、能獨立運作的子任務,并且每個線程都有自己的調用棧。所謂的多任務是通過周期性地将cpu時間片切換到不同的子任務,雖然從微觀上看來,單核的cpu上同時隻運作一個子任務,但是從宏觀來看,每個子任務似乎是同時連續運作的。(但是java的線程不是按時間片配置設定的,在本文的最後引用了一段網友翻譯的java原著中對線程的了解。)
2、在java中,線程指兩個不同的内容:一是java.lang.thread類的一個對象;另外也可以指線程的執行。線程對象和其他的對象一樣,在堆上建立、運作、死亡。但不同之處是線程的執行是一個輕量級的程序,有它自己的調用棧。
可以這樣想,每個調用棧都對應一個線程,每個線程又對應一個調用棧。
我們運作java程式時有一個入口函數main()函數,它對應的線程被稱為主線程。一個新線程一旦被建立,就産生一個新調用棧,從原主線程中脫離,也就是與主線程并發執行。
4、當提到線程時,很少是有保障的。我們必須了解到什麼是有保障的操作,什麼是無保障的操作,以便設計的程式在各種jvm上都能很好地工作。比如,在某些jvm實作中,把java線程映射為本地作業系統的線程。這是java核心的一部分。
5、線程的建立。
建立線程有兩種方式:
a、繼承java.lang.thread類。
class threadtest extends
thread{
public void run() {
system.out.println ("someting run
here!");
}
public void run(string s){
system.out.println ("string in run is " +
s);
public static void main (string[] args)
{
threadtest tt = new
threadtest();
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 {
public void run() {
system.out.println ("someting run here");
thread
t1 = new thread(tt);
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()方法,而不是它自己的。
如上例,可以把一個目标賦給多個線程,這意味着幾個執行線程将運作完全相同的作業。
6、什麼時候線程是活的?
在調用start()方法開始執行線程之前,線程的狀态還不是活的。測試程式如下:
thread t1 = new thread(tt);
system.out.println
(t1.isalive());
t1.start();
結果輸出:
false
true
isalive方法是确定一個線程是否已經啟動,而且還沒完成run()方法内代碼的最好方法。
7、啟動新線程。
線程的啟動要調用start()方法,隻有這樣才能建立新的調用棧。而直接調用run()方法的話,就不會建立新的調用棧,也就不會建立新的線程,run()方法就與普通的方法沒什麼兩樣了!
8、給線程起個有意義的名字。
沒有該線程命名的話,線程會有一個預設的名字,格式是:“thread-”加上線程的序号,如:thread-0
這看起來可讀性不好,不能從名字分辨出該線程具有什麼功能。下面是給線程命名的方式。
第一種:用setname()函數
第二種:選用帶線程命名的構造器
class threadtest implements runnable{
public void run(){
(thread.currentthread().getname());
}
threadtest tt = new
threadtest();
//thread t = new thread (tt,"eat apple");
t = new thread (tt);
t.setname("eat apple");
t.start();
9、“沒有保障”的多線程的運作。下面的代碼可能令人印象深刻。
implements runnable{
void run(){
system.out.println
thread[] ts =new thread[10];
for (int i =0; i < ts.length;
i++)
ts[i] = new
thread(tt);
for (thread t : ts)
在我的電腦上運作的結果是:
thread-0
thread-1
thread-3
thread-5
thread-2
thread-7
thread-4
thread-9
thread-6
thread-8
而且每次運作的結果都是不同的!繼續引用前面的話,一旦涉及到線程,其運作多半是沒有保障。這個保障是指線程的運作完全是由排程程式控制的,我們沒法控制它的執行順序,持續時間也沒有保障,有着不可預料的結果。
10、線程的狀态。
a、新狀态。
執行個體化thread對象,但沒有調用start()方法時的狀态。
threadtest tt = new
或者thread
此時雖然建立了thread對象,如前所述,但是它們不是活的,不能通過isalive()測試。
b、就緒狀态。
線程有資格運作,但排程程式還沒有把它選為運作線程所處的狀态。也就是具備了運作的條件,一旦被選中馬上就能運作。
也是調用start()方法後但沒運作的狀态。此時雖然沒在運作,但是被認為是活的,能通過isalive()測試。而且線上程運作之後、或者被阻塞、等待或者睡眠狀态回來之後,線程首先進入就緒狀态。
c、運作狀态。
從就緒狀态池(注意不是隊列,是池)中選擇一個為目前執行程序時,該線程所處的狀态。
d、等待、阻塞、睡眠狀态。
這三種狀态有一個共同點:線程依然是活的,但是缺少運作的條件,一旦具備了條就就可以轉為就緒狀态(不能直接轉為運作狀态)。另外,suspend()和stop()方法已經被廢棄了,比較危險,不要再用了。
e、死亡狀态。
一個線程的run()方法運作結束,那麼該線程完成其曆史使命,它的棧結構将解散,也就是死亡了。但是它仍然是一個thread對象,我們仍可以引用它,就像其他對象一樣!它也不會被垃圾回收器回收了,因為對該對象的引用仍然存在。
如此說來,即使run()方法運作結束線程也沒有死啊!事實是,一旦線程死去,它就永遠不能重新啟動了,也就是說,不能再用start()方法讓它運作起來!如果強來的話會抛出illegalthreadstateexception異常。如:
t.start();
放棄吧,人工呼吸或者心髒起搏器都無濟于事……線程也屬于一次性用品。
11、阻止線程運作。
a、睡眠。sleep()方法
讓線程睡眠的理由很多,比如:認為該線程運作得太快,需要減緩一下,以便和其他線程協調;查詢當時的股票價格,每睡5分鐘查詢一次,可以節省帶寬,而且即時性要求也不那麼高。
用thread的靜态方法可以實作thread.sleep(5*60*1000);
睡上5分鐘吧。sleep的參數是毫秒。但是要注意sleep()方法會抛出檢查異常interruptedexception,對于檢查異常,我們要麼聲明,要麼使用處理程式。
try {
thread.sleep(20000);
}
catch (interruptedexception ie) {
ie.printstacktrace();
既然有了sleep()方法,我們是不是可以控制線程的執行順序了!每個線程執行完畢都睡上一覺?這樣就能控制線程的運作順序了,下面是書上的一個例子:
for (int i = 1; i<4; i++){
system.out.println
try {
thread.sleep(1000);
} catch (interruptedexception ie) {
}
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
1
2
也就是thread 0 thread 1
thread 2 按照這個順序交替出現,作者指出雖然結果和我們預料的似乎相同,但是這個結果是不可靠的。果然被我的雙核電腦驗證了:
thread 0
看來線程真的很不可靠啊。但是盡管如此,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.join;
這段代碼的意思是取得目前的線程,把它加入到t線程的尾部,等t線程運作完畢之後,原線程繼續運作。書中的例子在我的電腦裡效果很糟糕,看不出什麼效果來。也許是cpu太快了,而且是雙核的;也許是jdk1.6的原因?
12、沒總結完。線程這部分很重要,内容也很多,看太快容易消化不良,偶要慢慢地消化掉……