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?
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.