Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ScheduledExecutorService Life Cycle?

I have an object that needs to do periodically do some work while the object itself is alive, so I designed something like the following. Basically a Main class which contains a reference to a ScheduledExecutorService instance. In this example, all the periodical work is to print a string to std.

I expect the code to behave like the following:

  1. test2 gets called, which create a Main object o1 (within it a ScheduledExecutorService).
  2. test2 register to print out a line every second on o1.
  3. test2 returns, o1 becomes garbage.
  4. System gc kicks in to gc o1, which has a finalize method to shutdown it's local scheduler.

However, if I run this program, what happens is that it will go on FOREVER. Basically the gc never calls o1's finalizer and as a result, scheduler never shuts down and as a result, even when main thread end, the program still won't quit.

Now if I comment out the o1.register in test2(), the program behaves like it should, e.g. gc called etc. Also in debugger it seems only after a call to ScheduledExecutorService.schedule will an actual thread created.

Any explanation what's happening?

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();
    }       
}

}

like image 330
Xian Xu Avatar asked Dec 29 '22 09:12

Xian Xu


2 Answers

Two problems:

  1. The default thread factory creates non-daemon threads. The main thread can end, but as long as there are active non-daemon threads the JVM will not terminate. I believe you will need to write a custom thread factory that creates daemon threads.
  2. Do not depend on a finalizer being called -- there is no guarantee that a finalizer will be invoked at any particular time, or ever. Also, the System.gc() call is defined as a suggestion to the JVM, not a command. The wording in the API doc is

Calling the gc method suggests that the Java Virtual Machine expend effort toward recycling unused objects ...

like image 169
Jim Garrison Avatar answered Jan 08 '23 17:01

Jim Garrison


After playing with WeakReference and ScheduledExecutorService, I think I have a better understanding of the problem now. The central problem in my code is the following method register(). It uses an anonymous object Runnable. The problem with anonymous object like this is it creates a strong reference back to the parent scope. Remember if you make fields in parent scope "final", you can reference to them from within run() method of Runnable. I thought I do not create such strong ref if I don't reference anything from my run(). As shown in this case, all I do in the run() is to print some static string out. However, according the the behavior observed, such reference is created nonetheless.

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

}

The correct way of doing this kind of programming is to create a class and pass in your object yourself. You also need to keep only a weak reference. The code is rather long, I'll just post the Runnable implementation, which keeps a weak reference to the domain object 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());
        }
    }
}

Now in the Main class, I have:

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

And the finalizer of Main:

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

With this setup, the code behaves as expected. E.g. when a Main object is no longer referenced, GC will kick in and the finalizer will get called. One more experiment I did is that without _future.cancel(true); in the finalize(), when the Main object is GC-ed, the weak reference in the Runnable.run() can't dereference to a Main object anymore, but the thread and tasks is still running.

like image 40
Xian Xu Avatar answered Jan 08 '23 19:01

Xian Xu