最終更新日:2021/01/30 原本2017-

マルチスレッドプログラミング

C++と同様にJavaでもマルチスレッドが扱えます. スレッド(Thread)とは「糸」の意味ですが,プログラミングの世界では1つの作業の事を指す場合が多いです. マルチスレッドとは複数の作業を同時並行で行う事を指し,そのためのプログラミング作法をマルチスレッドプログラミングと呼びます. Javaでマルチスレッドを扱う方法として の2つがあります. まず,Threadクラスを継承することでスレッドプログラミングを実現してみましょう.
下記のThreadTestExt.javaをコピーし,動作を確認してみましょう.

ThreadTestExt.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package jp.ac.utsunomiya_u.is;
 
public class ThreadTestExt extends Thread {
 
    /**
     * スレッド名
     */
    private String name = null;
 
    /**
     * コンストラクタ
     *
     * @param name スレッド名
     */
    public ThreadTestExt(String name) {
        this.name = name;
    }
 
    /**
     * メソッドrunのオーバーライド
     */
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(name + " " + i);
            try {
                sleep(1000);
            } catch (InterruptedException ex) {
                System.out.println(ex.getMessage());
            }
        }
    }
 
    public static void main(String[] args) {
        ThreadTestExt threadTestExt1 = new ThreadTestExt("thread 1");
        threadTestExt1.start();
    }
}
3行目
Threadクラスを継承しています.
23行目
Threadのサブクラスはrunメソッドをオーバーライドしなければなりません.
28行目
sleepメソッド(Threadクラスのstaticメソッド)で1000[ms]の間停止しています.
35行目
ThreadTestExtクラスのインスタンスを生成しています.
36行目
startメソッドでスレッドが開始されます.

出力

thread 1 0
thread 1 1
thread 1 2
thread 1 3
thread 1 4
thread 1 5
thread 1 6
thread 1 7
thread 1 8
thread 1 9
mainメソッドにもう1つスレッドを追加し,動作を確認してみましょう.

ThreadTestExt.javaの修正部分

1
2
3
4
5
6
public static void main(String[] args) {
    ThreadTestExt threadTestExt1 = new ThreadTestExt("thread 1");
    ThreadTestExt threadTestExt2 = new ThreadTestExt("thread 2");
    threadTestExt1.start();
    threadTestExt2.start();
}

出力例

thread 1 0
thread 2 0
thread 2 1
thread 1 1
thread 1 2
thread 2 2
thread 2 3
thread 1 3
thread 2 4
thread 1 4
thread 1 5
thread 2 5
thread 1 6
thread 2 6
thread 1 7
thread 2 7
thread 1 8
thread 2 8
thread 2 9
thread 1 9
この出力結果は実行の度に変わる可能性があります. threadTestExt1とthreadTestExt2の2つのインスタンスが同時に起動していますが,システムのスケジュール機能がどちらかのスレッドを選んで動作させているので,実行毎に結果が異なる可能性があることに注意しましょう.
次に,Runnableインタフェースを実装してマルチスレッドプログラミングを実現してみましょう.
mainメソッドにもう1つスレッドを追加し,動作を確認してみましょう.

ThreadTestImp.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package jp.ac.utsunomiya_u.is;
 
public class ThreadTestImp implements Runnable {
 
    /**
     * スレッド名
     */
    private String name = null;
 
    /**
     * コンストラクタ
     *
     * @param name スレッド名
     */
    public ThreadTestImp(String name) {
        this.name = name;
    }
 
    /**
     * runメソッドのオーバーライド
     */
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(name + " " + i);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException ex) {
                System.out.println(ex.getMessage());
            }
        }
    }
 
    public static void main(String[] args) {
        ThreadTestImp threadTestImp1 = new ThreadTestImp("thread 1");
        ThreadTestImp threadTestImp2 = new ThreadTestImp("thread 2");
        Thread thread1 = new Thread(threadTestImp1);
        Thread thread2 = new Thread(threadTestImp2);
        thread1.start();
        thread2.start();
    }
}
3行目
Runnableインタフェースを実装しています.
23-32行目
Runnableインタフェースの全てのメソッド(runメソッドのみ)を実装する必要があります. 中身はThreadTestExt.javaと同じです.
35-36行目
ThreadTestImpクラスの2つのインスタンスを生成しています.
37-38行目
Threadクラスの2つのインスタンスを,それぞれのThreadTestImpのインスタンスで生成しています.

