1
1

More than 1 year has passed since last update.

【並列デザインパターン連載】Single Thread Excecutionパターン

Last updated at Posted at 2021-10-10

業務で非同期処理を書いたときに「そのクラス、スレッドセーフですか?」と指摘を受けることが複数回ありました。
そこで、「そもそもスレッドセーフって何よ」「スレッドセーフじゃないと何が困るのよ」ということを勉強しました。本記事はそのまとめとなってます。

参考図書

この本は思考させる系の練習問題がついてて、良い気がします。
自分もまだ読破できてませんが(笑)

以下は、この本の第1章「Single Threaded Execution」が基になっています。

「スレッドセーフでない」とは

インスタンスまたはメソッドが、複数のスレッドから実行されたときに、期待した挙動をしなくなるような状態のこと。

要件

・一度に一人しか通れない門が一つある。通行人が3人いて、それぞれが頻繁にこの門を通る。
・門が通行人を通るごとに、下記の3つの値を表示したい。
 -それまで門を通った人数
 -通った人の名前
 -通った人の出身地

・通行人は下記の3人。
 -名前:Alice 出身地:Alaska
 -名前:Bobby 出身地:Brazil
 -名前:Clice 出身地:Canada

クラス設計

名前 役割
Main Javaのメインクラス。門を作り、人を通らせる
Gate 門を表すクラス。人が通る時に"それまでに通った人数""通った人の名前""通った人の出身地"を表示する
UserThread 人を表すクラス。Mainクラスが終了するまで門を通り続ける

実装

※こちら↓からDL可能です。
※筆者が作ったものではありません
https://www.hyuki.com/dp/dp2.html#download

Main.java
public class Main {
    public static void main(String[] args) {
        System.out.println("Testing Gate, hit CTRL+C to exit.");
        Gate gate = new Gate();
        new UserThread(gate, "Alice", "Alaska").start();
        new UserThread(gate, "Bobby", "Brazil").start();
        new UserThread(gate, "Chris", "Canada").start();
    }
}
Gate.java
public class Gate {
    private int counter = 0;
    private String name = "Nobody";
    private String address = "Nowhere";
    public void pass(String name, String address) {
        this.counter++;
        this.name = name;
        this.address = address;
        check();
    }
    public String toString() {
        return "No." + counter + ": " + name + ", " + address;
    }
    private void check() {
        if (name.charAt(0) != address.charAt(0)) {
            System.out.println("***** BROKEN ***** " + toString());
        }
    }
}
UserThread.java
public class UserThread extends Thread {
    private final Gate gate;
    private final String myname;
    private final String myaddress;
    public UserThread(Gate gate, String myname, String myaddress) {
        this.gate = gate;
        this.myname = myname;
        this.myaddress = myaddress;
    }
    public void run() {
        System.out.println(myname + " BEGIN");
        while (true) {
            gate.pass(myname, myaddress);
        }
    }
}

Gateクラスでは、checkメソッドで通過する人の名前出身地の頭文字が異なっていた場合にBROKENを標準出力しています。

動作確認

BROKENがたくさん出ました。なぜでしょう。
しかも、不思議なのは「名前と出身地の頭文字が合っている」「名前と出身地の頭文字が異なる」両方の場合にBROKENが出てしまっていることです。

"複数スレッド(=各UserThreadクラス)から実行されたときに期待した挙動しない"状態なので、ここではGateクラスがスレッドセーフでない状態になっています。

Testing Gate, hit CTRL+C to exit.
Alice BEGIN
Bobby BEGIN
Chris BEGIN
***** BROKEN ***** No.633: Bobby, Brazil //名前と出身地の頭文字が合っている
***** BROKEN ***** No.990: Bobby, Brazil
***** BROKEN ***** No.1511: Alice, Alaska
***** BROKEN ***** No.1928: Alice, Alaska
***** BROKEN ***** No.2512: Bobby, Brazil
***** BROKEN ***** No.2750: Chris, Canada
***** BROKEN ***** No.2998: Bobby, Brazil
***** BROKEN ***** No.3198: Bobby, Brazil
***** BROKEN ***** No.3499: Bobby, Brazil
***** BROKEN ***** No.4024: Bobby, Brazil
***** BROKEN ***** No.2198: Bobby, Brazil
***** BROKEN ***** No.4391: Bobby, Brazil
***** BROKEN ***** No.5290: Alice, Alaska
***** BROKEN ***** No.5514: Chris, Alaska //名前と出身地の頭文字が異なる
***** BROKEN ***** No.4901: Alice, Alaska

なぜこうなる?

