LoginSignup
1
3

More than 1 year has passed since last update.

Javaで安全なスレッドを作りましょう

Last updated at Posted at 2021-05-12

背景

新幹線の切符は100枚があります。番号は1から100までです。現在、それらの切符を全部売っていくことにしたいです。
通常一つの窓口を設置して、順番に販売していくことはいいですが、乗客は多くなると、窓口のかかりの人は手が回れないですので、もう一つの窓口を設置しました。
そして、現在窓口1と窓口2があります。窓口1と窓口2は一緒に新幹線の切符を販売することになりました。

ここの窓口はスレッドとして理解しても大丈夫です。では、窓口をどうやって設置しますか?

スレッドの基本的な使い方

使い方1 Threadを継承したクラスを作成します

public class Main {
    public static void main(String[] args) {
        Thread thread1 = new MyThread();
        Thread thread2 = new MyThread();
        thread1.setName("thread 1st");
        thread2.setName("thread 2nd");
        thread1.start();
        thread2.start();
    }
}

class MyThread extends Thread {
    int ticket = 100;
    @Override
    public void run() {
        super.run();
        while (true) {
            if (ticket <= 0) {
                break;
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":" + +ticket);
            ticket -= 1;
        }
    }
}

JDKが用意したThreadクラスがあります、Threadを継承したカスタマイズクラスを生成して、runメソッドをオーバーライドします。切符販売処理をrunメセッドの中に書きます。
各スレッドからrunメソッドを実行させるためには、各スレッドのrunメソッドを呼び出します。

JDKのソースコードから確認したら、start()を呼び出すことによって、以下の処理が行われます。

  • Causes this thread to begin execution
    • 新しいスレッドを立ち上げます
  • the Java Virtual Machine calls the run method of this thread
    • 立ち上げられたスレッドでrunメソッドの中身を実行されます

run()だけをよびだすと、runメソッドの中身を実行されます、新しいスレッドが始まらない。mainスレッドでrunの中身が実行されることと同じことになります。

正しい使い方です。
myThread.start()
メインスレッドでrunメソッドを実行させることになります。
myThread.run()

アンドロイド開発の話
昔アンドロイドの開発をした時に、AsyncTaskを実行しようとして、executeではなく、oInBackgroundを呼び出して、エラーが発生された原因はまさか同じような原因です。
MyAsyncTask.execute(params) -> 正しい使い方です。
MyAsyncTask.oInBackground() main threadでdoInBackgroundコールバック メソッドを実行させます。

使い方2: Runableを実例化したインスタンスを作成して、Threadの構造関数の引数として渡します

public class Main {
    public static void main(String[] args) {
        Thread thread1 = new Thread(new MyRunable());
        Thread thread2 = new Thread(new MyRunable());
        thread1.setName("thread 1st");
        thread2.setName("thread 2nd");
        thread1.start();
        thread2.start();
    }
}

class MyRunable implements Runnable {
    int ticket = 100;
    @Override
    public void run() {
        while (true) {
            if (ticket <= 0) {
                break;
            }

            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":" + +ticket);
            ticket -= 1;
        }
    }
}

課題:共通データをアクセスする時

新幹線の切符はそれぞれの販売センター独自で販売するものではなく、すべて販売センターが共通で使うものにんあるべきです
そうしないと、重複な切符が販売されてしまいます

thread 1st:100
thread 2nd:100  // ここスレッド1とスレッド2が99番目の切符が販売されてしまいました。
thread 1st:99
thread 2nd:99
thread 2nd:98

対策1: Threadを継承したクラスを作成した場合

マルチスレッドから操作するデータをクラスのstatic辺数にします。そうしたら、スレッド達から操作する辺数はそれぞれのスレッドに所属するものではなく、唯一のMyThreadクラスに所属されることになります。

public class Main {
    public static void main(String[] args) {
        Thread thread1 = new MyThread();
        Thread thread2 = new MyThread();
        thread1.setName("thread 1st");
        thread2.setName("thread 2nd");
        thread1.start();
        thread2.start();
    }
}

class MyThread extends Thread{
    static int ticket = 100; // 共通変数
    @Override
    public void run() {
        super.run();
        while (true){
            if (ticket <= 0) {
                break;
            }

            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(Thread.currentThread().getName() + ":" + + ticket);
            ticket -= 1;
        }
    }
}

対策2: Runableを実例化したインスタンスを作成した場合

Runable実例化したインスタンス一つだけ生成します。そうしたら、マルチスレッドから操作する辺数は唯一なMyRunableオブジェクトに所属することになります。