Treadクラスの継承とRunnableインタフェースの実装の使い分け

ThreadTestExt.javaとThreadTestImp.javaを比較すると,ThreadTestExt.javaの方が若干簡単です. しかし,Javaは単一継承なので,Treadクラスを継承してしまうと,他のスーパークラスを継承出来ません. runメソッドのオーバーライドだけで実現可能なような場合は,Runnableインタフェースを実装する方を選択しましよう. インタフェースはいくつでも実装できるので,都合が良い場合が多いです. どうしてもRunnbableインタフェースの実装では実現出来ないような場合はThreadクラスの継承を選択することを考えましょう.

スレッドの優先度

スレッドには優先度をつける事ができます. 優先度は数値で設定でき,高優先がThread.MAX_PRIORITY=10,低優先がThread.MIN_PRIORITY=1です. 初期値は5になっています. ただし,実際に処理される順番は処理系やJava VMに強く依存します.
ThreadTestImp.javaのmainメソッドに以下を追加し,動作を確認してみましょう.

ThreadTestImp.javaの追加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static void main(String[] args) {
    ThreadTestImp threadTestImp1 = new ThreadTestImp("thread 1");
    ThreadTestImp threadTestImp2 = new ThreadTestImp("thread 2");
    Thread thread1 = new Thread(threadTestImp1);
    Thread thread2 = new Thread(threadTestImp2);
    System.out.println(thread1.getPriority());
    System.out.println(thread2.getPriority());
    thread1.setPriority(Thread.MAX_PRIORITY);
    thread2.setPriority(Thread.MIN_PRIORITY);
    System.out.println(thread1.getPriority());
    System.out.println(thread2.getPriority());
    thread1.start();
    thread2.start();
}
6-7行目
2つのスレッドの優先度を表示しています.
8-9行目
thread1を最高優先,thread2を最低優先に設定しています.

スレッドの一時停止と停止

スレッドの一時停止にはThreadTestExt.javaやThreadTestImp.javaで既に見たようにsleepメソッドを用います. sleepメソッドのsleep(long millis)はsleep(long mills, int nanos)でオーバーロード可能で,millisはミリ秒,nanosはナノ秒で指定して一時停止させます. そのスレッドが一時停止している時に他のスレッドが実行可能であれば実行されるかもしれません.
sleepメソッドで一時停止したスレッドはその時間経過後,直ちに再開されるとは限りません. もし,時間経過時点で,他のスレッドが実行中であれば,それが終わるまで待たされるかもしれません. このスケジューリングはJava VMに依存してしまいます.
sleepメソッドは一時停止中に割り込みがかけられた時にチェック例外のInterruptedExceptionがスローされるので,try-catchかthrowで対応する必要があります. 次にスレッドの停止について学びます. stopやsuspendやresumeといったスレッド停止・再開に関するメソッドがありますが,現在は非推奨となっています.

スレッドの停止の待機

startメソッドでスレッドを開始し,その直後に処理を書くと,その処理は直ちに実行されてしまいます. しかし,スレッドが終了した事を受けてから次の処理を行いたい場合はよくあります. そこで,スレッドの終了時まで処理を待機するためにjoinメソッドが利用出来ます.
ThreadTestImp.javaのmainメソッドに以下を追加し,動作を確認してみましょう.

