views:

361

answers:

2

(similar to "Resettable Java Timer" but there are some subtleties I need to explore)

I need a resettable timeout feature, so that if my class does not perform a particular action within an interval of time T0 (where T0 is in the neighborhood of 50-1000msec), then a method gets called:

class MyClass {
    static final private timeoutTime = 50;
    final private SomeTimer timer = new SomeTimer(timeoutTime, 
        new Runnable () { public void run() {
            onTimeout();
        }});

    private void onTimeout() { /* do something on timeout */ }

    public void criticalMethod() { this.timer.reset(); }
}

What can I use to implement this? I'm familiar with ScheduledExecutorService, and the idea of calling ScheduledFuture.cancel() and then rescheduling the task seems like it should work, but then there's a potential hazard if cancel() fails and the scheduled task executes when it shouldn't. I feel like I'm missing a subtlety here.

Also (perhaps more importantly), is there a way to test my implementation / prove that it works properly?

edit: I am particularly concerned about the case where criticalMethod() gets called often (perhaps several times per millisecond)... if I use ScheduledExecutorService, it just seems like a potential resource problem to keep creating new scheduled tasks + canceling old ones, rather than having a direct way to reschedule a task.

+3  A: 

The cancelled attribute is attached to the task object. So, either the task hasn't started when you call cancel, and it won't get run; or the task has already started when you call cancel, and it gets interrupted.

How to handle interruption is up to you. You should regularly poll Thread.interrupted() (which, by the way, resets the interrupted flag, so beware) if you aren't calling any interruptible function (ones that declare InterruptedException in their throws clause).

Of course, if you are calling such functions, you should handle InterruptedException sensibly (which includes reasserting the interrupted flag (Thread.currentThread().interrupt()) before your task returns). :-)

To answer your edit, object creation is cheap, as long as your object doesn't have a lot of state. I personally wouldn't worry about it too much unless profiling shows it to be a bottleneck.

Chris Jester-Young
cancelled attribute is attached to the task object.... what if I schedule the same Runnable twice? wouldn't the cancelled attribute be kept as state with the ScheduledFuture that results from scheduling the task?
Jason S
Each time you schedule a `Runnable`, a new `ScheduledFutureTask` is created for it.
Chris Jester-Young
ah, ok, that makes sense.
Jason S
+2  A: 

ok, here's an attempt at using ScheduledExecutorService. I'm impressed by the performance; I ran the test program with arguments 50 1 10 (50msec timeout; every 1msec the ResettableTimer is reset 10 times) and it uses virtually none of my CPU.

package com.example.test;

import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

public class ResettableTimer {
    final private ScheduledExecutorService scheduler;
    final private long timeout;
    final private TimeUnit timeUnit;
    final private Runnable task;
    final private AtomicReference<ScheduledFuture<?>> ticket
        = new AtomicReference<ScheduledFuture<?>>();
    /* use AtomicReference to manage concurrency 
     * in case reset() gets called from different threads
     */

    public ResettableTimer(ScheduledExecutorService scheduler, 
            long timeout, TimeUnit timeUnit, Runnable task)
    {
        this.scheduler = scheduler;
        this.timeout = timeout;
        this.timeUnit = timeUnit;
        this.task = task;
    }

    public ResettableTimer reset(boolean mayInterruptIfRunning) {
        /*
         *  in with the new, out with the old;
         *  this may mean that more than 1 task is scheduled at once for a short time,
         *  but that's not a big deal and avoids some complexity in this code 
         */
        ScheduledFuture<?> newTicket = this.scheduler.schedule(
                this.task, this.timeout, this.timeUnit);
        ScheduledFuture<?> oldTicket = this.ticket.getAndSet(newTicket);
        if (oldTicket != null)
        {
            oldTicket.cancel(mayInterruptIfRunning);
        }
        return this;
    }


    static public void main(String[] args)
    {
        if (args.length >= 3) 
        {
            int timeout = Integer.parseInt(args[0]);
            int period = Integer.parseInt(args[1]);
            final int nresetsPerPeriod = Integer.parseInt(args[2]);
            ScheduledExecutorService scheduler = new ScheduledThreadPoolExecutor(1);
            final ResettableTimer timer = new ResettableTimer(scheduler, 
                    timeout, TimeUnit.MILLISECONDS,
                    new Runnable() { 
                        public void run() { System.out.println("timeout!"); }
                    }
            );

            // start a separate thread pool for resetting
            new ScheduledThreadPoolExecutor(5).scheduleAtFixedRate(new Runnable() {
                private int runCounter = 0;
                public void run() { 
                    for (int i = 0; i < nresetsPerPeriod; ++i)
                    {
                        timer.reset(false);
                    }
                    if ((++this.runCounter % 100) == 0)
                    {
                        System.out.println("runCounter: "+this.runCounter);
                    }
                }
            }, 0, period, TimeUnit.MILLISECONDS);

            try 
            {
                while (true)
                {
                    Thread.sleep(1000);
                }
            }
            catch (InterruptedException e)
            {
                System.out.println("interrupted!");
            }
        }
    }
}
Jason S