public class Main {
    public static void main(String[] args) {
        Runnable myRunable = new MyRunable();
        Thread thread1 = new Thread(myRunable);
        Thread thread2 = new Thread(myRunable);
        thread1.setName("thread 1st");
        thread2.setName("thread 2nd");
        thread1.start();
        thread2.start();
    }
}


class MyRunable implements Runnable {
    int ticket = 100; // 共通変数
    @Override
    public void run() {
        while (true) {
            if (ticket <= 0) {
                break;
            }
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":" + +ticket);
            ticket -= 1;
        }
    }
}

安全なマルチスレッド

以上なThreadもしくはRunableの使い方によって、マルチスレッドから共通データを処理することができますが、安全スレッドとは言えません。ではなぜ安全ではないですか

安全スレッドではない理由

thread 2nd:4
thread 1st:3
thread 2nd:2
thread 1st:1
thread 2nd:0 // ここは0が出てきます、安全なスレッドではない証です

たとえ新幹線の切符を販売しているときに、0番目の切符を販売できないではないでしょうか?
なぜ0が出たか、理由はこちらです。
スクリーンショット 2021-05-10 9.48.06.png

runメソッドの中に、たくさんのスレッドが同時に入ることができます。 それによって、共通のデータが同時に複数のスレッドに触られ、if文、while文など条件に満たして処理に入りましたけど、処理する時もうはや条件に満たせないことが発生してしまい、不安全です。
スクリーンショット 2021-05-10 9.52.30.png

安全スレッド対策

新幹線に各車両には一つトイレがあります。一つのトイレは同時にもちろん一人しか利用できないです。一つのトイレは同時にもちろん一人しか利用できないことは安全なスレッドになります。
では、新幹線はどうやって、同時に一人しか利用できないことにさせたかというと、理由は二つがあります。

スクリーンショット 2021-05-10 22.51.38.png
新幹線の便所使用知らせ灯(Photo by PIXTA)

トイレの使用状況を乗客に公開して、利用されるときに、利用しようとする乗客をトイレ外で待たせます。トイレ利用されてないときだけ、利用しようとする乗客にはトイレに入ってもらいます。
図のように、使用状態を知らせ灯によって、乗客はトイレの使用状態に合わせて、トイレに行くタイミングを調整できます。

トイレにドーアにロックを設置します。中にトイレを利用する人にドーア内側からロックをかけてもらいます。そうすると、トイレが利用される時には、いくら外で待っている乗客を入ろうとしても、トイレに入れないです。
スクリーンショット 2021-05-10 23.01.36.png
https://chikirin.hatenablog.com/entry/20150610

安全なスレッドの使い方

便所使用知らせ灯 + ロックをかけますの役割を果たせるこはsynchronizedです。
synchronized(ロックオブジェクト)を使えば、新幹線のトイレみたいに安全なスレッドを管理できます。
ここで以下のことと認識していただければ、大丈夫です。


synchronized

トイレのドーア

ロックオブジェクト

ドーアのロック(唯一性が必要)

ドーアを使います

ドーアの使い方1: synchronizedブロック

安全保護したい部分をsynchronizedブロックで囲みます。

public class Main {
    public static void main(String[] args) {
        Runnable myRunable = new MyRunable();
        Thread thread1 = new Thread(myRunable);
        Thread thread2 = new Thread(myRunable);
        thread1.setName("thread 1st");
        thread2.setName("thread 2nd");
        thread1.start();
        thread2.start();
    }
}


class MyRunable implements Runnable {
    int ticket = 100;
    private Object lock = new Object(); // ドーアのロックを作成します

    @Override
    public void run() {
        while (true) {
            synchronized (lock) { // ドーアにロックをかけます
                if (ticket <= 0) {
                    break;
                }
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + ":" + +ticket);
                ticket -= 1;
            }
        }
    }
}

気をつけないといけないのは、以下の書き方は安全ではないです

    @Override
    public void run() {
        while (true) {
            synchronized (new Object()) {  // <- 新しいロックを作成して、ドーアにかけます。  だめです、間違います、安全なスレッドではないです。
                if (ticket <= 0) {
                    break;
                }
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + ":" + +ticket);
                ticket -= 1;
            }
        }
    }

なぜかというと、毎回スレッドははいると、新しいロックオブジェクトを作成することになります。そうしたら、ロックは唯一ではなく、トイレが利用される時に、外で待ちの乗客を止めることができなくなります

ドーアの使い方2: synchronizedメソッド