ThreadTestImp.javaの追加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public static void main(String[] args) {
    ThreadTestImp threadTestImp1 = new ThreadTestImp("thread 1");
    ThreadTestImp threadTestImp2 = new ThreadTestImp("thread 2");
    Thread thread1 = new Thread(threadTestImp1);
    Thread thread2 = new Thread(threadTestImp2);
    System.out.println(thread1.getPriority());
    System.out.println(thread2.getPriority());
    thread1.setPriority(Thread.MAX_PRIORITY);
    thread2.setPriority(Thread.MIN_PRIORITY);
    System.out.println(thread1.getPriority());
    System.out.println(thread2.getPriority());
    thread1.start();
    thread2.start();
    try {
        thread1.join();
        System.out.println("thread 1 has finished.");
    } catch (InterruptedException ex) {
        System.out.println(ex.getMessage());
    }
}
15行目
thread1の終了まで待機します.
16行目
thread1が終了したら実行されます.
joinメソッドも割り込みがかけられた時にチェック例外のInterruptedExceptionがスローされるので,try-catchかthrowで対応する必要があります.

スレッドの排他制御

複数のスレッドが1つのインスタンス対して作用する時,時に予期せぬ動作をすることがあります.
BankTest.javaをコピーし,動作を確認してみましょう.

BankTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
package jp.ac.utsunomiya_u.is;
 
/**
 * 銀行口座クラス
 */
class Account {
 
    /**
     * 預金額
     */
    private int balance = 0;
 
    /**
     * 入金
     *
     * @param amount 入金額
     */
    void deposit(int amount) {
        balance += amount;
    }
 
    /**
     * 預金額の取得
     *
     * @return 預金額
     */
    public int getBalance() {
        return balance;
    }
}
 
/**
 * 顧客クラス
 */
class Customer implements Runnable {
 
    /**
     * 銀行口座
     */
    private final Account account;
 
    /**
     * コンストラクタ
     *
     * @param account 銀行口座
     */
    Customer(Account account) {
        this.account = account;
    }
 
    /**
     * runメソッドのオーバーライド
     */
    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            // 1入金
            account.deposit(1);
        }
    }
}
 
public class BankTest {
 
    /**
     * 顧客数
     */
    private final static int NUMBER_OF_CUSTOMERS = 10;
 
    public static void main(String[] args) {
        // 銀行口座生成
        Account account = new Account();
        // 顧客配列生成
        Customer[] customers = new Customer[NUMBER_OF_CUSTOMERS];
        // スレッド生成
        Thread[] threads = new Thread[NUMBER_OF_CUSTOMERS];
        // 個々の顧客生成とスレッド開始
        for (int i = 0; i < NUMBER_OF_CUSTOMERS; i++) {
            customers[i] = new Customer(account);
            threads[i] = new Thread(customers[i]);
            threads[i].start();
        }
        // 全てのスレッド終了まで待機
        for (int i = 0; i < NUMBER_OF_CUSTOMERS; i++) {
            try {
                threads[i].join();
            } catch (InterruptedException ex) {
                System.out.println(ex.getMessage());
            }
        }
        // 銀行口座の預金額表示
        System.out.println(account.getBalance());
    }
}
18-20行目
引数で与えられた入金額を預金額に合算しています.
47-49行目
顧客の預金口座を指定しています.
55-60行目
runメソッドをオーバーライドし,1円ずつ1000回入金しています.
72行目
Accountクラスのインスタンスを1つだけ生成しています.
74行目
Customerクラスのインスタンスの配列を生成しています.
79行目
全ての顧客は同じ預金口座を使用するように設定しています.

このプログラムでは,10人の顧客が同じ口座に1円づつ1000回入金しています. したがって,10*1*1000=10000なので,最後の預金額表示は10000となるはずです. しかしながら,このプログラムは実行の度に異なる結果を示すと思います. この原因は19行目のbalance += amount;に原因があります. これはコードでは1行ですが,Java VM上での処理ではスレッド固有のメモリ領域にコピーされて作業が行われている可能性があります. balanceのインスタンスは1つしかないようですが,スレッド毎の処理でコピーが発生し,不整合が起こってしまっています. これを解決するためのスレッドの排他処理(あるスレッドの処理が他のスレッドの影響を受けないようにする処理)が必要になります. Javaではsynchronized修飾子を使う方法があります.
BankTest.javaのAccountクラスのdepositメソッドを修正し,動作を確認してみましょう.