2

我一直在挠头,试图找出 Java 计时器的挂起问题。我想知道这里是否有人可以帮忙。高度赞赏诊断问题的任何帮助。

我有一个包含三个 TimerTask 类(A、B 和 Stopper)的简单程序。A 和 B 分别每 400ms 和 500ms 重复运行一次。Stopper 任务计划在 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线程死亡
  • DestroyJavaVM线程启动执行JVM的关闭
  • DestroyJavaVM线程进入方法中的synchronized块,Shutdown.exit获取Shutdown.classmonitor
  • sequence方法运行注册的关闭挂钩
  • Thread-0线程开始运行我们在main方法中注册的关闭钩子
  • 线程打印其Thread-0名称并通过该System.exit方法启动另一个 JVM 关闭,该方法反过来尝试获取 Shutdown.class监视器但它不能,因为它已经被获取

总结一下:

  • DestroyJavaVM线程等待Thread-0线程完成
  • Thread-0线程等待DestroyJavaVM线程完成

根据定义,这是死锁。

笔记:

  • 有关其他信息,我建议阅读 SO 问题如何捕获 System.exit 事件?
  • 系统java类的链接代码是openjdk 6-b14,而我的是oracle 1.6.0_37,但我注意到源代码没有区别。
  • 我认为 Eclipse 没有正确显示线程状态,Thread-0肯定应该处于BLOCKED状态,因为它试图获取一个被占用的监视器(参见这里的代码示例)。不确定DestroyJavaVM线程​​,我不会假设不进行线程转储。
于 2013-07-23T22:25:29.827 回答
0

而不是System.exit(0)执行退货。此外,您应该将运行变量标记为 volatile。

于 2013-07-23T19:10:11.130 回答