從零開始學Java之線程詳解(1):原理、創建

Java線程:概念與原理

一、操作系統中線程和進程的概念


現在的操作系統是多任務操作系統。多線程是實現多任務的一種方式。


進程是指一個內存中運行的應用程序,每個進程都有自己獨立的一塊內存空間,一個進程中可以啓動多個線程。比如在Windows系統中,一個運行的exe就是一個進程。

 

線程是指進程中的一個執行流程,一個進程中可以運行多個線程。比如java.exe進程中可以運行很多線程。線程總是屬於某個進程,進程中的多個線程共享進程的內存。

 

同時執行是人的感覺,在線程之間實際上輪換執行。

 

二、Java中的線程


Java中,線程指兩件不同的事情:
1
java.lang.Thread類的一個實例;

2、線程的執行。

 

使用java.lang.Thread類或者java.lang.Runnable接口編寫代碼來定義、實例化和啓動新線程。

 

一個Thread類實例只是一個對象,像Java中的任何其他對象一樣,具有變量和方法,生死於堆上。

 

Java中,每個線程都有一個調用棧,即使不在程序中創建任何新的線程,線程也在後臺運行着。

 

一個Java應用總是從main()方法開始運行,mian()方法運行在一個線程內,它被稱爲主線程。

 

一旦創建一個新的線程,就產生一個新的調用棧。

 

線程總體分兩類:用戶線程和守候線程。


當所有用戶線程執行完畢的時候,JVM自動關閉。但是守候線程卻不獨立於JVM,守候線程一般是由操作系統或者用戶自己創建的

Java線程:創建與啓動

一、定義線程

 

1、擴展java.lang.Thread類。

 

此類中有個run()方法,應該注意其用法:

public void run()

如果該線程是使用獨立的Runnable運行對象構造的,則調用該Runnable對象的run方法;否則,該方法不執行任何操作並返回。

 

Thread的子類應該重寫該方法。

2、實現java.lang.Runnable接口。

 

void run()

使用實現接口Runnable的對象創建一個線程時,啓動該線程將導致在獨立執行的線程中調用對象的run方法。

 

方法run的常規協定是,它可能執行任何所需的操作。

 

二、實例化線程

 

1、如果是擴展java.lang.Thread類的線程,則直接new即可。

 

2、如果是實現了java.lang.Runnable接口的類,則用Thread的構造方法:

Thread(Runnable target) 
Thread(Runnable target, String name) 
Thread(ThreadGroup group, Runnable target) 
Thread(ThreadGroup group, Runnable target, String name) 
Thread(ThreadGroup group, Runnable target, String name, long stackSize)

 

三、啓動線程

 

在線程的Thread對象上調用start()方法,而不是run()或者別的方法。

 

在調用start()方法之前:線程處於新狀態中,新狀態指有一個Thread對象,但還沒有一個真正的線程。

 

在調用start()方法之後:發生了一系列複雜的事情

啓動新的執行線程(具有新的調用棧);

該線程從新狀態轉移到可運行狀態;

當該線程獲得機會執行時,其目標run()方法將運行。

 

注意:對Java來說,run()方法沒有任何特別之處。像main()方法一樣,它只是新線程知道調用的方法名稱(和簽名)。因此,在Runnable上或者Thread上調用run方法是合法的。但並不啓動新的線程。

 

 

四、例子

 

1、實現Runnable接口的多線程例子

/** 
實現Runnable接口的類

* @author leizhimin 2008-9-13 18:12:10 
*/
 
publicclass DoSomethingimplements Runnable {
    
private String name;

    
public DoSomething(String name) {
        
this.name = name;
    } 

    
publicvoid run() {
        
for (int i = 0; i < 5; i++) {
            
for (long k = 0; k < 100000000; k++) ;
            System.out.println(name + 
": " + i);
        } 
    } 
}

 

/** 
測試Runnable類實現的多線程程序

* @author leizhimin 2008-9-13 18:15:02 
*/
 
publicclass TestRunnable {
    
publicstaticvoid main(String[] args) {
        DoSomething ds1 = 
new DoSomething("阿三");
        DoSomething ds2 = 
new DoSomething("李四");

        Thread t1 = 
new Thread(ds1);
        Thread t2 = 
new Thread(ds2);

        t1.start(); 
        t2.start(); 
    } 
}

 

