Java 多線程詳解(一)

一、進程與線程

1、進程是什麼?

狹義定義:進程是正在運行的程序的實例(an instance of a computer program that is being executed)。

廣義定義:進程是一個具有一定獨立功能的程序關於某個數據集合的一次運行活動。它是操作系統動態執行的基本單元,在傳統的操作系統中,進程既是基本的分配單元,也是基本的執行單元。

2、線程是什麼?

線程,有時被稱爲輕量級進程(Lightweight Process,LWP),是程序執行流的最小單元。一個標準的線程由線程ID,當前指令指針(PC),寄存器集合和堆棧組成。另外,線程是進程中的一個實體,是被系統獨立調度和分派的基本單位,線程自己不擁有系統資源,只擁有一點兒在運行中必不可少的資源,但它可與同屬一個進程的其它線程共享進程所擁有的全部資源。

3、進程和線程的區別?

進程和線程的主要差別在於它們是不同的操作系統資源管理方式
進程有獨立的地址空間,一個進程崩潰後,在保護模式下不會對其它進程產生影響,而線程只是一個進程中的不同執行路徑。線程有自己的堆棧和局部變量,但線程之間沒有單獨的地址空間,一個線程死掉就等於整個進程死掉,所以多進程的程序要比多線程的程序健壯,但在進程切換時,耗費資源較大,效率要差一些。但對於一些要求同時進行並且又要共享某些變量的併發操作,只能用線程,不能用進程。

簡言之,線程與進程的區別就是:

(1)一個程序至少有一個進程,一個進程至少有一個線程;
(2) 線程的劃分尺度小於進程,使得多線程程序的併發性高。
(3)進程在執行過程中擁有獨立的內存單元,而多個線程共享內存,從而極大地提高了程序的運行效率。
(4)線程在執行過程中與進程是有區別的。每個獨立的線程有一個程序運行的入口、順序執行序列和程序的出口。但是線程不能夠獨立執行,必須依存在應用程序中,由應用程序提供多個線程執行控制。
(5)從邏輯角度來看,多線程的意義在於一個應用程序中,有多個執行部分可以同時執行。但操作系統並沒有將多個線程看做多個獨立的應用,來實現進程的調度和管理以及資源分配。

這就是進程和線程的重要區別。

二、線程的生命週期及五種基本狀態

1

Java線程具有五種基本狀態:

(1)新建狀態(New):當線程對象對創建後,即進入了新建狀態,如:Thread t = new MyThread();

(2)就緒狀態(Runnable):當調用線程對象的start()方法(t.start();),線程即進入就緒狀態。處於就緒狀態的線程,只是說明此線程已經做好了準備,隨時等待CPU調度執行,並不是說執行了t.start()此線程立即就會執行;

(3)運行狀態(Running):當CPU開始調度處於就緒狀態的線程時,此時線程才得以真正執行,即進入到運行狀態。注:就 緒狀態是進入到運行狀態的唯一入口,也就是說,線程要想進入運行狀態執行,首先必須處於就緒狀態中;

(4)阻塞狀態(Blocked):處於運行狀態中的線程由於某種原因,暫時放棄對CPU的使用權,停止執行,此時進入阻塞狀態,直到其進入到就緒狀態,才 有機會再次被CPU調用以進入到運行狀態。根據阻塞產生的原因不同,阻塞狀態又可以分爲三種:

①等待阻塞:運行狀態中的線程執行wait()方法,使本線程進入到等待阻塞狀態;

②同步阻塞:線程在獲取synchronized同步鎖失敗(因爲鎖被其它線程所佔用),它會進入同步阻塞狀態;

③其他阻塞 : 通過調用線程的sleep()或join()或發出了I/O請求時,線程會進入到阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態。

(5)死亡狀態(Dead):線程執行完了或者因異常退出了run()方法,該線程結束生命週期。

三、Java多線程的實現

在Java中,如果要實現多線程的程序,那麼必須依靠一個線程的主體類(好比主類的概念一樣,表示一個線程的主類),但是這個線程的主體類在定義的時候需要有一些特殊的要求,這個類可以繼承Thread類或實現Runnable接口來完成定義

1、繼承Thread類實現多線程

