Search code examples
javamultithreadingdictionaryserverscheduled-tasks

Managing multiple ScheduledFuture instance in Java


Consider the following scenario: You want to write a Java program that many users can interact with at the same time. When a user registers a request, the application must create a task for him, which will be done, for example, ten seconds later. But the user can cancel the task any time before it starts. The question is, what is the best way to implement this scenario?

Consider the following code snippet and optimize it if possible.

public class OurServer {
    ScheduledExecutorService executor = Executors.newScheduledThreadPool(100);
    Map<User, ScheduledFuture<?>> scheduledFutures = new HashMap<>();

    public static void main(String[] args) {}

    public void createTaskRequest (Request req) {
        ScheduledFuture<?> sf = executor.schedule(new Runnable() {
            @Override
            public void run() {
                //doSomeTasks
                scheduledFutures.remove(req);
            }
        }, 10, TimeUnit.SECONDS);
        scheduledFutures.put(req,sf);
    }

    public void cancelTaskRequest (Request req) {
        scheduledFutures.get(req).cancel(true);
    }
}

Is this way of using Map and ScheduledFuture classes correct? any better alternatives?


Solution

  • You can do several improvements:

    1. Extract your Runnable into an inner class for better readability (you can use a record with a recent Java version).
    2. Use an implementation of java.util.ConcurrentMap - Java provides java.util.concurrent.ConcurrentHashMap and java.util.concurrent.ConcurrentSkipListMap, for your application both should be sufficient.
    3. You forgot to remove cancelled requests from your map.
    4. You did not try to remove the requests from the executor to prevent even the start.
    5. You have to take care that the request for cancellation is not being processed as long as you did not add the result of the call to executor.schedule to the map - otherwise the cancellation will be lost. I used the computeIfAbsent and computeIfPresent to add and remove the entries to the map, those are atomic.
    6. Small optimization: I added a custom thread factory that will create "daemon threads". This means that the threads will not prevent your program from exiting. This is just to show you how to do it, in case you need it.
    package examples.stackoverflow.q76639234;
    
    import java.util.Map;
    import java.util.concurrent.*;
    
    public class OurServer {
      private final ScheduledExecutorService executor = Executors.newScheduledThreadPool(100, new OurThreadFactory());
      private final ConcurrentMap<Request, ScheduledFuture<?>> scheduledFutures = new ConcurrentHashMap<>();
    
      public void createTaskRequest(Request req) {
        scheduledFutures.computeIfAbsent(req, request -> {
          OurTaskRunnable command = new OurTaskRunnable(request, scheduledFutures);
          return executor.schedule(command, 10, TimeUnit.SECONDS);
        });
      }
    
      public void cancelTaskRequest(Request req) {
        scheduledFutures.computeIfPresent(req, (r, future) -> {
          future.cancel(true);
          return null; // this removes the value from the map
        });
    
      }
    
      private record OurTaskRunnable(Request request, Map<Request, ?> schedulerMap) implements Runnable {
    
        @Override
        public void run() {
          //doSomeTasks
          schedulerMap.remove(request);
        }
      }
    
      private static class OurThreadFactory implements ThreadFactory {
        private final ThreadFactory delegate = Executors.defaultThreadFactory();
    
        @Override
        public Thread newThread(Runnable r) {
          Thread thread = delegate.newThread(r);
          thread.setDaemon(true);
          return thread;
        }
      }
    }