執行結果:

李四: 0
阿三: 0 
李四: 1 
阿三: 1 
李四: 2 
李四: 3 
阿三: 2 
李四: 4 
阿三: 3 
阿三: 4 

Process finished with exit code 0

 

2、擴展Thread類實現的多線程例子

 

/** 
測試擴展Thread類實現的多線程程序

* @author leizhimin 2008-9-13 18:22:13 
*/
 
publicclass TestThreadextends Thread{ 
    
public TestThread(String name) {
        
super(name);
    } 

    
publicvoid run() {
        
for(int i = 0;i<5;i++){
            
for(long k= 0; k <100000000;k++);
            System.out.println(
this.getName()+" :"+i);
        } 
    } 

    
publicstaticvoid main(String[] args) {
        Thread t1 = 
new TestThread("阿三");
        Thread t2 = 
new TestThread("李四");
        t1.start(); 
        t2.start(); 
    } 
}

 

執行結果:

阿三 :0
李四 :0 
阿三 :1 
李四 :1 
阿三 :2 
李四 :2 
阿三 :3 
阿三 :4 
李四 :3 
李四 :4 

Process finished with exit code 0

 

對於上面的多線程程序代碼來說,輸出的結果是不確定的。其中的一條語句for(long k= 0; k <100000000;k++);是用來模擬一個非常耗時的操作的。

 

五、一些常見問題

 

1、線程的名字,一個運行中的線程總是有名字的,名字有兩個來源,一個是虛擬機自己給的名字,一個是你自己的定的名字。在沒有指定線程名字的情況下,虛擬機總會爲線程指定名字,並且主線程的名字總是mian,非主線程的名字不確定。

2、線程都可以設置名字,也可以獲取線程的名字,連主線程也不例外。

3、獲取當前線程的對象的方法是:Thread.currentThread()

4、在上面的代碼中,只能保證:每個線程都將啓動,每個線程都將運行直到完成。一系列線程以某種順序啓動並不意味着將按該順序執行。對於任何一組啓動的線程來說,調度程序不能保證其執行次序,持續時間也無法保證。

5、當線程目標run()方法結束時該線程完成。

6、一旦線程啓動,它就永遠不能再重新啓動。只有一個新的線程可以被啓動,並且只能一次。一個可運行的線程或死線程可以被重新啓動。

7、線程的調度是JVM的一部分,在一個CPU的機器上上,實際上一次只能運行一個線程。一次只有一個線程棧執行。JVM線程調度程序決定實際運行哪個處於可運行狀態的線程。

衆多可運行線程中的某一個會被選中做爲當前線程。可運行線程被選擇運行的順序是沒有保障的。

8、儘管通常採用隊列形式,但這是沒有保障的。隊列形式是指當一個線程完成一輪時,它移到可運行隊列的尾部等待,直到它最終排隊到該隊列的前端爲止,它才能被再次選中。事實上,我們把它稱爲可運行池而不是一個可運行隊列,目的是幫助認識線程並不都是以某種有保障的順序排列唱呢個一個隊列的事實。

9、儘管我們沒有無法控制線程調度程序,但可以通過別的方式來影響線程調度的方式。

Java線程:線程棧模型與線程的變量

要理解線程調度的原理,以及線程執行過程,必須理解線程棧模型。

線程棧是指某時刻時內存中線程調度的棧信息,當前調用的方法總是位於棧頂。線程棧的內容是隨着程序的運行動態變化的,因此研究線程棧必須選擇一個運行的時刻(實際上指代碼運行到什麼地方)

 

下面通過一個示例性的代碼說明線程(調用)棧的變化過程。

 

 

這幅圖描述在代碼執行到兩個不同時刻12時候,虛擬機線程調用棧示意圖。

 

當程序執行到t.start();時候,程序多出一個分支(增加了一個調用棧B),這樣,棧A、棧B並行執行。

 

從這裏就可以看出方法調用和線程啓動的區別了。

Java線程:線程狀態的轉換

一、線程狀態

 