java.lang.Thread是一個負責線程操作的類,任何的類繼承了Thread類就可以成爲一個線程的主類。既然是主類,必須有它的使用方法,而線程啓動的主方法需要覆寫Thread類中的run()方法纔可以。

定義一個線程的主體類:

class MyThread extends Thread { // 線程的主體類
    private String title;

    public MyThread(String title) {
        this.title = title;
    }

    @Override
    public void run() { // 線程的主方法
        for (int x = 0; x < 10; x++) {
            System.out.println(this.title + "運行,x = " + x);
        }
    }
}

現在已經有了線程類,並且裏面也存在了相應的操作方法,那麼就應該產生對象並調用裏面的方法,於是編寫出了下的程序:

public class TestDemo {
    public static void main(String[] args) {
        MyThread mt1 = new MyThread("線程A");
        MyThread mt2 = new MyThread("線程B");
        MyThread mt3 = new MyThread("線程C");
        mt1.run();
        mt2.run();
        mt3.run();
    }

運行結果:

線程A運行,x = 0
線程A運行,x = 1
線程A運行,x = 2
線程A運行,x = 3
線程A運行,x = 4
線程A運行,x = 5
線程A運行,x = 6
線程A運行,x = 7
線程A運行,x = 8
線程A運行,x = 9
線程B運行,x = 0
線程B運行,x = 1
線程B運行,x = 2
線程B運行,x = 3
線程B運行,x = 4
線程B運行,x = 5
線程B運行,x = 6
線程B運行,x = 7
線程B運行,x = 8
線程B運行,x = 9
線程C運行,x = 0
線程C運行,x = 1
線程C運行,x = 2
線程C運行,x = 3
線程C運行,x = 4
線程C運行,x = 5
線程C運行,x = 6
線程C運行,x = 7
線程C運行,x = 8
線程C運行,x = 9

我們發現:以上的操作並沒有真正的啓動多線程,因爲多個線程彼此之間的執行一定是交替的方式運行,而此時是順序執行,每一個對象的代碼執行完之後才向下繼續執行。

如果要想在程序之中真正的啓動多線程,必須依靠Thread類的一個方法:public void start(),表示真正啓動多線程,調用此方法後會間接調用run()方法:

public class TestDemo {
    public static void main(String[] args) {
        MyThread mt1 = new MyThread("線程A");
        MyThread mt2 = new MyThread("線程B");
        MyThread mt3 = new MyThread("線程C");
        mt1.start();
        mt2.start();
        mt3.start();
    }

}

運行結果:

線程C運行,x = 0
線程A運行,x = 0
線程B運行,x = 0
線程A運行,x = 1
線程C運行,x = 1
線程A運行,x = 2
線程B運行,x = 1
線程A運行,x = 3
線程A運行,x = 4
線程A運行,x = 5
線程C運行,x = 2
線程C運行,x = 3
線程C運行,x = 4
線程C運行,x = 5
線程C運行,x = 6
線程C運行,x = 7
線程C運行,x = 8
線程C運行,x = 9
線程A運行,x = 6
線程A運行,x = 7
線程A運行,x = 8
線程A運行,x = 9
線程B運行,x = 2
線程B運行,x = 3
線程B運行,x = 4
線程B運行,x = 5
線程B運行,x = 6
線程B運行,x = 7
線程B運行,x = 8
線程B運行,x = 9

此時可以發現:多個線程之間彼此交替執行,但每次的執行結果是不一樣的。通過以上的代碼就可以得出結論:要想啓動線程必須依靠Thread類的start()方法執行,線程啓動之後會默認調用了run()方法。

在調用start()方法之後,發生了一系列複雜的事情:
(1)啓動新的執行線程(具有新的調用棧);
(2)該線程從新狀態轉移到可運行狀態;
(3)當該線程獲得機會執行時,其目標run()方法將運行。
注意:對Java來說,run()方法沒有任何特別之處。像main()方法一樣,它只是新線程知道調用的方法名稱(和簽名)。因此,在Runnable上或者Thread上調用run方法是合法的,但並不啓動新的線程。

說明:爲什麼線程啓動的時候必須調用start()而不是直接調用run()?

我們發現,在調用了start()之後,實際上它執行的還是覆寫後的run()方法,那爲什麼不直接調用run()方法呢?爲了解釋此問題,下面打開Thread類的源代碼,觀察一下start()方法的定義:

