Java進階05 多執行緒

ii_chengzi發表於2019-11-21

多執行緒( multiple thread)是計算機實現多工並行處理的一種方式。

在單執行緒情況下,計算機中存在一個控制權,並按照順序依次執行指令。單執行緒好像是一個只有一個隊長指揮的小隊,整個小隊同一個時間只能執行一個任務。

單執行緒

 

在多執行緒情境下,計算機中有多個控制權。多個控制權可以同時進行,每個控制權依次執行一系列的指令。多執行緒好像是一個小隊中的成員同時執行不同的任務。

可參考 Linux多執行緒與同步,並對比 Python多執行緒與同步

多執行緒

傳統意義上,多執行緒是由作業系統提供的功能。對於單核的CPU,硬體中只存在一個執行緒。在作業系統的控制下,CPU會在不同的任務間(執行緒間)切換,從而造成多工齊頭並進的效果。這是單CPU 分時複用機制下的多執行緒。現在,隨著新的硬體技術的發展,硬體本身開始提供多執行緒支援,比如多核和超執行緒技術。然而,硬體的多執行緒還是要接受作業系統的統一管理。在作業系統之上的多執行緒程式依然通用。

多個執行緒可以並存於同一個程式空間。在JVM的一個程式空間中,一個 棧(stack)代表了方法呼叫的次序。對於多執行緒來說,程式空間中需要有 多個棧,以記錄不同執行緒的呼叫次序。多個棧互不影響,但所有的執行緒將 共享堆(heap)中的物件

 

建立執行緒

Java中“一切皆物件”,執行緒也被封裝成一個物件。我們可以透過 繼承Thread類來建立執行緒。執行緒類中的的 run()方法包含了該執行緒應該執行的指令。我們在衍生類中覆蓋該方法,以便向執行緒說明要做的任務:

public class Test
{    public static void main(String[] args)
    {
        NewThread thread1 = new NewThread();
        NewThread thread2 = new NewThread();
        thread1.start(); // start thread1
        thread2.start(); // start thread2    }
}/**
 * create new thread by inheriting Thread */class NewThread extends Thread {    private static int threadID = 0; // shared by all
    /**
     * constructor     */
    public NewThread() {        super("ID:" + (++threadID));
    }    /**
     * convert object to string     */
    public String toString() {        return super.getName();
    }    /**
     * what does the thread do?     */
    public void run() {
        System.out.println(this);
    }
}

(++是Java中的累加運算子,即讓變數加1。這裡++出現在threadID之前,說明先將threadID加1,再對周邊的表示式求值

toString是Object根類的方法,我們透過覆蓋該方法,來將物件轉換成字串。當我們列印該物件時,Java將自動呼叫該方法。)

 

可以看到,Thread基類的 構建方法(super())可以接收一個字串作為引數。該字串是該執行緒的名字,並使用 getName()返回。

定義類之後,我們在main()方法中建立執行緒物件。每個執行緒物件為一個執行緒。建立執行緒物件後,執行緒還沒有開始執行。

我們呼叫執行緒物件的 start()方法來啟動執行緒。start()方法可以在構造方法中呼叫。這樣,我們一旦使用 new建立執行緒物件,就立即執行。

 

Thread類還提供了下面常用方法:

join(Thread tr)   等待執行緒tr完成

setDaemon()       設定當前執行緒為後臺daemon (程式結束不受daemon執行緒的影響)

Thread類官方文件: 

 

Runnable

實現多執行緒的 另一個方式是實施 Runnable介面,並提供run()方法。實施介面的好處是容易實現多重繼承(multiple inheritance)。然而,由於內部類語法,繼承Thread建立執行緒可以實現類似的功能。我們在下面給出一個簡單的例子,而不深入:

 

public class Test
{    public static void main(String[] args)
    {
        Thread thread1 = new Thread(new NewThread(), "first");
        Thread thread2 = new Thread(new NewThread(), "second");
        thread1.start(); // start thread1
        thread2.start(); // start thread2    }
}/**
 * create new thread by implementing Runnable */class NewThread implements Runnable {    /**
     * convert object to string     */
    public String toString() {        return Thread.currentThread().getName();
    }    /**
     * what does the thread do?     */
    public void run() {
        System.out.println(this);
    }
}

 

synchronized

多工程式設計的難點在於多工共享資源。對於同一個程式空間中的多個執行緒來說,它們都共享堆中的物件。某個執行緒對物件的操作,將影響到其它的執行緒。

在多執行緒程式設計中,要盡力避免 競爭條件(racing condition),即執行結果依賴於不同執行緒執行的先後。執行緒是併發執行的,無法確定執行緒的先後,所以我們的程式中不應該出現競爭條件。

然而,當多工共享資源時,就很容易造成競爭條件。我們需要將共享資源,並造成競爭條件的多個執行緒線性化執行,即同一時間只允許一個執行緒執行。

(可更多參考 Linux多執行緒與同步)

 

下面是一個售票程式。3個售票亭(Booth)共同售賣100張票(Reservoir)。每個售票亭要先判斷是否有餘票,然後再賣出一張票。如果只剩下一張票,在一個售票亭的判斷和售出兩個動作之間,另一個售票亭賣出該票,那麼第一個售票亭(由於已經執行過判斷)依然會齒形賣出,造成票的超賣。為了解決該問題,判斷和售出兩個動作之間不能有“空隙”。也就是說,在一個執行緒完成了這兩個動作之後,才能有另一個執行緒執行。

在Java中,我們將 共享的資源置於一個物件中,比如下面r(Reservoir)物件。它包含了總共的票數;將可能造成競爭條件的,針對共享資源的操作,放在 synchronized (同步)方法中,比如下面的sellTicket()。synchronized是方法的修飾符。在Java中, 同一物件的synchronized方法只能同時被一個執行緒呼叫。其他執行緒必須等待該執行緒呼叫結束,(餘下的執行緒之一)才能執行。這樣,我們就排除了競爭條件的可能。

在main()方法中,我們將共享的資源(r物件)傳遞給多個執行緒:


public 
class
 Test
{    
public 
static 
void
 main(String[] args)
    {
        Reservoir r = 
new Reservoir(100
);
        Booth b1 = 
new
 Booth(r);
        Booth b2 = 
new
 Booth(r);
        Booth b3 = 
new
 Booth(r);
    }
}
/**

* contain shared resource
*/
class Reservoir {     private int total;     public Reservoir( int t)    {         this.total = t;    }     /**     * Thread safe method     * serialized access to Booth.total     */     public synchronized boolean sellTicket()    {         if( this.total > 0 ) {             this.total = this.total - 1 ;             return true; // successfully sell one        }         else {             return false; // no more tickets        }    } } /** * create new thread by inheriting Thread */ class Booth extends Thread {     private static int threadID = 0; // owned by Class object     private Reservoir release;       // sell this reservoir     private int count = 0;           // owned by this thread object     /**     * constructor     */     public Booth(Reservoir r) {         super("ID:" + (++ threadID));         this.release = r;           // all threads share the same reservoir         this .start();    }     /**     * convert object to string     */     public String toString() {         return super .getName();    }     /**     * what does the thread do?     */     public void run() {         while( true ) {             if( this .release.sellTicket()) {                 this.count = this.count + 1 ;                System.out.println( this.getName() + ": sell 1" );                 try {                    sleep(( int) Math.random()*100);   // random intervals                }                 catch (InterruptedException e) {                     throw new RuntimeException(e);                }            }             else {                 break ;            }        }        System.out.println( this.getName() + " I sold:" + count);    } }

( Math.random()用於產生隨機數)

 

Java的每個物件都 自動包含有一個用於支援同步的計數器,記錄synchronized方法的呼叫次數。執行緒獲得該計數器,計數器加1,並執行synchronized方法。如果方法內部進一步呼叫了該物件的其他synchronized方法,計數器加1。當synchronized方法呼叫結束並退出時,計數器減1。其他執行緒如果也呼叫了同一物件的synchronized方法,必須等待該計數器變為0,才能鎖定該計數器,開始執行。Java中的類同樣也是物件( Class類物件)。Class類物件也包含有計數器,用於同步。

 

關鍵程式碼

上面,我們利用synchronized修飾符同步了整個方法。我們可以同步部分程式碼,而不是整個方法。這樣的程式碼被稱為 關鍵程式碼(critical section)。我們使用下面的語法:

synchronized (syncObj) {
  ...;
}

花括號中包含的是 想要同步的程式碼,syncObj是任意物件。我們將使用syncObj物件中的計數器,來同步花括號中的程式碼。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/31543790/viewspace-2665088/,如需轉載,請註明出處,否則將追究法律責任。

相關文章