線程的狀態轉換是線程控制的基礎。線程狀態總的可分爲五大狀態:分別是生、死、可運行、運行、等待/阻塞。用一個圖來描述如下:

 

1、新狀態:線程對象已經創建,還沒有在其上調用start()方法。

 

2、可運行狀態:當線程有資格運行,但調度程序還沒有把它選定爲運行線程時線程所處的狀態。當start()方法調用時,線程首先進入可運行狀態。在線程運行之後或者從阻塞、等待或睡眠狀態回來後,也返回到可運行狀態。

 

3、運行狀態:線程調度程序從可運行池中選擇一個線程作爲當前線程時線程所處的狀態。這也是線程進入運行狀態的唯一一種方式。

 

4、等待/阻塞/睡眠狀態:這是線程有資格運行時它所處的狀態。實際上這個三狀態組合爲一種,其共同點是:線程仍舊是活的,但是當前沒有條件運行。換句話說,它是可運行的,但是如果某件事件出現,他可能返回到可運行狀態。

 

5、死亡態:當線程的run()方法完成時就認爲它死去。這個線程對象也許是活的,但是,它已經不是一個單獨執行的線程。線程一旦死亡,就不能復生。如果在一個死去的線程上調用start()方法,會拋出java.lang.IllegalThreadStateException異常。

 

有關詳細狀態轉換圖可以參看本人的Java多線程編程總結中的圖

 

二、阻止線程執行

對於線程的阻止,考慮一下三個方面,不考慮IO阻塞的情況:

睡眠;

等待;

因爲需要一個對象的鎖定而被阻塞。

 

1、睡眠

Thread.sleep(long millis)Thread.sleep(long millis, int nanos)靜態方法強制當前正在執行的線程休眠(暫停執行),以減慢線程。當線程睡眠時,它入睡在某個地方,在甦醒之前不會返回到可運行狀態。當睡眠時間到期,則返回到可運行狀態。

 

線程睡眠的原因:線程執行太快,或者需要強制進入下一輪,因爲Java規範不保證合理的輪換。

 

睡眠的實現:調用靜態方法。

        try {
            Thread.sleep(123);
        } catch (InterruptedException e) {
            e.printStackTrace();  
        }

 

睡眠的位置:爲了讓其他線程有機會執行,可以將Thread.sleep()的調用放線程run()之內。這樣才能保證該線程執行過程中會睡眠。

 

例如,在前面的例子中,將一個耗時的操作改爲睡眠,以減慢線程的執行。可以這麼寫:

 

    public void run() {
        for(int i = 0;i<5;i++){

// 很耗時的操作,用來減慢線程的執行
//            for(long k= 0; k <100000000;k++);
            try {
                Thread.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();  .
            }

            System.out.println(this.getName()+" :"+i);
        }
    }

 

運行結果:

阿三 :0
李四 :0 
阿三 :1 
阿三 :2 
阿三 :3 
李四 :1 
李四 :2 
阿三 :4 
李四 :3 
李四 :4 

Process finished with exit code 0

 

這樣,線程在每次執行過程中,總會睡眠3毫秒,睡眠了,其他的線程就有機會執行了。

 

注意:

1、線程睡眠是幫助所有線程獲得運行機會的最好方法。

2、線程睡眠到期自動甦醒,並返回到可運行狀態,不是運行狀態。sleep()中指定的時間是線程不會運行的最短時間。因此,sleep()方法不能保證該線程睡眠到期後就開始執行。

3sleep()是靜態方法,只能控制當前正在運行的線程。

 

下面給個例子:

/** 
一個計數器,計數到100,在每個數字之間暫停1秒,每隔10個數字輸出一個字符串

* @author leizhimin 2008-9-14 9:53:49 
*/
 
publicclass MyThreadextends Thread {

    
publicvoid run() {
        
for (int i = 0; i < 100; i++) {
            
if ((i) % 10 == 0) {
                System.out.println(
"-------" + i);
            } 
            System.out.print(i); 
            
try {
                Thread.sleep(1); 
                System.out.print(
"    線程睡眠1毫秒!\n");
            } 
catch (InterruptedException e) {
                e.printStackTrace(); 
            } 
        } 
    } 

    
publicstaticvoid main(String[] args) {
        
new MyThread().start();
    } 
}

 

