Introduction to Multithread

講到多執行緒時,我們常常在講concurrent(並行)與parallel(平行),在單核的情況下,只會出現concurrent,因為concurrent表示的是一段時間只會有一個執行緒在動作;但是在雙核以上的情況下,則會出現parallel的情形,表示同一段時間可能會有多個執行緒在動作。

不管是concurrent或者是parallel,都要考慮到thread-safe的問題。

一般來說,在java中可以透過兩種方式建立thread。

  • 繼承Thread類別,建立PrintThread.java

    public class PrintThread extends Thread {
        private String message;
        public PrintThread(String message) {
            this.message = message;
        }
        public void run() {
            for (int i = 0; i < 10000; i++) {
                System.out.print(message);
            }
        }
    }
    

    Main.java

    public class Main {
        public static void main(String[] args) {
            new PrintThread("Good!").start();
            new PrintThread("Nice!").start();
        }
    }
    
  • 實作Runnable界面,建立Printer.java

    public class Printer implements Runnable {
        private String message;
        public Printer(String message) {
            this.message = message;
        }
        public void run() {
            for (int i = 0; i < 10000; i++) {
                System.out.print(message);
            }
        }
    }
    

    Main.java

    public class Main {
        public static void main(String[] args) {
            new Thread(new Printer("Good!")).start();
            new Thread(new Printer("Nice!")).start();
        }
    }
    

接下來要來介紹synchronized方法。

建立Bank.java

public class Bank {
    private int money;
    private String name;
    public Bank(String name, int money) {
        this.name = name;
        this.money = money;
    }
    public synchronized void deposit(int m) {
        money += m;
    }
    public synchronized boolean withdraw(int m) {
        if (money >= m) {
            money -= m;
            check();
            return true;
        } else {
            return false;
        }
    }
    public String getName() {
        return name;
    }
    private void check() {
        if (money < 0) {
            System.out.println("可用餘額為負數 ! money = " + money);
        }
    }
}

ClientThread.java

public class ClientThread extends Thread {
    private Bank bank;
    public ClientThread(Bank bank) {
        this.bank = bank;
    }
    public void run() {
        while (true) {
            boolean ok = bank.withdraw(1000);
            if (ok) {
                bank.deposit(1000);
            }
        }
    }
}

Main.java

public class Main {
    public static void main(String[] args) {
        Bank bank = new Bank("A Good Bank", 1000);
        new ClientThread(bank).start();
        new ClientThread(bank).start();
    }
}

當一個執行緒在執行Bank實體的deposit方法時,表示它已經取得鎖定,其它執行緒就不能執行同一個實體的deposit方法或withdraw方法,必須排隊取得鎖定。 換句話說,一個實體只會有一個鎖定。而且,只有取得鎖定的執行緒才能夠執行synchronized方法。

如果不想要讓整個方法都被synchronized保護,則可以用部份保護的方式。

void method(){
    //something
    synchronized(variable){

    }
}

假設現在有一個加上synchronized的方法

synchronized void method(){
    //something
}

它等價於

void method(){
    synchronized(this){

    }
}

相當於取得this的鎖定。

另一方面,synchronized類別方法又有不同

class something{
    static synchronized void method(){
        //something
    }
}

其等價於

class something{
    start void method(){
        synchronized(something.class){
            //something
        }
    }
}

注意到了嗎?取得的是something這個類別的鎖定。

最後介紹wait set的觀念

每個實體都會有一個wait set,wait set是一個在執行該實體的wait方法時,動作所停止的執行緒的集合。 一執行wait方法時,該執行緒便會暫時停止動作,進入wait set這個集合,除非發生下列其中一種情形,否則該thread會永遠被留在wait set裡。

  • 有其他執行緒呼叫notify方法,可以從wait set中叫醒一個執行緒出來,不能保證是哪一個。
  • 有其他執行緒呼叫notifyAll方法,叫醒所有正在wait set中的執行緒。
  • 有其他執行緒呼叫Interrupt方法,Interrupt是針對執行緒實體呼叫,並且會丟出InterruptException。
  • wait方法已到期

要執行wait或notify方法,執行緒必須先取得鎖定,進入wait set後,才會釋放鎖定。

一般來說,會設計成這樣。

synchronized void method1(){
    //something
    wait(); //或是this.wait()
    //something
}
synchronized void method2(){
    //something
    notify(); //或是this.notify()
    //something
}

被叫醒後的執行緒,要繼續排隊取得鎖定,之後會執行wait()後的敘述。