Javaでスレッドの順序を保証するための操作コード


スレッドが多すぎると、スレッド開始の順序が実行の順序と異なることが分かります。三つのスレッドを作成して実行するだけでは、最後の実行順序は予想できない。これは、スレッドを作成した後、スレッド実行の開始時間はCPUがいつタイムプレートを割り当てるかによって決まり、スレッドは主スレッドに対する非同期動作と見なされるからである。

public class FIFOThreadExample {
    public synchronized static void foo(String name) {
        System.out.print(name);
    }

    public static void main(String[] args) {
        Thread thread1 = new Thread(() -> foo("A"));
        Thread thread2 = new Thread(() -> foo("B"));
        Thread thread3 = new Thread(() -> foo("C"));
        thread1.start();
        thread2.start();
        thread3.start();
    }
}
出力結果:ACB/ABC/CBA…
スレッドの順序をどうやって保証しますか?
スレッドの順序をどのように保証しますか?
1.Thread.join()を使って実現するThread.join()の役割は、親スレッドが子スレッドの終了を待ってから運転を継続することである。上記の例では、main()方法が存在するスレッドは親スレッドであり、ここでは3つのサブスレッドA,B,Cを作成し、子スレッドの実行は親スレッドに対して非同期であり、順序性を保証することができない。一方、子スレッドに対してThread.join()方法を使用した後、親スレッドが子スレッドの運転が終了するのを待ってから、親スレッドの実行を開始することができます。このように、スレッド実行が強制的に同期になり、Thread.join()方法でスレッド実行の順序が保証されます。

public class FIFOThreadExample {
    
    public static void foo(String name) {
        System.out.print(name);
    }

    public static void main(String[] args) throws InterruptedException{
        Thread thread1 = new Thread(() -> foo("A"));
        Thread thread2 = new Thread(() -> foo("B"));
        Thread thread3 = new Thread(() -> foo("C"));
        thread1.start();
        thread1.join();
        thread2.start();
        thread2.join();
        thread3.start();
    }
}
出力結果:ABC
2.単一ラインスレッドを使って実現する
もう一つの保証スレッドは、スレッドの順序を保証するための方法であり、このようなスレッドの池にはスレッドが一つしかないため、内部のスレッドは加入順に実行される。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class FIFOThreadExample {

    public static void foo(String name) {
        System.out.print(name);
    }

    public static void main(String[] args) throws InterruptedException{
        Thread thread1 = new Thread(() -> foo("A"));
        Thread thread2 = new Thread(() -> foo("B"));
        Thread thread3 = new Thread(() -> foo("C"));
        ExecutorService executor = Executors.newSingleThreadExecutor();
        executor.submit(thread1);
        executor.submit(thread2);
        executor.submit(thread3);
        executor.shutdown();
    }
}
出力結果:ABC
3.volatileキーワードで修飾した信号量を実現
上の二つの考えは、スレッドの実行順序を保証し、スレッドを一定の順序で実行させることです。ここでは三つ目の考えを紹介します。スレッドは無秩序に運行できますが、実行結果は順次実行されます。
3つのスレッドが作成され、start()が実行されると考えられます。したがって、run()によって実行される順序を保証するために、任意のタイミングで論理コードが実行されるかどうかをスレッドに知らせるための信号量が必要であることは間違いない。
また、3つのスレッドは独立しているので、この信号量の変化は他のスレッドに対して透明である必要があります。volatileキーワードも必要です。

public class TicketExample2 {

    //   
    static volatile int ticket = 1;
    //      
    public final static int SLEEP_TIME = 1;

    public static void foo(int name){
        //               ,          
        while (true) {
            if (ticket == name) {
                try {
                    Thread.sleep(SLEEP_TIME);
                    //        3 
                    for (int i = 0; i < 3; i++) {
                        System.out.println(name + " " + i);
                    }

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //     
                ticket = name%3+1;
                return;

            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> foo(1));
        Thread thread2 = new Thread(() -> foo(2));
        Thread thread3 = new Thread(() -> foo(3));
        thread1.start();
        thread2.start();
        thread3.start();
    }
}
実行結果:
1 0
1
1 2
2 0
2 1
2
3 0
3 1
3 2
4.ロックと信号量で実現
この方法の考え方は第三の方法と同じで、スレッド実行の順序を考慮せずに、いくつかの方法でスレッド実行のトラフィックロジックの順序を制御することを考慮している。ここでは、原子型信号量run()を使ってもいいです。もちろん、原子型を使わなくてもいいです。ここでは、自己増殖動作のスレッドの安全を保証するためだけです。その後、私たちは再入力可能ロックticketを使った。方法にロックをかけます。スレッドがロックされていて、ビットが正しいと、トラフィックロジックが実行されます。実行が完了したら、次のスレッドが起動します。
ここでは、whileを使ってスピン操作をする必要はありません。ロックは指定されたスレッドを起動させることができますので、ifに変更すれば順次実行できます。

public class TicketExample3 {
    //   
    AtomicInteger ticket = new AtomicInteger(1);
    public Lock lock = new ReentrantLock();
    private Condition condition1 = lock.newCondition();
    private Condition condition2 = lock.newCondition();
    private Condition condition3 = lock.newCondition();
    private Condition[] conditions = {condition1, condition2, condition3};

    public void foo(int name) {
        try {
            lock.lock();
            //               ,          
            System.out.println("  " + name + "     ");
            if(ticket.get() != name) {
                try {
                    System.out.println("      " + ticket.get() + ",  " + name + "     ");
                    //       
                    conditions[name - 1].await();
                    System.out.println("  " + name + "    ");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(name);
            ticket.getAndIncrement();
            if (ticket.get() > 3) {
                ticket.set(1);
            }
            //    ,     。1  2,2  3
            conditions[name % 3].signal();
        } finally {
            //      
            lock.unlock();
        }

    }

    public static void main(String[] args) throws InterruptedException {
        TicketExample3 example = new TicketExample3();
        Thread t1 = new Thread(() -> {
            example.foo(1);
        });
        Thread t2 = new Thread(() -> {
            example.foo(2);
        });
        Thread t3 = new Thread(() -> {
            example.foo(3);
        });
        t1.start();
        t2.start();
        t3.start();
    }
}
出力結果:
スレッド2の実行開始
現在の識別ビットは1で、スレッド2が待ち始めます。
スレッド1の実行開始
1
スレッド3の実行開始
現在の識別ビットは2です。スレッド3が待ち始めます。
スレッド2が呼び覚まされます
2
スレッド3が呼び覚まされます
3
上記の実行結果は一意ではないが、印刷の順序は123の順序であることを保証することができる。
参考文献
javaマルチスレッドは、複数スレッドの順番実行を実現します。Hoonick-ブログ園(cnblogs.com)
Java lockロックの詳細メモハウス-CSDNブログ
VolatileCallSite(Java Platform SE 8)(oracle.com)
java保証マルチスレッドの実行順序-james.yj-ブログ園(cnblogs.com)
以上はJavaの中でスレッドの順序の実行の詳しい内容を保証します。javaスレッドの実行順序に関する資料は他の関連記事に注目してください。