 public synchronized void start() {
        if (threadStatus != 0)
            throw new IllegalThreadStateException();
        group.add(this);
        boolean started = false;
        try {
            start0();
            started = true;
        } finally {
            try {
                if (!started) {
                    group.threadStartFailed(this);
                }
            } catch (Throwable ignore) {
            }
        }
    }
    private native void start0();

打開此方法的源代碼首先可以發現:方法會拋出一個“IllegalThreadStateException”異常。一般來講,如果一個方法中使用了throw拋出一個異常對象,那麼這個異常應該使用try…catch捕獲,或者是方法的聲明上使用throws拋出,但是這塊都沒有,爲什麼呢?因爲這個異常類是屬於運行時異常(RuntimeException)的子類:

java.lang.Object
   |- java.lang.Throwable
     |- java.lang.Exception
       |- java.lang.RuntimeException
         |- java.lang.IllegalArgumentException
           |- java.lang.IllegalThreadStateException

當一個線程對象被重複啓動之後會拋出此異常,即:一個線程對象只能啓動一次。

在start()方法之中有一個最爲關鍵的部分就是start0()方法,而且這個方法上使用了一個native關鍵字的定義。

native關鍵字指的是Java本地接口調用(Java Native Interface),即:是使用Java調用本機操作系統的函數功能完成一些特殊的操作,而這樣的代碼開發在Java之中幾乎很少出現,因爲Java的最大特點是可移植性,如果一個程序只能在固定的操作系統上使用,那麼可移植性就將徹底的喪失,所以,此操作一般不用。

多線程的實現一定需要操作系統的支持,那麼以上的start0()方法實際上就和抽象方法很類似沒有方法體,而這個方法體交給JVM去實現,即:在windows下的JVM可能使用A方法實現了start0(),而在linux下的JVM可能使用了B方法實現了start0(),但是在調用的時候並不會去關心具體是何方式實現了start0()方法,只會關心最終的操作結果,交給JVM去匹配了不同的操作系統。

所以在多線程操作之中,使用start()方法啓動多線程的操作是需要進行操作系統函數調用的。

2、實現Runnable接口實現多線程

使用Thread類的確是可以方便的進行多線程的實現,但是這種方式最大的缺點就是單繼承的問題。爲此,在java之中也可以利用Runnable接口來實現多線程。這個接口的定義如下:

public interface Runnable {
    public void run();
}

通過Runnable接口實現多線程:

class MyThread implements Runnable {  // 線程的主體類
    private String title;

    public MyThread(String title) {
        this.title = title;
    }

    @Override
    public void run() { // 線程的主方法
        for (int x = 0; x < 10; x++) {
            System.out.println(this.title + "運行,x = " + x);
        }
    }
}

這和之前繼承Thread類的方式區別不大,但是有一個優點就是避免了單繼承侷限。

不過問題來了。之前說過,如果要啓動多線程,需要依靠Thread類的start()方法完成,之前繼承Thread類的時候可以將此方法直接繼承過來使用,但現在實現的是Runable接口,沒有這個方法可以繼承了,怎麼辦?

要解決這個問題,還是需要依靠Thread類完成。在Thread類中定義了一個構造方法,接收Runnable接口對象:

public Thread(Runnable target);

利用Thread類啓動多線程:

public class TestDemo {
    public static void main(String[] args) throws Exception {
        MyThread mt1 = new MyThread("線程A");
        MyThread mt2 = new MyThread("線程B");
        MyThread mt3 = new MyThread("線程C");
        new Thread(mt1).start();
        new Thread(mt2).start();
        new Thread(mt3).start();
    }
}

運行結果:

線程A運行,x = 0
線程B運行,x = 0
線程B運行,x = 1
線程C運行,x = 0
線程B運行,x = 2
線程A運行,x = 1
線程B運行,x = 3
線程C運行,x = 1
線程C運行,x = 2
線程B運行,x = 4
線程B運行,x = 5
線程A運行,x = 2
線程A運行,x = 3
線程A運行,x = 4
線程A運行,x = 5
線程A運行,x = 6
線程A運行,x = 7
線程A運行,x = 8
線程A運行,x = 9
線程B運行,x = 6
線程B運行,x = 7
線程B運行,x = 8
線程B運行,x = 9
線程C運行,x = 3
線程C運行,x = 4
線程C運行,x = 5
線程C運行,x = 6
線程C運行,x = 7
線程C運行,x = 8
線程C運行,x = 9

此時,不但實現了多線程的啓動,而且沒有了單繼承侷限。

3、實現多線程的第三種方法:.使用Callable接口實現多線程

使用Runnable接口實現的多線程可以避免單繼承侷限,但是有一個問題,Runnable接口裏面的run()方法不能返回操作結果。爲了解決這個問題,提供了一個新的接口:Callable接口(java.util.concurrent.Callable)。

public interface Callable<V>{
    public V call() throws Exception;
}

執行完Callable接口中的call()方法會返回一個結果,這個返回結果的類型由Callable接口上的泛型決定。

實現Callable接口來實現多線程的具體操作是:
創建Callable接口的實現類,並實現clall()方法;然後使用FutureTask類來包裝Callable實現類的對象,且以此FutureTask對象作爲Thread對象的target來創建線程。

定義一個線程主體類:

import java.util.concurrent.Callable;

class MyThread implements Callable<String>{

