スレッドはどのように通信しますか?


次に、Javaでスレッド間通信を実現する方法をいくつかの例から切り込みポイントとして説明します.
  • は、2つのスレッドを順次実行する方法(Thread.join)
  • は、2つのスレッドを指定された方法で順次交差させる方法ですか?(lock.wait() lock.notify())
  • の4つのスレッドABCDで、DはABCがすべて実行するまで待たなければならないが、ABCは同期実行(CountDownLatch)
  • である.
  • 3人の選手がそれぞれ準備して、3人が準備ができたら、一緒に
  • を走ります.
  • サブスレッドあるタスクが完了すると、得られた結果をメインスレッド
  • に返信する.
    2つのスレッドを順次実行する方法
    2つのスレッドがあると仮定し、1つはスレッドAであり、もう1つはここにチップスレッドBを挿入し、2つのスレッドはそれぞれ1〜3の数字を順次印刷すればよい.ちょっと見てみましょう
    private static void demo1() {
        Thread A = new Thread(new Runnable() {
            @Override
            public void run() {
                printNumber("A");
            }
        });
        Thread B = new Thread(new Runnable() {
            @Override
            public void run() {
                printNumber("B");
            }
        });
        A.start();
        B.start();
    }
    

    このうちprintNumber(String)は、1,2,3の3つの数字を順次印刷するために実現される.
    private static void printNumber(String threadName) {
        int i=0;
        while (i++ < 3) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(threadName + " print: " + i);
        }
    }
    
    B print: 1
    A print: 1
    B print: 2
    A print: 2
    B print: 3
    A print: 3
    

    AとBが同時に印刷されていることがわかります.では、もし私たちがBがAを全部印刷してから印刷を始めたいなら?thread.join()メソッドを使用できます.コードは次のとおりです.
    private static void demo2() {
        Thread A = new Thread(new Runnable() {
            @Override
            public void run() {
                printNumber("A");
            }
        });
        Thread B = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("B      A");
                try {
                    **A.join();**
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                printNumber("B");
            }
        });
        B.start();
        A.start();
    }
    

    結果は次のとおりです.
    B      A
    A print: 1
    A print: 2
    A print: 3
    B print: 1
    B print: 2
    B print: 3
    

    したがって、A.join()メソッドは、Aが実行されるまでBを待たせることがわかります.
    2つのスレッドを指定した方法で順次交差させるにはどうすればいいですか?
    やはり上の例ですが、私は今Aが1を印刷した後、Bに1、2、3を印刷させ、最後にAに戻って2、3を印刷し続けてほしいと思っています.このような需要の下で、明らかにThread.join()はもう満足できない.実行順序を制御するには、より微細なロックが必要です.ここでは,object.wait()とobject.notify()の2つの方法を用いて実現できる.コードは次のとおりです.
    /**
     * A 1, B 1, B 2, B 3, A 2, A 3
     */
    private static void demo3() {
        Object lock = new Object();
        Thread A = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    System.out.println("A 1");
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("A 2");
                    System.out.println("A 3");
                }
            }
        });
        Thread B = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    System.out.println("B 1");
                    System.out.println("B 2");
                    System.out.println("B 3");
                    lock.notify();
                }
            }
        });
        A.start();
        B.start();
    }
    

    印刷結果は次のとおりです.
    A 1
    A waiting…
    B 1
    B 2
    B 3
    A 2
    A 3
    

    まさに私たちが望んでいる結果です.では、この過程で何が起こったのでしょうか.
  • まず、AとBが共有するオブジェクトロックlock=new Object()を作成する.
  • Aがロックされた後、まず1を印刷し、lock.wait()メソッドを呼び出し、ロックの制御権を渡し、wait状態に入る.
  • Bは、Aが最初にロックされたため、Bが実行できない.Aがlock.wait()を呼び出して制御権を解放するまで、Bはロックされた.
  • Bは、ロックを取得した後、1,2,3を印刷する.次にlock.notify()メソッドを呼び出し、wait中のAを起動します.
  • Aが起動した後、残りの2,3を印刷し続けます.よりよく理解するために、私は上のコードにlogを加えて読者が
  • を見るのに便利です.
    private static void demo3() {
        Object lock = new Object();
        Thread A = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("INFO: A     ");
                synchronized (lock) {
                    System.out.println("INFO: A      lock");
                    System.out.println("A 1");
                    try {
                        System.out.println("INFO: A         ,    lock      ");
                        lock.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("INFO:       A, A       lock");
                    System.out.println("A 2");
                    System.out.println("A 3");
                }
            }
        });
        Thread B = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("INFO: B     ");
                synchronized (lock) {
                    System.out.println("INFO: B      lock");
                    System.out.println("B 1");
                    System.out.println("B 2");
                    System.out.println("B 3");
                    System.out.println("INFO: B     ,   notify    ");
                    lock.notify();
                }
            }
        });
        A.start();
        B.start();
    }
    

    印刷結果は次のとおりです.
    INFO: A    
    INFO: A      lock
    A 1
    INFO: A         ,   lock.wait()     lock     
    INFO: B    
    INFO: B      lock
    B 1
    B 2
    B 3
    INFO: B     ,   lock.notify()   
    INFO:       A, A       lock
    A 2
    A 3
    

    4つのスレッドA B C D、ここでDはA B Cの全実行が完了するまで待ってから実行され、A B Cは同期して実行される
    最初にthread.join()を紹介しましたが、あるスレッドを別のスレッドが実行されてから実行を続けることができます.Dスレッドでは順次join A B Cを実行することができますが、これによりA B Cは順次実行されなければなりません.私たちが望んでいるのは、この3つが同期して実行できることです.あるいは、私たちが達成したい目的は、A B Cの3つのスレッドが同時に実行され、それぞれ独立して実行された後にDに通知することです.Dについては、A B Cがすべて実行済みであれば、Dは運転を再開します.この場合,CountdownLatchを用いてこのような通信方式を実現することができる.基本的な使い方は次のとおりです.
  • カウンタを作成し、初期値を設定します.CountdownLatch countDownLatch=new CountDownLatch(2);
  • 待機スレッドでcountDownLatch.await()メソッドを呼び出し、カウント値が0になるまで待機状態に入る.
  • 他のスレッドでcountDownLatch.contDown()メソッドが呼び出され、カウント値が1減少します.
  • 他のスレッドのcountDown()メソッドがカウント値を0にすると、スレッド内のcountDownLatch.await()が直ちに終了するのを待って、次のコードを実行し続けます.
  • private static void runDAfterABC() {
        int worker = 3;
        CountDownLatch countDownLatch = new CountDownLatch(worker);
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("D is waiting for other three threads");
                try {
                    countDownLatch.await();
                    System.out.println("All done, D starts working");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        for (char threadName='A'; threadName <= 'C'; threadName++) {
            final String tN = String.valueOf(threadName);
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println(tN + " is working");
                    try {
                        Thread.sleep(100);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    System.out.println(tN + " finished");
                    countDownLatch.countDown();
                }
            }).start();
        }
    }
    

    実行結果
    D is waiting for other three threads
    A is working
    B is working
    C is working
    A finished
    C finished
    B finished
    All done, D starts working
    

    実は簡単に言えば、CountDownLatchは逆カウンタで、私たちは初期カウント値を3に設定して、Dが実行する時、先にcountDownLatch.await()を呼び出してカウンタ値が0かどうかを検査して、0でなければ待機状態を維持します;A B Cは、それぞれの運転が完了するとcountDownLatch.contDown()を利用して、カウンタを1ずつ減算し、3つの運転が完了するとカウンタを0に減算する.この時点でDのawait()実行が直ちにトリガーされて終了し、引き続き下向きに実行されます.したがって、CountDownLatchは、1つのスレッドが複数のスレッドを待つ場合に適しています.
    3人の選手はそれぞれ準備をして、3人が準備ができたら、一緒に走ります.
    上はイメージの比喩で、スレッドA B Cに対してそれぞれ準備を開始し、3つの準備が完了するまで同時に実行します.つまり、スレッド間で互いに待つ効果を実現するには、どのように実現すればいいのでしょうか.上のCountDownLatchはカウントダウンに使用できますが、カウントが完了すると、1つのスレッドのawait()だけが応答し、複数のスレッドを同時にトリガすることはできません.このような要求をスレッド間で互いに待つために,CyclicBarrierデータ構造を利用することができ,その基本的な使い方は:
  • まず共通のCyclicBarrierオブジェクトを作成し、同時に待機するスレッド数を設定します.CyclicBarrier cyclicBarrier=new CyclicBarrier(3);
  • これらのスレッドは同時に自分で準備を開始し、自分で準備が完了した後、他の人の準備が完了するのを待つ必要があります.このときcyclicBarrier.await()を呼び出します.他の人を待つことができます.
  • 指定された同時待機スレッド数がcyclicBarrier.await()を呼び出す.を選択すると、これらのスレッドの準備が完了し、同時に実行されることを意味します.
  • private static void runABCWhenAllReady() {
        int runner = 3;
        CyclicBarrier cyclicBarrier = new CyclicBarrier(runner);
        final Random random = new Random();
        for (char runnerName='A'; runnerName <= 'C'; runnerName++) {
            final String rN = String.valueOf(runnerName);
            new Thread(new Runnable() {
                @Override
                public void run() {
                    long prepareTime = random.nextInt(10000) + 100;
                    System.out.println(rN + " is preparing for time: " + prepareTime);
                    try {
                        Thread.sleep(prepareTime);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    try {
                        System.out.println(rN + " is prepared, waiting for others");
                        **cyclicBarrier.await();** //          ,       
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } catch (BrokenBarrierException e) {
                        e.printStackTrace();
                    }
                    System.out.println(rN + " starts running"); //           ,     
                }
            }).start();
        }
    }
    

    印刷結果は次のとおりです.
    A is preparing for time: 4131
    B is preparing for time: 6349
    C is preparing for time: 8206
    A is prepared, waiting for others
    B is prepared, waiting for others
    C is prepared, waiting for others
    C starts running
    A starts running
    B starts running
    

    サブスレッドがタスクを完了したら、結果をメインスレッドに返さなければなりません.
    実際の開発では,サブスレッドを作成して時間のかかるタスクを行い,タスク実行結果をプライマリスレッドに返信して使用することが多いが,Javaではどのように実現するのか.スレッドの作成をレビューすると、一般的にRunnableオブジェクトをThreadに渡して実行します.Runnableの定義は次のとおりです.
    public interface Runnable {
        public abstract void run();
    }
    

    run()は、実行後も結果を返さないことがわかります.結果を返したいなら?ここでは、別の類似のインタフェースクラスCallableを利用することができます.
    @FunctionalInterface
    public interface Callable<V> {
        /**
         * Computes a result, or throws an exception if unable to do so.
         *
         * @return computed result
         * @throws Exception if unable to compute a result
         */
        V call() throws Exception;
    }
    

    Callableの最大の違いは,モデルVの結果を返すことである.
    次の問題は、サブスレッドの結果をどのように返すかということです.Javaでは、Callableに合わせて使用されるクラスがあります.FutureTaskですが、結果を取得するgetメソッドがメインスレッドをブロックすることに注意してください.
    たとえば,サブスレッドに1から100を加算し,算出した結果をメインスレッドに戻したい.
    private static void doTaskWithResultInWorker() {
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                System.out.println("Task starts");
                Thread.sleep(1000);
                int result = 0;
                for (int i=0; i<=100; i++) {
                    result += i;
                }
                System.out.println("Task finished and return result");
                return result;
            }
        };
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        new Thread(futureTask).start();
        try {
            System.out.println("Before futureTask.get()");
            System.out.println("Result: " + futureTask.get());
            System.out.println("After futureTask.get()");
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
    

    印刷結果は次のとおりです.
    Before futureTask.get()
    Task starts
    Task finished and return result
    Result: 5050
    After futureTask.get()
    

    メインスレッドがfutureTask.get()メソッドを呼び出すと、メインスレッドがブロックされることがわかります.その後、Callable内部で実行を開始し、演算結果を返します.このときfutureTask.get()の結果が得られ、メインスレッドは運転を再開します.ここでは,FutureTaskとCallableによりサブスレッドの演算結果を直接メインスレッドで得ることができ,メインスレッドをブロックする必要があるだけであることを学ぶことができる.もちろん、メインスレッドをブロックしたくない場合は、ExecutorServiceを利用してFutureTaskをスレッドプールに配置して実行を管理することも考えられます.