-------0 
0    
線程睡眠1毫秒!
1    
線程睡眠1毫秒!
2    
線程睡眠1毫秒!
3    
線程睡眠1毫秒!
4    
線程睡眠1毫秒!
5    
線程睡眠1毫秒!
6    
線程睡眠1毫秒!
7    
線程睡眠1毫秒!
8    
線程睡眠1毫秒!
9    
線程睡眠1毫秒!
-------10 
10    
線程睡眠1毫秒!
11    
線程睡眠1毫秒!
12    
線程睡眠1毫秒!
13    
線程睡眠1毫秒!
14    
線程睡眠1毫秒!
15    
線程睡眠1毫秒!
16    
線程睡眠1毫秒!
17    
線程睡眠1毫秒!
18    
線程睡眠1毫秒!
19    
線程睡眠1毫秒!
-------20 
20    
線程睡眠1毫秒!
21    
線程睡眠1毫秒!
22    
線程睡眠1毫秒!
23    
線程睡眠1毫秒!
24    
線程睡眠1毫秒!
25    
線程睡眠1毫秒!
26    
線程睡眠1毫秒!
27    
線程睡眠1毫秒!
28    
線程睡眠1毫秒!
29    
線程睡眠1毫秒!
-------30 
30    
線程睡眠1毫秒!
31    
線程睡眠1毫秒!
32    
線程睡眠1毫秒!
33    
線程睡眠1毫秒!
34    
線程睡眠1毫秒!
35    
線程睡眠1毫秒!
36    
線程睡眠1毫秒!
37    
線程睡眠1毫秒!
38    
線程睡眠1毫秒!
39    
線程睡眠1毫秒!
-------40 
40    
線程睡眠1毫秒!
41    
線程睡眠1毫秒!
42    
線程睡眠1毫秒!
43    
線程睡眠1毫秒!
44    
線程睡眠1毫秒!
45    
線程睡眠1毫秒!
46    
線程睡眠1毫秒!
47    
線程睡眠1毫秒!
48    
線程睡眠1毫秒!
49    
線程睡眠1毫秒!
-------50 
50    
線程睡眠1毫秒!
51    
線程睡眠1毫秒!
52    
線程睡眠1毫秒!
53    
線程睡眠1毫秒!
54    
線程睡眠1毫秒!
55    
線程睡眠1毫秒!
56    
線程睡眠1毫秒!
57    
線程睡眠1毫秒!
58    
線程睡眠1毫秒!
59    
線程睡眠1毫秒!
-------60 
60    
線程睡眠1毫秒!
61    
線程睡眠1毫秒!
62    
線程睡眠1毫秒!
63    
線程睡眠1毫秒!
64    
線程睡眠1毫秒!
65    
線程睡眠1毫秒!
66    
線程睡眠1毫秒!
67    
線程睡眠1毫秒!
68    
線程睡眠1毫秒!
69    
線程睡眠1毫秒!
-------70 
70    
線程睡眠1毫秒!
71    
線程睡眠1毫秒!
72    
線程睡眠1毫秒!
73    
線程睡眠1毫秒!
74    
線程睡眠1毫秒!
75    
線程睡眠1毫秒!
76    
線程睡眠1毫秒!
77    
線程睡眠1毫秒!
78    
線程睡眠1毫秒!
79    
線程睡眠1毫秒!
-------80 
80    
線程睡眠1毫秒!
81    
線程睡眠1毫秒!
82    
線程睡眠1毫秒!
83    
線程睡眠1毫秒!
84    
線程睡眠1毫秒!
85    
線程睡眠1毫秒!
86    
線程睡眠1毫秒!
87    
線程睡眠1毫秒!
88    
線程睡眠1毫秒!
89    
線程睡眠1毫秒!
-------90 
90    
線程睡眠1毫秒!
91    
線程睡眠1毫秒!
92    
線程睡眠1毫秒!
93    
線程睡眠1毫秒!
94    
線程睡眠1毫秒!
95    
線程睡眠1毫秒!
96    
線程睡眠1毫秒!
97    
線程睡眠1毫秒!
98    
線程睡眠1毫秒!
99    
線程睡眠1毫秒!