各UserThreadクラス(=Alice,Bobby,Chris)はpassメソッドを実行し、Gateクラスのクラス変数であるcounter,name,addressを次々と変更します。

Aliceさんは今Gateクラス変数がどのような状態になっているかなどお構いなしにpassメソッドを実行し続けます。BobbyさんもChrisさんも然りです。

もしかしたらタイミングによっては、次のような処理順になることもあります。

BROKEN出力 処理順例1(出力された名前と出身地の頭文字が異なっている場合)
  • AliceさんがGateクラス変数nameAliceに変更する
  • BobbyさんがGateクラス変数addressBrazilに変更する
  • Gateクラスのcheckメソッドが実行される
  • nameAlice,AddressBrazilなので、name.charAt(0) != address.charAt(0)がtrueとなる
  • BROKEN, Alice, Brazilが出力される
BROKEN出力 処理順例2(出力された名前と出身地の頭文字が合っている場合)
  • AliceさんがGateクラス変数nameAliceに変更する
  • BobbyさんがGateクラス変数addressBrazilに変更する
  • Gateクラスのcheckメソッドが実行される
  • nameAlice,AddressBrazilなので、name.charAt(0) != address.charAt(0)がtrueとなる
  • AliceさんがGateクラス変数addressAlaskaに変更する
  • BROKEN, Alice, Alaskaが出力される

つまり、各スレッド(Alice,Bobby,Chris)がGateクラスに干渉する順番によって、得られる結果が異なる状態になっています。現状、干渉する順は制御できません。

改善方法

Aliceさんは今Gateクラス変数がどのような状態になっているかなどお構いなしにpassメソッドを実行し続けます。BobbyさんもChrisさんも然りです。`

ココが良くないので、Gateクラスのメソッド側で「自分を使っていいのは一回に一人だけ、同時に実行しないでね」という制限をかけます。抽象的な言葉で言うと「排他制御」ですね。

具体的にはJavaのsyncronized句を使います。
メソッドにsyncronized句を付けることで、このような仕様が追加されます

  • あるスレッドがsyncronizedメソッドを使う際、メソッドのロックを取得する
  • ロックを取得されたメソッドを他スレッドが使用すると、ブロックされる
  • メソッドの使用が終わるとロックが解放され、他スレッドが使用可能になる
(排他制御後)Gate.java
public class Gate {
    private int counter = 0;
    private String name = "Nobody";
    private String address = "Nowhere";
    //syncronizedで1度に1スレッドからしか使用できなくする    
    public synchronized void pass(String name, String address) {
        this.counter++;
        this.name = name;
        this.address = address;
        check();
    }
    //syncronizedで1度に1スレッドからしか使用できなくする
    public synchronized String toString() {
        return "No." + counter + ": " + name + ", " + address;
    }
    private void check() {
        if (name.charAt(0) != address.charAt(0)) {
            System.out.println("***** BROKEN ***** " + toString());
        }
    }
}

動作確認

排他制御前の実行では数秒とまたずにBROKENしていましたが、数秒待ってもBROKENしなくなりました。
(何時間もやってると壊れ始めるのかもしれませんが...)

Testing Gate, hit CTRL+C to exit.
Alice BEGIN
Bobby BEGIN
Chris BEGIN

どうして治ったの?

passメソッドを使えるのが1度に1スレッドになり、下記のような処理順が実現されるためです。

非BROKEN出力 処理順例
  • Aliceさんがpassメソッドのロックを取得する
  • AliceさんがGateクラス変数nameAliceに変更する
  • AliceさんがGateクラス変数addressAlaskaに変更する
  • Aliceさんがpassメソッドのロックを開放する
  • Gateクラスのcheckメソッドが実行される
  • nameAlice,addressAlaskaなので、name.charAt(0) != address.charAt(0)がfalseとなりBROKENしない
  • Bobbyさんがpassメソッドのロックを取得する
  • BobbyさんがGateクラス変数nameBobbyに変更する
  • BobbyさんがGateクラス変数addressBrazilに変更する
  • Bobbyさんがpassメソッドのロックを開放する
  • Gateクラスのcheckメソッドが実行される
  • nameBobby,addressBrazilなので、name.charAt(0) != address.charAt(0)がfalseとなりBROKENしない(以下略)

まとめ

  • 「スレッドセーフでない」とは、メソッドやインスタンスが複数スレッドから使用された時に意図しない挙動をしてしまう状態のこと。
  • 原因は、メソッドやインスタンスが複数スレッドで使用される際の使用順が制御されていないため。
  • 防ぐには、排他制御が必要。
  • 排他制御の一つとして、メソッドに「syncronized」句をつける方法がある。
1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1