0

I have an abstract class which I use to expire instances of a subclass:

public abstract class Expirable {
    private transient Timer timer;

    protected abstract void onExpire();

    protected void setExpire(long delay) {
        resetExpire();

        timer = new Timer();
        timer.schedule(new TimerTask() {
                           @Override
                           public void run() {
                               resetExpire();
                               onExpire();
                           }
                       }, delay
        );
    }

    protected void resetExpire() {
        if (timer != null) {
            timer.cancel();
            timer = null;
        }
    }
}

I extend any class and overide onExpire, then I call setExpire(delay) from the subclass (not shown):

public class MyClass extends Expirable {
    @Override
    protected void onExpire() {
        // expiration code here
    }
}

This class works perfectly fine, but the timer object is very expensive. Because I have tens of thousands of instances, I wrote a cheap version with the same functionality, which uses a single timer scheduled at a fixed rate and a queue. Not as precise, but cheap.

public abstract class ExpirableCheap {
    private static final long COLLECTOR_INTERVAL_MSEC = 1000;
    private static final int INITIAL_CAPACITY = 128;

    private static Queue<ExpirableCheap> queue = new PriorityBlockingQueue<>(
            INITIAL_CAPACITY,
            Comparator.comparingLong(expirable -> expirable.expiresWhen)
    );

    @SuppressWarnings("FieldCanBeLocal")
    private static TimerTask timerTask;
    @SuppressWarnings("FieldCanBeLocal")
    private static Timer timer;
    static {
        timerTask = new TimerTask() {
            @Override
            public void run() {
                // this Runnable stops working
                long time = new Date().getTime();
                while (queue.peek() != null && queue.peek().expiresWhen < time) {
                    queue.poll().onExpire();
                }
            }
        };
        timer = new Timer();
        timer.scheduleAtFixedRate(timerTask,
                COLLECTOR_INTERVAL_MSEC,
                COLLECTOR_INTERVAL_MSEC
        );
    }

    private transient long expiresWhen;

    protected abstract void onExpire();

    protected synchronized void setExpire(long delay) {
        resetExpire();

        long time = new Date().getTime();
        expiresWhen = time + delay;

        queue.offer(this);
    }

    protected synchronized void resetExpire() {
        queue.remove(this);
    }
}

Obviously, the static code block is executed once and schedules the timer in regular intervals. The timerTask peeks at the queue and calls onExpire().

What's wrong?

This runs fine for a while, but then suddenly the timerTask is no longer executed. When testing, it works fine and I cannot simulate the situation, but it fails after some time in production.

I am not sure what happens, but I suspect the static variables which I initialized in the static code block are garbage collected when the last instance of the subclass is collected. Then, when the class is re-used, the static code block is not run again. In other words, it seems to work until there are no instances anymore which extend ExpirableCheap.

Strangely, the queue persists, reason why I expected an exception to happen inside the Runnable, which I believe is not the case.

As you can see, I tried to move timer and timerTask variables from the static code block into member variables (which didn't help). I also tried to synchronize setExpire() and resetExpire(), which I believe makes no difference either.

Can anybody see what's happening? Did I make another stupid mistake and am I on the wrong track?

Any suggestions what I could change to make this work?

Oliver Hausler
  • 4,900
  • 4
  • 35
  • 70
  • 1
    I don't think that the JVM would normally garbage collect something which could again be referenced later in the code. – Tim Biegeleisen Oct 18 '18 at 03:37
  • 1
    Are you sure it's not something going wrong in `onExpire`? – user2357112 Oct 18 '18 at 03:40
  • @user2357112 I have exception handing in the server, but I explicitly wrapped the code where `onExpire` is called into a try-catch block. Will get back to this. – Oliver Hausler Oct 18 '18 at 03:55
  • It doesn't have to be an exception. `onExpire` could be hanging. – user2357112 Oct 18 '18 at 04:01
  • @user2357112 This was a good bet, thanks. It was an exception in `onExpire`, but it was swallowed and not bubbled up to the server-wide exception handler. I expected to see such exception in the logs or have the JVM crash, but this obviously is not the case. Interesting discussion here: https://stackoverflow.com/questions/11584159/is-there-a-way-to-make-runnables-run-throw-an-exception - Post as answer and I'll accept :) – Oliver Hausler Oct 18 '18 at 04:17
  • @TimBiegeleisen You were right, the abstract class was not garbage collected. – Oliver Hausler Oct 18 '18 at 04:18
  • 1
    That’s one of the reasons why programmers should not use `java.util.Timer` and `TimerTask`; if an exception occurs, it will be swallowed and the task doesn’t get rescheduled. Use a [`ScheduledExecutorService`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/ScheduledExecutorService.html) instead. See also [`Executors.newSingleThreadScheduledExecutor()`](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/concurrent/Executors.html#newSingleThreadScheduledExecutor())… – Holger Oct 18 '18 at 11:19

1 Answers1

0

As @TimBiegeleisen correctly pointed out, Java works as expected. An abstract superclass is NOT garbage collected when the last instance of the subclass is collected.

My problem was unrelated.

Oliver Hausler
  • 4,900
  • 4
  • 35
  • 70