Try   HackMD

平行與並行程式設計

tags: java parallelism concurrency

目錄

簡介

程序(process)就是電腦中的一個正在執行的程式,其記憶體空間都是獨立的,彼此不會互相影響,也無法互相存取,一個作業系統內,通常會有多個程序同時在進行;而執行緒(thread)則是程序內的一個流程,Java程序內由main()開始,則是一個執行緒的開始,一直執行到程式碼的最後一行,執行緒結束後,就代表該程序也結束了;一個程序內至少會有一個執行緒,而且透過Thread物件,可以產生多個執行緒,進行不同的程式流程。

Thread(執行緒)可以用來開發,concurrency(並行)以及parallelism(平行)的程式;執行緒讓一個程序(process)可以「同時」進行多個流程,並共享同一個記憶體空間。

通常有以下幾種情況會使用到執行緒:

  1. 資料的I/O(Input and Output)相關的工作。例如:檔案或是資料庫等等,如果同時需要讀很多個檔案,或是同時要處理很多的資料庫資料,用執行緒的方式來做可以不用影響到使用者在界面上的操作或是資料計算。
  2. 執行需要計算很久的工作,當這種工作一多,透過執行緒,可以把它們分配給不同的CPU核心去做運算,就可以不浪費CPU多核心的威力。
  3. 週期性工作,例如每秒鐘後執行一次工作或是在特定時間去執行某個工作,這些工作勢必平常都在等待,等時間到時才會執行,而等待時,主程式要可以繼續運作或是接受使用者的輸入,不能因為等待而造成程式無法回應。
  4. 背景服務程式,平時沒事就在背景等待,等某個event發生後,才會開始執行對應的工作,例如網路伺服器,當沒有連線時,會持續監聽是否有連線進來,才能及時地提供服務。

建立執行緒

建立執行緒有幾種方式:

  1. 繼承Thread類別,並覆寫run()方法,建立物件後呼叫start()來啟動執行緒。
  2. 實現Runnable介面,建立Thread物件時傳遞該Runnable物件給建構式,之後呼叫start()方法啟動執行緒。
  3. 使用Java 8的Lambda語法,直接建立Thread物件,一樣是呼叫start()方法啟動執行緒。

Thread

原始版本

