Help us understand the problem. What is going on with this article?

『Java言語で学ぶデザインパターン(マルチスレッド編)』まとめ(その1)

More than 3 years have passed since last update.

javathread.jpeg

『Java言語で学ぶデザインパターン(マルチスレッド編)』を読んだので要点を記録していく。

Java言語のスレッド

スレッドの起動

  • 方法1 java.lang.Threadクラスを使う。

あたらしく起動されるスレッドの動作はThreadクラスのrunメソッドに記述する。Threadクラスのstartメソッドを呼び出すと新しいスレッドが起動する。startメソッドを呼び出すと、新しいスレッドの起動、runメソッドの呼び出し、が行われる。

public class Main {
    public static void main(String[] args) {
        MyThread myThread = new MyThread();
        myThread.start();
        for (int i = 0; i < 100; i++) {
            System.out.print("1");
        }
    }

    public static class MyThread extends Thread {
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.print("2");
            }
        }
    }
}

1回目

実行結果
11111111111111111111111111111111222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222211111111111111111111111111111111111111111111111111111111111111111111222222

2回目

実行結果
11111222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222222211111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111
  • 方法2 Runnbaleインターフェースを使う

Runnableインターフェースを実装したクラスのインスタンスを使ってスレッドを起動する。Runnableインターフェースは次のように宣言されている。

public interface Runnable {
    public abstract void run();
}

Runnbaleインターフェースを実装したクラスを作り、そのインスタンスをThreadのコンストラクタに渡し、startメソッドを呼び出す。

public class Main {
    public static void main(String[] args) {
        Runnable myThread = new MyThread();
        Thread thread = new Thread(myThread);
        thread.start();
        for (int i = 0; i < 100; i++) {
            System.out.print("1");
        }
    }

    public static class MyThread implements Runnable {
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.print("2");
            }
        }
    }
}

スレッドの一時停止

Thread.sleepを使用する。sleepメソッドの呼び出しは、try-catchで囲む。sleepメソッドはInterruptedException例外を投げる。

public class Main {
    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            System.out.print("1");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
            }
        }
    }
}

スレッドの排他制御

スレッドAとスレッドBが競争(レース)して引き起こされる、期待に反するスレッドの挙動をデータ・レース(data race)と呼ぶ。data traceを防ぐためには、交通整理が必要で、これを一般的に排他制御と呼ぶ。Javaでは、スレッドの排他制御を行うときにsynchronizedというキーワードを使う。メソッドにsynchronizedというキーワードをつけて宣言すると、そのメソッドは一つのスレッドで動作するようになる。このようなメソッドをsynchronizedメソッド同期メソッドと呼ぶ。

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;
            return true;
        } else {
            return false;
        }
    }

    public String getName() {
        return name;
    }
}

スレッドが一つでもロックをとっている場合、他のスレッドは入ることができない。synchronizedメソッドを実行していたスレッドがそのメソッドの実行を終えると、ロックが解放(release)される。ロックが解放されると、いままでロックが取られていたために入れなかったスレッドのうち、どれか一つがロックを取ることができる。

ロックはインスタンスごとに存在する。あるインスタンスのsynchronizedメソッドを実行しているからといって、別のインスタンスのsynchronizedメソッドが実行できなくなるわけではない。

現在のスレッドが、あるオブジェクトのロックをとっているかどうかは、Thread.holdsLockメソッドで調べることができる。

メソッド全体ではなく、メソッドの一部を一つのスレッドで動作させたい場合には、synchronizedブロックを使う。

synchronized () {
    ...
}

synchronizedなインスタンスメソッドとsynchronizedブロック

以下は等価

synchronized void method() {
    ...
}

void method() {
    synchronized (this) {
        ...
    }
}

synchronizedなクラスメソッドとsynchronizedブロック

以下は等価

class Something {
    static synchronized void method() {
        ....
    }
}

class Something {
    static void method() {
        synchronized (Something.class) {
            ....
        }
    }
}

スレッドの協調

全てのインスタンスはウェイトセットというものを持ている。ウェイトセットはそのインスタンスのwaitメソッドを実行し、動作を停止しているスレッドの集合。インスタンスごとに用意されているスレッドの待合室のようなもの。スレッドは、waitメソッドを実行すると、動作を一時停止し、ウェイトセットという待合室に入る。スレッドは以下のどれかが発生するまでそのウェイトセットの中で待つ。

  • 他のスレッドからnotifyメソッドによって起こされる
  • 他のスレッドからnotifyAllメソッドによって起こされる
  • 他のスレッドからinterruptメソッドによって起こされる
  • waitメソッドがタイムアウトする

例えば、
obj.wait();
という文を実行したとき、現在のスレッドを一時停止し、インスタンスobjのウェイトセットに入る。このことをスレッドがobjの上でwaitしているという。waitメソッドを実行するためには、スレッドがロックを持っていなければならない。

ウエイトセットは仮想的な概念。インスタンス上でウェイトしているスレッドの一覧を得るメソッドはない。

(本書にこの概念をわかりやすく描いた図があるのでそちらを参照するとよい)

notifyメソッドを使うと、ウェイトセットにいるスレッドの一つをウェイトセットから出す。
例えば、
obj.notify();
という文をあるスレッドが実行したとする。すると、objのウェイトセットの中にいるスレッドから一つのスレッドが選ばれ、そのスレッドが起こされる。起こされたスレッドはウェイトセットから外へ出る。notifyメソッドを実行するためには、スレッドがロックを持っていなければならない。notifyで起こされたスレッドは、notifyを行った瞬間に実行を再開するわけではない。なぜなら、notifyを行った瞬間は、notifyしたスレッドがロックを持っているため。notifyメソッドを実行したとき、ウェイトセットで待っているスレッドが複数存在した場合、どのスレッドが選ばれるかは、仕様では定められていない。

notifyAllメソッドを使うと、ウェイトセットにいる全てのスレッドをウェイトセットから出す。notifyAllメソッドはwaitメソッド、norifyメソッドと同様に、呼び出すインスタンスのロックを持ったスレッドからしか呼び出せない。notify同様、出たスレッドがすぐに再開されるわけではない。notifyAllを実行したスレッドがロックを解放してから、その中の幸運な一つのスレッドだけが実行を再開できる。

ロックを持っていないスレッドがwait, notify, notifyAllを呼び出すと例外java.lang.IllegalMonitorStateExceptionが投げられる。

スレッドの状態遷移

スレッドは以下の状態を持つ。

  • NEW:起動していないスレッドの状態
  • RUNNABLE:Java仮想マシンで実行されているスレッドの状態
  • TERMINATED:終了したスレッドの状態
  • WAITING:ほかのスレッドが特定のアクションを実行するのを無期限に待機しているスレッドの状態
  • TIMED_WAITING:指定された待機時間、ほかのスレッドがアクションを実行するのを待機しているスレッドの状態
  • BLOCKED:ブロックされ、モニターロックを待機しているスレッドの状態

これら状態はThreadクラスのgetStateメソッドで得ることができる。


ソースコードは筆者のHPからDL可能。
http://www.hyuki.com

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away