天天看點

Java多線程程式設計——線程建立

Java中的線程有三種建立方式:繼承Thread類、實作Runnable接口和實作Callable接口。下面我們一一進行介紹。

一、繼承Thread類

使用該方法建立線程有如下三步:

1. 自定義線程類繼承Thread類;

2. 重寫該類中的run()方法,編寫線程執行體;

3. 建立線程對象,調用start()方法啟動線程。

例如,我們建立3個線程,分别去尋找指定區間内的素數。

public class Main {
    public static void main(String[] args) {
        EnumeratePrimeNumber task1 = new EnumeratePrimeNumber(10000, 11000);
        EnumeratePrimeNumber task2 = new EnumeratePrimeNumber(20000, 21000);
        EnumeratePrimeNumber task3 = new EnumeratePrimeNumber(30000, 31000);

        task1.start();
        task2.start();
        task3.start();
    }
}

class EnumeratePrimeNumber extends Thread {
    private int start;
    private int end;

    public EnumeratePrimeNumber() {}

    public EnumeratePrimeNumber(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    public void run() {
        for (int i = start; i < end; ++i) {
            boolean isPrime = true;
            for (int j = 2; j * j <= i; ++j) {
                if (i % j == 0) {
                    isPrime = false;
                    break;
                }
            }
            if (isPrime)
                System.out.println(i);
        }
    }
}
           

該程式的核心點有:

  • 語句
    class EnumeratePrimeNumber extends Thread      
    定義了任務類EnumeratePrimeNumber,它繼承了Thread類。
  • @Override
    public void run() {...}      

        重寫了Thread類裡的run()方法,進而可以尋找素數。

  • EnumeratePrimeNumber task1 = new EnumeratePrimeNumber(10000, 11000);
    EnumeratePrimeNumber task2 = new EnumeratePrimeNumber(20000, 21000);
    EnumeratePrimeNumber task3 = new EnumeratePrimeNumber(30000, 31000);      

        分别建立了3個線程對象,分别尋找10000~11000、20000~21000和30000~31000間的素數。

  • task1.start();
    task2.start();
    task3.start();      

        調用start()方法啟動這3個線程。

程式的運作結果如下所示:

10007
10009
30011
20011
30013
10037
10039
10061
30029
20021
30047
10067
10069
......           

可以看到,程式的執行結果并非是按照10000~11000、20000~21000和30000~31000三個區間依次輸出的,而是在這三個區間之間來回切換,但是每個區間内找到的素數依然還是有序的。

二、實作Runnable接口

1. 自定義線程類實作Runnable接口;

3. 建立自定義線程類的對象,接着建立Thread線程類對象,并将自定義線程類的對象作為Thread線程類構造方法的傳入參數,最後調用Thread線程類對象的start()方法啟動線程。

具體的步驟其實和第一種線程建立方式類似,隻是第一步不再是繼承而是實作,第三步不是直接調用自定義線程類對象的start方法,而是通過Thread線程類對象進行代理。

依然以上面尋找素數的問題為例,使用Runnable接口的實作程式如下:

public class Main {
    public static void main(String[] args) {
        EnumeratePrimeNumber task1 = new EnumeratePrimeNumber(10000, 11000);
        EnumeratePrimeNumber task2 = new EnumeratePrimeNumber(20000, 21000);
        EnumeratePrimeNumber task3 = new EnumeratePrimeNumber(30000, 31000);

        // can also be
        //      Thread t1 = new Thread(task1);
        //      t1.start();
        new Thread(task1).start();
        new Thread(task2).start();
        new Thread(task3).start();
    }
}

class EnumeratePrimeNumber implements Runnable {
    private int start;
    private int end;

    public EnumeratePrimeNumber() {}

    public EnumeratePrimeNumber(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    public void run() {
        for (int i = start; i < end; ++i) {
            boolean isPrime = true;
            for (int j = 2; j * j <= i; ++j) {
                if (i % j == 0) {
                    isPrime = false;
                    break;
                }
            }
            if (isPrime)
                System.out.println(i);
        }
    }
}
           

上面代碼相比于之前的,最大的差別有:

