4

我有一个对象需要在对象本身还活着的时候定期做一些工作,所以我设计了类似下面的东西。基本上是一个 Main 类,其中包含对 ScheduledExecutorService 实例的引用。在此示例中,所有定期工作是将字符串打印到 std。

我希望代码的行为如下:

  1. test2 被调用,它创建了一个 Main 对象 o1(其中有一个 ScheduledExecutorService)。
  2. test2 寄存器在 o1 上每秒打印一行。
  3. test2 返回,o1 变成垃圾。
  4. 系统 gc 启动到 gc o1,它有一个 finalize 方法来关闭它的本地调度程序。

但是,如果我运行这个程序,会发生什么,它将永远持续下去。基本上 gc 永远不会调用 o1 的终结器,因此调度程序永远不会关闭,因此即使主线程结束,程序仍然不会退出。

现在,如果我在 test2() 中注释掉 o1.register,程序的行为就像它应该的那样,例如调用 gc 等。此外,在调试器中,似乎只有在调用 ScheduledExecutorService.schedule 之后才会创建实际线程。

任何解释发生了什么?

public class Main {

public static void main(String[] args) throws Exception {
    test2();

    System.gc();
    System.out.println("Waiting for finalize to be called..");
    Thread.sleep(5000);
}

private static void test2() throws Exception {
    Main o1 = new Main();
    o1.register();
    Thread.sleep(5000);     
}

private final ScheduledExecutorService _scheduler = Executors.newSingleThreadScheduledExecutor();   

private void register() {
    _scheduler.scheduleWithFixedDelay(new Runnable() { 
        @Override public void run() { 
            System.out.println("!doing stuff...");
            }
        }, 1, 1, TimeUnit.SECONDS);
}

@Override
protected void finalize() throws Throwable  {
    try {
        System.out.print("bye");
        _scheduler.shutdown();          
    } finally {
        super.finalize();
    }       
}

}

4

2 回答 2

7

两个问题:

  1. 默认线程工厂创建非守护线程。主线程可以结束,但只要有活动的非守护线程,JVM 就不会终止。我相信您将需要编写一个创建守护线程的自定义线程工厂。
  2. 不要依赖被调用的终结器——不能保证终结器会在任何特定时间或永远被调用。此外,System.gc()调用被定义为对 JVM 的建议,而不是命令。API 文档中的措辞是

调用 gc 方法表明 Java 虚拟机花费精力来回收未使用的对象......

于 2010-08-02T01:58:55.593 回答
3

玩了WeakReference和ScheduledExecutorService之后,我想我现在对这个问题有了更好的理解。我的代码中的核心问题是以下方法 register()。它使用一个匿名对象 Runnable。像这样的匿名对象的问题是它创建了对父范围的强引用。请记住,如果您将父作用域中的字段设置为“final”,则可以从 Runnable 的 run() 方法中引用它们。我想如果我没有从我的 run() 中引用任何东西,我就不会创建如此强大的 ref。如本例所示,我在 run() 中所做的只是打印一些静态字符串。但是,根据观察到的行为,仍然会创建此类引用。

private void register() {
_scheduler.scheduleWithFixedDelay(new Runnable() { 
    @Override public void run() { 
        System.out.println("!doing stuff...");
        }
    }, 1, 1, TimeUnit.SECONDS);

}

进行这种编程的正确方法是创建一个类并自己传入您的对象。您还需要只保留一个弱参考。代码比较长,我只发布 Runnable 实现,它保持对域对象 Main 的弱引用。

private static class ResourceRefreshRunner implements Runnable
{
    WeakReference<Main> _weakRef;
    public ResourceRefreshRunner(Main o)
    {
        _weakRef = new WeakReference<Main>(o);
    }       
    @Override
    public void run() { 
        try {
            Main m = _weakRef.get();
            if (m != null) 
                m.shout(); 
            else 
                System.out.println("object not there, but future is running. ");
        } catch (Exception ex) {
            System.out.println(ex.toString());
        }
    }
}

现在在 Main 类中,我有:

public class Main {
ScheduledExecutorService _poolInstance;
ScheduledFuture<?> _future;
public Main(ScheduledExecutorService p)
{
    _poolInstance = p;
    _future = _poolInstance.scheduleWithFixedDelay(new ResourceRefreshRunner(this), 1, 1, TimeUnit.SECONDS);
}  ...

以及 Main 的终结器:

    @Override
protected void finalize() throws Throwable  {
    try {
        System.out.println("bye");
        _future.cancel(true);
    } finally {
        super.finalize();
    }       
}

使用此设置,代码将按预期运行。例如,当不再引用 Main 对象时,GC 将启动并调用终结器。我做的另一个实验是没有 _future.cancel(true); 在 finalize() 中,当 Main 对象被 GC-ed 时,Runnable.run() 中的弱引用不能再取消对 Main 对象的引用,但线程和任务仍在运行。

于 2010-08-03T20:13:08.827 回答