撥開雲霧見天日 —— Java多執行緒程式設計概念剖析

五天幾年發表於2017-03-08

說到Java多執行緒程式設計,大多數人都會想到繼承Thread或實現Runnable程式設計,new 一個Thread例項,呼叫start()方法,由OS呼叫即可。具體過程如下:

public class MyThread extends Thread {

    @Override
    public void run() {
        System.out.println("MyThread");
    }

    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
        System.out.println("執行結束!");
    }

}

繼承Thread;

public class MyRunnable implements Runnable {

    @Override
    public void run() {
        System.out.println("MyRunnable");
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new MyRunnable());
        thread.start();
        System.out.println("執行結束!");
    }

}

實現Runnable;

這裡,有一個問題,就是無論繼承Thread還是實現Runnable,最後都需要有一個Thread例項,來啟用執行緒。那麼,這個例項化的Thread例項是什麼呢?僅僅代表一個執行緒。如果是這樣,那麼實現了Runnable的物件有代表什麼,它與Thread有何不同?讓我們來仔細看看Java執行緒。

Java執行緒

執行緒,就是程式中獨立執行的子任務。通常理解為輕量級程式,多個執行緒共享一個程式資源。由於執行緒是由OS分配與排程,因此容易產生資源競爭問題,所以成為整個多執行緒開發中的難點。

從對執行緒概念的理解上,可以看到,真正執行緒應該有兩個概念,(執行緒)任務和(執行緒)排程。

執行緒任務

再來看看執行緒程式碼。無論繼承Thread還是實現Runnable,都需要重寫一個run()方法,這裡面的程式碼,是執行緒執行時真正要執行的方法,也就是說,這是執行緒任務。沒任務,執行緒幹嘛?自己玩!!!肯定不可以。

從Runnable介面來看,它只有一個宣告——run,也就是說,你要為它的實現類指派任務。之所以有Runnable介面,一方面是因為Java是單繼承,不可能為了執行緒繼承了Thread還去繼承其他類,所以必須要有Runnable。第二,從物件導向設計原則——單一職責來看,一個類只負責某一方面的任務就可以了,避免引入多變數,多變數代表多不確定性,失控的機會會加大。所以需要Runnable的存在,只負責指派執行緒任務。

Thread也是存在run抽象方法的,它本身就是代表執行緒(具體哪個暫時不說)。由它例項自己然後自己啟用也是可以的,要不然,只有一個啟用而沒有任務,怎麼也說不過去。其實Thread也是實現Runnable,自己的任務還參考別人的……

    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start();
        System.out.println("執行結束!");
    }

這個MyThread繼承自Thread類,並(暗藏)重寫了run()方法。沒任務玩什麼!!!

執行緒排程

有了任務,就要有人執行,就需要有排程。是誰?就是開篇問到的那個問題。兩種方式的執行緒都需要有一個Thread例項,這就是執行緒排程。

對於繼承式的執行緒,我們好理解,自己啟用自己的任務,那麼對於實現式的執行緒呢,是怎麼啟用的?

在Thread中,有一個Thread(Runnable target)構造方法,也就是說在Thread裡有一個Runnable變數。對此,Thread裡是這樣註釋的:“ What will be run.”。再看看Thread的排程程式碼:

@Override
public void run() {
     if (target != null) {
         target.run();
    }
}

target就是Runnable例項,也就是先看看有沒有單獨的任務表(可以把Runnable看做是一個任務表),如果沒有,再檢視自己的任務。

那麼,現在就很明顯了,Thread類其實更重的職責在於執行緒排程(雖然也可以指派任務),Runnable是用來指派執行緒任務。在多執行緒程式設計中,有關競爭、安全、同步這種資源處理的問題,都發生線上程任務身上。看看同步關鍵字synchronized和Thread一點關係都沒有。如果不指定鎖,那麼預設的鎖物件就是任務例項。有關狀態、優先、通訊等,真正涉及到執行緒本身的執行,這些才是執行緒排程處理的問題。

執行緒與共享

為了進一步說明執行緒任務與執行緒排程,這裡再給出一個常見的執行緒變數共享例子:

public class MyThread extends Thread {
    private int count = 5;

    public MyThread(){}

    /** 設定執行緒名稱 */
    public MyThread(String name){
        this.setName(name);
    }

    @Override
    public void run() {
        while(count>0){
            count -- ;
            System.out.println("由 " + this.currentThread().getName() + " 計算,count:= " +count);
        }
    }

    public static void main(String[] args) {
        MyThread thread = new MyThread();
        Thread a = new Thread(thread, "A");
        Thread b = new Thread(thread, "B");
        Thread c = new Thread(thread, "C");
        a.start();
        b.start();
        c.start();
    }

}

執行結果如下(不同時刻執行結果不一樣):

enter image description here

這裡沒考慮安全問題。 可以看到,這裡三個執行緒排程器共同執行一個任務,才能實現執行緒變數共享:執行緒排程器A操作改變count值,然後執行緒排程器B接著操作改變count……

以上就是對Java執行緒程式設計概念的初步理解……

相關文章