Process finished with exit code 0


2
、線程的優先級和線程讓步yield()

線程的讓步是通過Thread.yield()來實現的。yield()方法的作用是:暫停當前正在執行的線程對象,並執行其他線程。

 

要理解yield(),必須瞭解線程的優先級的概念。線程總是存在優先級,優先級範圍在1~10之間。JVM線程調度程序是基於優先級的搶先調度機制。在大多數情況下,當前運行的線程優先級將大於或等於線程池中任何線程的優先級。但這僅僅是大多數情況。

 

注意:當設計多線程應用程序的時候,一定不要依賴於線程的優先級。因爲線程調度優先級操作是沒有保障的,只能把線程優先級作用作爲一種提高程序效率的方法,但是要保證程序不依賴這種操作。

 

當線程池中線程都具有相同的優先級,調度程序的JVM實現自由選擇它喜歡的線程。這時候調度程序的操作有兩種可能:一是選擇一個線程運行,直到它阻塞或者運行完成爲止。二是時間分片,爲池內的每個線程提供均等的運行機會。

 

設置線程的優先級:線程默認的優先級是創建它的執行線程的優先級。可以通過setPriority(int newPriority)更改線程的優先級。例如:

        Thread t = new MyThread();
        t.setPriority(8);
        t.start();

線程優先級爲1~10之間的正整數,JVM從不會改變一個線程的優先級。然而,1~10之間的值是沒有保證的。一些JVM可能不能識別10個不同的值,而將這些優先級進行每兩個或多個合併,變成少於10個的優先級,則兩個或多個優先級的線程可能被映射爲一個優先級。

 

線程默認優先級是5Thread類中有三個常量,定義線程優先級範圍:

static int MAX_PRIORITY 
          
線程可以具有的最高優先級。
static int MIN_PRIORITY 
          
線程可以具有的最低優先級。
static int NORM_PRIORITY 
          
分配給線程的默認優先級。

 

3Thread.yield()方法

 

Thread.yield()方法作用是:暫停當前正在執行的線程對象,並執行其他線程。

yield()應該做的是讓當前運行線程回到可運行狀態,以允許具有相同優先級的其他線程獲得運行機會。因此,使用yield()的目的是讓相同優先級的線程之間能適當的輪轉執行。但是,實際中無法保證yield()達到讓步目的,因爲讓步的線程還有可能被線程調度程序再次選中。

結論:yield()從未導致線程轉到等待/睡眠/阻塞狀態。在大多數情況下,yield()將導致線程從運行狀態轉到可運行狀態,但有可能沒有效果。

 

4join()方法

 

Thread的非靜態方法join()讓一個線程B“加入到另外一個線程A的尾部。在A執行完畢之前,B不能工作。例如:

        Thread t = new MyThread();
        t.start();
        t.join();

另外,join()方法還有帶超時限制的重載版本。例如t.join(5000);則讓線程等待5000毫秒,如果超過這個時間,則停止等待,變爲可運行狀態。

 

線程的加入join()對線程棧導致的結果是線程棧發生了變化,當然這些變化都是瞬時的。下面給示意圖:

 

 

 

小結

到目前位置,介紹了線程離開運行狀態的3種方法:

1、調用Thread.sleep():使當前線程睡眠至少多少毫秒(儘管它可能在指定的時間之前被中斷)。

2、調用Thread.yield():不能保障太多事情,儘管通常它會讓當前運行線程回到可運行性狀態,使得有相同優先級的線程有機會執行。

3、調用join()方法:保證當前線程停止執行,直到該線程所加入的線程完成爲止。然而,如果它加入的線程沒有存活,則當前線程不需要停止。

 

除了以上三種方式外,還有下面幾種特殊情況可能使線程離開運行狀態:

1、線程的run()方法完成。

2、在對象上調用wait()方法(不是在線程上調用)。

3、線程不能在對象上獲得鎖定,它正試圖運行該對象的方法代碼。

4、線程調度程序可以決定將當前運行狀態移動到可運行狀態,以便讓另一個線程獲得運行機會,而不需要任何理由。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章