  • 自定義任務類時不再繼承Thread類,而是實作Runnable接口:
    class EnumeratePrimeNumber implements Runnable      
  • 在啟動線程時,需要先建立任務類的執行個體,并在建立Thread對象時作為參數傳遞,再調用start()方法啟動:
    new Thread(task1).start();      

由于Java隻支援單繼承,是以在實踐中更推薦使用實作Runnable接口建立線程。除此之外,這樣的方式還支援同一對象被多個線程所使用。例如,我們可以編寫程式模拟“搶票”——由多個線程去使用同一資源。

public class Main {
    public static void main(String[] args) {
        SnapUp race = new SnapUp();

        new Thread(race, "Alice").start();
        new Thread(race, "Bob").start();
        new Thread(race, "Cathy").start();
    }
}

class SnapUp implements Runnable {
    private static int numberOfTicket = 10;

    @Override
    public void run() {
        while (true) {
            if (numberOfTicket > 0) {
                System.out.println(Thread.currentThread().getName() + " gets " + (11 - numberOfTicket) + " ticket");
                numberOfTicket--;
            } else {
                break;
            }
        }
    }
}
           

上述程式安排了3個線程去模拟3個人,搶購10張票,并輸出搶購結果。運作結果如下:

Alice gets 1 ticket
Alice gets 2 ticket
Alice gets 3 ticket
Bob gets 1 ticket
Alice gets 4 ticket
Alice gets 6 ticket
Alice gets 7 ticket
Bob gets 5 ticket
Bob gets 9 ticket
Bob gets 10 ticket
Cathy gets 8 ticket
Alice gets 8 ticket           

非常驚訝的是,第1張票被Alice和Bob兩個人都搶到了,第8張票被Cathy和Alice兩個人都搶到了,這顯然是不正确的。專業上将其稱之為多線程通路導緻的資料不一緻。

最後總結一下使用實作Runnable接口建立線程的好處:

1. 避免了Thread類單繼承的局限,更加靈活友善;

2. 友善同一個對象被多個線程使用。

三、實作Callable接口

有時候我們希望傳回每個線程的執行結果,或者希望線程可以抛出異常,那麼我們可以使用實作Callable接口建立線程。

實作Callable接口來建立線程的步驟如下:

1. 自定義線程類實作Callable接口;

2. 重寫該類中的call()方法,編寫線程執行體,給出傳回值并抛出異常;

3. 建立自定義線程類的對象,比如

    Task t = new Task();

4. 建立執行服務,比如

    ExecuteService es = Executors.newFixedThreadPool(3); // 這裡建立了大小為3的定長線程池

5. 送出執行,比如

    Future<Integer> futureResult = es.submit(t); // 這裡的傳回值為Integer類

6. 擷取結果,比如

    Integer result = futureResult.get();

7. 關閉服務

    es.shutdownNow();

import java.util.concurrent.*;

public class Main {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        Summation task1 = new Summation(1, 10000);
        Summation task2 = new Summation(10000, 20000);
        Summation task3 = new Summation(20000, 30000);

        ExecutorService es = Executors.newFixedThreadPool(3);

        Future<Double> futureSum1 = es.submit(task1);
        Future<Double> futureSum2 = es.submit(task2);
        Future<Double> futureSum3 = es.submit(task3);

        double sum1 = futureSum1.get();
        double sum2 = futureSum2.get();
        double sum3 = futureSum3.get();

        System.out.println("Sum = " + (sum1 + sum2 + sum3));

        es.shutdown();
    }
}

class Summation implements Callable<Double> {
    private int start;
    private int end;

    public Summation() {}

    public Summation(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    public Double call() throws ArithmeticException {
        double sum = 0.0;
        for (int i = start; i < end; ++i) {
            if (i == 0)
                throw new ArithmeticException();
            sum += 1.0 / i;
        }
        System.out.println(Thread.currentThread().getName() + ": " + sum);
        return sum;
    }
}
           
pool-1-thread-2: 0.6931721811849485
pool-1-thread-3: 0.40547344155723775
pool-1-thread-1: 9.787506036044348
Sum = 10.886151658786535