    private int ticket = 10;
    @Override
    public String call() throws Exception {
        for(int i = 0 ; i < 20 ; i++){
            if(this.ticket > 0){
                System.out.println("賣票,剩餘票數爲"+ this.ticket --);
            }
        }
        return "票已賣光";
    }

}

Thread類沒有直接支持Callable接口。而在JDK1.5之後,提供了一個類:

java.util.concurrent.FutureTask<V>

這個類主要負責Callable接口對象操作。其定義結構如下:

public class FutureTask<V> 
extends Object 
implements RunnableFurture<V>

而RunnableFurture這個接口又有如下定義:

public interface RunnableFurture<V> 
extends Runnable,Future<V>

在FutureTask 類裏面定義有如下構造方法:

public FutureTask(Callable<V> callable)

現在,終於可以通過FutureTask類來接收Callable接口對象了,接收的目的是爲了取得call()方法的返回結果。

從上面分析我們可以發現:
FutureTask類可以接收Callable接口對象,而FutureTask類實現了RunnableFurture接口,RunnableFurture接口又繼承了Runnable接口。

於是,我們可以這樣來啓動多線程:

public class TestDemo {
    public static void main(String[] args) throws Exception {
        MyThread mt1 = new MyThread();
        MyThread mt2 = new MyThread();

        FutureTask<String> task1 = new FutureTask<String>(mt1);//取得call()方法返回結果
        FutureTask<String> task2 = new FutureTask<String>(mt2);//取得call()方法返回結果

        //FutureTask是Runnable接口的子類,可以使用Thread類的構造來接收task對象
        new Thread(task1).start();
        new Thread(task2).start();

        //多線程執行完畢後,可以使用FutureTask的父接口Future中的get()方法取得執行結果
        System.out.println("線程1的返回結果:"+task1.get());
        System.out.println("線程2的返回結果:"+task2.get());

    }
}

運行結果:

賣票,剩餘票數爲10
賣票,剩餘票數爲10
賣票,剩餘票數爲9
賣票,剩餘票數爲8
賣票,剩餘票數爲7
賣票,剩餘票數爲9
賣票,剩餘票數爲6
賣票,剩餘票數爲8
賣票,剩餘票數爲5
賣票,剩餘票數爲7
賣票,剩餘票數爲4
賣票,剩餘票數爲6
賣票,剩餘票數爲3
賣票,剩餘票數爲5
賣票,剩餘票數爲2
賣票,剩餘票數爲4
賣票,剩餘票數爲1
賣票,剩餘票數爲3
賣票,剩餘票數爲2
賣票,剩餘票數爲1
線程1的返回結果:票已賣光
線程2的返回結果:票已賣光

小結:

上述講解了三種實現多線程的方式,對於線程的啓動而言,都是調用線程對象的start()方法,需要特別注意的是:不能對同一線程對象兩次調用start()方法。

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