2

Java タイマーのハングの問題を解決しようとして頭を悩ませています。ここで誰かが助けてくれるかどうか疑問に思っていました。問題を診断する際の助けをいただければ幸いです。

3 つの TimerTask クラス (A、B、Stopper) を持つ単純なプログラムがあります。A と B は、それぞれ 400ms と 500ms ごとに繰り返し実行されます。ストッパー タスクは、すべてをシャットダウンするために 2 秒で実行されるようにスケジュールされています。タイマーは期待どおりに起動し、タスクの run() メソッドは期待どおりに実行されます。ただし、ストッパータスクが実行されると、プログラムが終了することを期待していますが、「すべてのタスクとタイマーがキャンセルされ、終了しています」と出力した後にハングします。jstack を使用して問題を診断しようとしましたが、何をリリース/停止/キャンセルする必要があるかなどを示す明らかなものはありません。

これが私のコードです:

package com.example.experiments;

import java.util.Date;

/** 
 * A test timer class to check behavior of exit/hang issues
 */
public class TimerTest {

    TimerTest(){
    }

    class TaskA extends java.util.TimerTask {

        TaskA(){
        }
        public void run() {
            System.err.println("A.run() called.");

            if (!running){
                System.err.println("A: calling this.cancel().");
                this.cancel();
                return;
            }

        }
        public boolean cancel(){
            System.err.println("Canceling TaskA");
            return super.cancel();
        }
    }

    class TaskB extends java.util.TimerTask {

        TaskB(){
        }

        public void run(){
            System.err.println("B.run() called.");

            if (!running){
                System.err.println("B: calling this.cancel().");
                this.cancel();
                return;
            }

        }
        public boolean cancel(){
            System.err.println("Canceling TaskB");
            return super.cancel();
        }
    }


    private void start(){
        this.running = true; // Flag to indicate if the server loop should continue running or not

        final java.util.Timer timerA = new java.util.Timer();
        final TaskA taskA = new TaskA();
        timerA.schedule(taskA, 0, 400);

        final java.util.Timer timerB = new java.util.Timer();
        final TaskB taskB = new TaskB();
        timerB.schedule(taskB, 0, 500);

        class StopperTask extends java.util.TimerTask {
            private java.util.Timer myTimer;

            StopperTask(java.util.Timer timer){
                myTimer = timer;
            }

            public void run(){
                taskA.cancel();
                taskB.cancel();
                timerA.cancel();
                timerB.cancel();

                this.cancel();
                myTimer.cancel();
                System.err.println("Stopper task completed");
            }
        }
        final java.util.Timer stopperTimer = new java.util.Timer();
        final StopperTask stopperTask = new StopperTask(stopperTimer);
        stopperTimer.schedule(stopperTask, 2*1000);


        /** Register witjh JVM to be notified on when the JVM is about to exit */
        java.lang.Runtime.getRuntime().addShutdownHook(new Thread() {
            @Override
            public void run() {
                System.err.println("shutting down...");
                running = false;

                taskA.cancel();
                taskB.cancel();
                timerA.cancel();
                timerB.cancel();

                stopperTask.cancel();
                stopperTimer.cancel();

                System.err.println("All tasks and timers canceled, exiting");
                System.exit(0);
            }
        });     

    }

    public static void main(String[] args) {
        new TimerTest().start();
    }

    private boolean running = false;
}
4

2 に答える 2

6

Karthik が答えたように、を削除するSystem.exit(0)と、プログラムはハングしません。volatile私もキーワードに関する彼の発言に同意します。

シャットダウン フックが実行されているとき、JVM はすでに "静的" モニターによって保護されているシャットダウン シーケンスにあります。System.exit(0)その時点でメソッドを呼び出すと、実質的に JVM がデッドロック状態になります。

次のコード例を検討してください。

public static void main(String[] args) {
    System.out.println(Thread.currentThread().getName());
    java.lang.Runtime.getRuntime().addShutdownHook(new Thread() {
        @Override
        public void run() {   
            System.out.println(Thread.currentThread().getName());
            System.exit(0);                
        }
    });  
}

また、ハングします。赤い四角のボタンは、プログラムがまだ実行中であることを意味し、コンソール タブに表示されているように、mainメソッドmainを実行したスレッドの名前 ( ) と、シャットダウン フックを実行したスレッドの名前が出力されます。 ( Thread-0):

Eclipse IDE のスクリーンショット

System.exitメソッドを呼び出すと、次に呼び出されるメソッドはShutdown.exitメソッドです (無関係なソースはすべて省略しました)。

static void exit(int status) {

   ...

    synchronized (Shutdown.class) {  // "static" monitor mentioned in the first part of the post        
        sequence();
        halt(status);
    }
}

メソッドはすべてのsequenceフックとファイナライザーを実行しますが、haltメソッドはネイティブhalt0メソッドを呼び出し、その時点で JVM は最終的に終了すると思います。

これが起こることです:

  • mainメソッドはスレッドで実行され、スレッドmain名を出力し、シャットダウン フックを登録します。
  • 他のコードがないため、mainスレッドは終了します
  • JVMのDestroyJavaVMシャットダウンを実行するためにスレッドが開始されます
  • スレッドはメソッド内のDestroyJavaVM同期ブロックに入り、モニターShutdown.exitを取得しますShutdown.class
  • sequenceメソッドは、登録されたシャットダウン フックを実行します。
  • メソッドThread-0に登録されているシャットダウン フックを実行するスレッドが開始されます。main
  • スレッドはその名前を出力し、メソッドを Thread-0介して別の JVM シャットダウンを開始します。次に、モニターを取得しようとしますが、既に取得されているため取得できません。System.exitShutdown.class

総括する:

  • スレッドはスレッドが終了するDestroyJavaVMのを待ちますThread-0
  • スレッドはスレッドが終了するThread-0のを待ちますDestroyJavaVM

これは定義上デッドロックです。

ノート:

  • 追加情報については、SO question How to capture System.exit event? を読むことをお勧めします。
  • システムJavaクラスのリンクされたコードはopenjdk 6-b14ですが、私のものはoracle 1.6.0_37ですが、ソースに違いはありませんでした。
  • Eclipse はスレッドの状態を正しく表示していないと思います。取得したモニターを取得しようとしたため、間違いなくBLOCKEDThread-0状態になっているはずです (コード例については、こちらを参照)。スレッドについてはよくわかりませんが、スレッドダンプを行わないと想定できません。DestroyJavaVM
于 2013-07-23T22:25:29.827 に答える