This is an interesting problem. It took me a bit deeper into the core ExecutorService implementation. Thanks!
Solving without ExecutorService
From what you have mentioned, you will have atmost one thread executing tasks and atmost one task pending because we are interested only in the last submitted task. Do you really need an ExecutorService for this?
You can just hold the next task in a static AtomicReference field of a POJO object. Since we are interested only in the latest task, task producers may simply replace the object in the AtomicReference. The task consumer can get from this field as soon as the current task execution is done. The field must be:
static because there should be only one instance of this field
AtomicReference since multiple threads may be trying to set the next task.
Solving using ExecutorService
However, if you still want to go the ExecutorService way, you can try this out. Create a ThreadPoolExecutor with only one thread (core and maximum) and give it a BlockingQueue implementation that "forgets" all its elements as soon as a new one is added.
Here is a sample test code that submits new tasks 100 times. If the previous task has not been taken up for execution, it is discarded. If it has been, then it is executed and the new one is queued.
public class OnlyOneTask{
public static void main( String[] args ){
ExecutorService svc = null;
/* A BlockingQueue that immediately "forgets" all tasks it had as soon as a new one is "offered". */
BlockingQueue<Runnable> Q = new ArrayBlockingQueue<Runnable>( 1 ) {
private static final long serialVersionUID = 1L;
/* Forget the current task(s) and add the new one
* TODO These 2 steps may need synchronization. */
public boolean offer( Runnable e) {
clear();
return super.offer( e );
}
};
try {
/* A ThreadPoolExecutor that uses the queue we created above. */
svc = new ThreadPoolExecutor( 1, 1, 5000, TimeUnit.MILLISECONDS, Q );
for( int i = 0; i < 100; i++ ) {
/* Our simple task. */
int id = i;
Runnable r = () -> {
System.out.print( "|" + id + "|" );
};
svc.submit( r );
/* A delay generator. Otherwise, tasks will be cleared too fast. */
System.out.print( " " );
}
}
finally {
svc.shutdown();
try{
svc.awaitTermination( 10, TimeUnit.SECONDS );
}
catch( InterruptedException e ){
e.printStackTrace();
}
}
}
}
This sample class is only to give a sense of what I thought will work. You will certainly need to improve upon the following drawbacks in this implementation:
- The first task is anyhow executed because it is immediately picked up by the
ExecutorService. (This is why the next point becomes important.)
- Interruptibility/cancellability has to be brought in to the running tasks, if necessary
Another way using ExecutorService and Future.cancel()
This is actually the simplest, if you are checking thread interruption in the task. This is basically the same as above but instead of clear()ing the queue, we simply use Future.cancel() to indicate that we don't need to execute the last task.
public static void main( String[] args ){
ExecutorService svc = null;
try {
/* A single thread executor is enough. */
svc = Executors.newSingleThreadExecutor();
Future<?> f = null;
for( int i = 0; i < 100; i++ ) {
int id = i;
/* Our simple task. */
Runnable r = () -> {
/* If the thread has been interrupted (by the Future.cancel() call, then return from here. */
if( Thread.currentThread().isInterrupted() ) return;
System.out.print( "|" + id + "|" );
};
if( f != null ) f.cancel( true );
f = svc.submit( r );
/* A pseudo delay generator. */
System.out.print( " " );
}
}
finally {
svc.shutdown();
try{
svc.awaitTermination( 10, TimeUnit.SECONDS );
}
catch( InterruptedException e ){
e.printStackTrace();
}
}
}