synchronizedブロックだと、ロックオブジェクトを用意する必要があります。一方、スレッド安全を確保したいコードを別のメソッドにして、メソッドをsynchronizedにすれば、スレッド安全を確保はできます。ロックオブジェクトも自ら用意しなくてもよいです(内部的に処理されてます)。

class MyRunable implements Runnable {
    int ticket = 100;
    @Override
    public void run() {
        while (true) {
            sellTicket();
            if (ticket <= 0) {
                break;
            }
        }
    }

    synchronized private void sellTicket() {
        if (ticket <= 0) {
            return;
        }
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + ":" + +ticket);
        ticket -= 1;

    }
}

ドーアのロックを使います

ロックの作りかたは二つがあります

ドーアのロックの使い方1: new Object()

JavaのObject型のオブジェクトを入れる必要がありますので、new Object()でいきます。

ドーアのロックの使い方2: this

MyRunableを元に作られたオブジェクトは一つしか存在していないし、MyRuableもObject側のサブクラスですので、ロックオブジェクトとして使えます。

public class Main {
    public static void main(String[] args) {
        Runnable myRunable = new MyRunable();
        Thread thread1 = new Thread(myRunable);
        Thread thread2 = new Thread(myRunable);
        thread1.setName("thread 1st");
        thread2.setName("thread 2nd");
        thread1.start();
        thread2.start();
    }
}


class MyRunable implements Runnable {
    int ticket = 100;
    @Override
    public void run() {
        while (true) {
            synchronized (this) {
                if (ticket <= 0) {
                    break;
                }
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + ":" + +ticket);
                ticket -= 1;
            }
        }
    }

}
ドーアのロックの使い方3: MyThread.class

クラス名.classもObject型のオブジェクトです、そしてMyThreadは唯一なクラスですので、ロックオブジェクトとして使えます。

public class Main {
    public static void main(String[] args) {
        Thread thread1 = new MyThread();
        Thread thread2 = new MyThread();
        thread1.setName("thread 1st");
        thread2.setName("thread 2nd");
        thread1.start();
        thread2.start();
    }
}

class MyThread extends Thread {
    static int ticket = 100;
    @Override
    public void run() {
        super.run();
        while (true) {
            synchronized (MyThread.class) {
                if (ticket <= 0) {
                    break;
                }

                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + ":" + +ticket);
                ticket -= 1;
            }
        }
    }
}

応用 Singleton パターン

パターン1

このパタンはなぜかスレッド安全とはいえないかというと、マルチスレッドからgetInstanceアクセスするところで、synchronizedというロックはないから、複数なinstanceが生成されてしまう可能性があります。

class TicketWindowNotSafe{
    static private TicketWindowNotSafe instance = null;
    private static TicketWindowNotSafe getInstance(){
        if (instance == null){
            return new TicketWindowNotSafe();
        }
        return instance;
    }
}

パターン2

synchronizedというロックを使って、あるスレッドはgetInstanceをアクセスするときに、他のスレッドはgetInstanceのそとで待つことになります。getInstanceは同時に一つのスレッドしかアクセスできないため、スレッド安全とは言えます。
しかし、instanceが生成された場合は、synchronizedの中に入ってからinstanceを取り出すことになります。synchronizedの中に入るためには、前のスレッドがgetInstanceから出ることを待つ必要があります。その待ち時間が長いため、全体的に非効率です。

class TicketWindowSafeNotEfficient{
    static private TicketWindowSafeNotEfficient instance = null;
    private static TicketWindowSafeNotEfficient getInstance(){
        synchronized (TicketWindowSafeNotEfficient.class){
            if (instance != null){
                return instance;
            }
            return new TicketWindowSafeNotEfficient();
        }
    }
}

パラーン3

synchronizedの外側で、もしinstanceがすでに生成されましたら、ただちにinstanceを取ります。そのため、instanceがすでに生成された場合、synchronizedに入る必要がなく、待ち時間も必要がなく、全体的に効率よくなります。

class TicketWindowSafeAndEfficient{
    static private TicketWindowSafeNotEfficient instance = null;
    private static TicketWindowSafeNotEfficient getInstance(){
        if (instance != null){
            return instance;
        }

        synchronized (TicketWindowSafeNotEfficient.class){
            if (instance != null){
                return instance;
            }
            return new TicketWindowSafeNotEfficient();
        }

    }
}

結論

Javaのsynchronizedはマルチスレッドを安全に守ることができます。大事な知識です。
決して難しいことではないです。

1
3
3

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
3