Java文法總結 – 線程 ————轉

來源:互聯網
上載者:User

如有侵權 請通知作者予以刪除 謝謝~


提到線程好像是件很麻煩很複雜的事,事實上確實如此,涉及到線程的編程是很講究技巧的。這就需要我們變換思維方式,瞭解線程機制的比較通用的技巧,寫出高
效的、不依賴於某個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");
        }
        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()方法,而不是它自己的。
如上例,可以把一個目標賦給多個線程,這意味著幾個執行線程將運行完全相同的作業。

6、什麼時候線程是活的?
在調用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()方法內代碼的最好方法。

7、啟動新線程。
線程的啟動要調用start()方法,只有這樣才能建立新的調用棧。而直接調用run()方法的話,就不會建立新的調用棧,也就不會建立新的線程,run()方法就與普通的方法沒什麼兩樣了!

8、給線程起個有意義的名字。
沒有該線程命名的話,線程會有一個預設的名字,格式是:“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();
        }
    }

9、“沒有保障”的多線程的運行。下面的代碼可能令人印象深刻。
    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
而且每次啟動並執行結果都是不同的!繼續引用前面的話,一旦涉及到線程,其運行多半是沒有保障。這個保障是指線程的運行完全是由發送器控制的,我們沒法控制它的執行順序,期間也沒有保障,有著不可預料的結果。

10、線程的狀態。
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();
放棄吧,人工呼吸或者心臟起搏器都無濟於事……線程也屬於一次性用品。

11、阻止線程運行。
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的原因?

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.