public class App {
    public static void main(String[] args) {
        for(int i = 0 ; i < 10 ; i++) {
            try {
                Thread.sleep(1000);
                System.out.println("完成第" + i + "個工作.");
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

使用Thread版本

class MyThread extends Thread {
    private int i;

    public MyThread(int i) {
        this.i = i;
    }
    public void run() {
        try {
            Thread.sleep(1000);
            System.out.println("完成第" + i + "個工作.");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class App {
    public static void main(String[] args) {
        for(int i = 0 ; i < 10 ; i++) {
            new MyThread(i).start();
        }
    }
}

Runnable

使用Runnable版本

class MyRunnable implements Runnable {
    private int i;

    public MyRunnable(int i) {
        this.i = i;
    }

    @Override
    public void run() {
        try {
            Thread.sleep(1000);
            System.out.println("完成第" + i + "個工作.");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
}

public class App {
    public static void main(String[] args) {
        for(int i = 0 ; i < 10 ; i++) {
            new Thread(new MyRunnable(i)).start();
        }
    }
}

Lambda

public class App {
    public static void main(String[] args) {
        for(int i = 0 ; i < 10 ; i++) {
            final int fi = i;

            new Thread(() -> {
                try {
                    Thread.sleep(1000);
                    System.out.println("完成第" + fi + "個工作.");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
    }
}

同步

執行緒開始執行後,因為是同時進行,每個執行緒會拿到多少CPU時間是不可控的,所以完成的順序每次執行程式可能都會不一樣,而且當執行緒會操作同一個變數的時候,有可能因為同時修改,造成資料遺失,因此需要有一些機制來避免照些情況發生。

有一程式如下要做0~9數字相加:

class MyThread {
    private int i;

    public MyThread(int i) {this.i = i;}
    
    public void run() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            
            e.printStackTrace();
        }

        App.sum += i; 
    }
}

public class App {
    public static int sum = 0;
    
    public static void main(String[] args) throws InterruptedException { 
        ArrayList<Thread> ts = new ArrayList<>();
        for(int i = 0 ; i < 10 ; i++) {
            new MyThread(i).run();
        }
        
        System.out.println("總和:" + sum);
    }
}

會輸出:

總和:45

注意

該範例是直接呼叫run()方法來執行相加的動作,所以只是方法名稱跟執行緒的run()依樣,但並非啟動執行緒,因為該類別也沒有繼承自Thread類別。

為了提升效能,改成多執行緒版本:

class MyThread extends Thread {
    private int i;

    public MyThread(int i) {this.i = i;}
    
    public void run() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            
            e.printStackTrace();
        }

        App.sum += i; 
    }
}

public class App {
    public static int sum = 0;
    
    public static void main(String[] args) throws InterruptedException { 
        ArrayList<Thread> ts = new ArrayList<>();
        for(int i = 0 ; i < 10 ; i++) {
            new MyThread(i).start();
        }
        
        System.out.println("總和:" + sum);
    }
}

該範例加上了:

  1. MyThread繼承自Thread類別。
  2. 在main()方法內是呼叫start()來啟動執行緒。

執行效能變快了,但會發現輸出都是:

總和:0

這是因為啟動執行序後,主執行緒就會繼續往下執行,因為子執行緒都還沒執行完畢主執行緒就結束了,因次不會有計算結果發生。

因此在子執行緒啟動後,主執行緒需要暫停,等子執行緒執行完畢再繼續往下進行。

Join

Thread的join()方法,可以讓主執行緒停在該行,等執行緒結束才繼續往下執行。

class MyThread extends Thread {
    private int i;

    public MyThread(int i) {this.i = i;}
    
    public void run() {
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        App.sum += i;
    }
}

public class App {
    public static int sum = 0;
    
    public static void main(String[] args) throws InterruptedException { 
        ArrayList<Thread> ts = new ArrayList<>();
        for(int i = 0 ; i < 10 ; i++) {
            Thread t = new MyThread(i);
            t.start();
            ts.add(t);
        }

        for(Thread t : ts) {
            t.join();
        }
        
        System.out.println("總和:" + sum);
    }
}

加了join()後,可以看到計算後的結果了。

注意

不要在呼叫start()啟動執行緒後就馬上做join(),這樣的話,就相當於執行緒執行結束後才會再啟動下一個新的執行緒,如此就沒有多執行緒同時執行的效果了,也就是效果會跟單執行緒一樣。

但有另外一個問題,每次計算的結果都不一樣,這是因為App.sum += i;在底層其實是分成三個動作,取值、相加、存值,因為執行緒都會操作到同一個變數,在進行到哪一個動作都可能被中斷,因為有可能出現多個執行緒都取到同一個值然後計算完畢後存值互相去覆蓋了,造成計算結果出現問題,這種情況又稱為Race condition(競爭條件)。

因此我們需要對會被操作的App.sum變數做鎖定的動作,不能同時被多個執行緒存取。

synchronized

使用synchronized關鍵字鎖住物件:

class MyThread extends Thread {
    private int i;

    public MyThread(int i) {this.i = i;}
    
    public void run() {
        
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            
            e.printStackTrace();
        }

        synchronized(App.class) {
            App.sum += i; 
        }
    }
}

public class App {
    public static int sum = 0;
    
    public static void main(String[] args) throws InterruptedException { 
        ArrayList<Thread> ts = new ArrayList<>();
        for(int i = 0 ; i < 10 ; i++) {
            Thread t = new MyThread(i);
            t.start();
            ts.add(t);
        }

        for(Thread t : ts) {
            t.join();
        }
        
        System.out.println("總和:" + sum);
    }
}

將輸出正確結果:

總和:45

2021-09-04

import java.util.ArrayList;

class Summarize implements Runnable {
    private int i;
    public Summarize(int i) { this.i = i; }

    @Override
    public void run() {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) { }
        App.sum += i;
    }
}
public class App {
    public static int sum = 0;
    public static void main(String[] argc) throws InterruptedException {
        ArrayList<Thread> threads = new ArrayList<>();
        
        for(int i = 0 ; i < 10 ; i++) {
            Summarize summarize = new Summarize(i);
            Thread t = new Thread(summarize);
            t.start(); // 啟動執行緒
            threads.add(t); // 將執行緒物件存下來
        }

        for(Thread t : threads) { // 將全部執行緒join到主執行緒
            t.join();
        }

        System.out.println("sum=" + sum);
    }
}