1

In particular, does it implement a while-true loop internally like so?

while (System.currentTimeMillis() < timeToRunTask){
    Thread.sleep(1000);
}

doTask();
  • 2
    JDK sources are available ... you can go and see the actual implementation. OpenJDK: http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/8u40-b25/java/util/concurrent/ScheduledThreadPoolExecutor.java?av=f – Fildor Aug 30 '17 at 09:15
  • 2
    But meanwhile: I *highly* doubt any JDK will implement that by using a spin-wait. – Fildor Aug 30 '17 at 09:20
  • related: https://stackoverflow.com/questions/9072431/thread-sleep-implementation – rjdkolb Aug 30 '17 at 09:29
  • 1
    I looked through the implementation, I don't really understand it though. I'm guessing there is a main thread checking for the time, and delegating the tasks to worker threads from the pool. How does the main thread check for time though? – nicholaslyz2 Aug 30 '17 at 09:30
  • 1
    It doesn't. At least in the OpenJDK impl. tasks are decorated and added to a DelayedQueue which handles the delay. You'll probably have to find your way to the exact "hot" spot. – Fildor Aug 30 '17 at 09:36
  • The actual delay seems to be implemented by usage of [Condition.awaitNanos](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/Condition.html#awaitNanos-long-) on a worker Thread. – Fildor Aug 30 '17 at 09:50
  • Wow how did you find that out? I'm still stepping through the constructor code. – nicholaslyz2 Aug 30 '17 at 09:56
  • @Fildor, now that you've dug into it that much, maybe you could write up an answer with some more explanation as to what the implementation you linked to actually does, roughly? – domsson Aug 30 '17 at 12:53
  • 1
    @domsson I wrote up an answer. Not sure if it is clear enough for a newbie but at least it will save hours of code-digging. – Fildor Aug 30 '17 at 13:43
  • @nicholaslyz2 The constructor won't lead you very far. I started where a task is added : `schedule` , There you will see, it will be added to a queue and it is ensured a worker will try to `take()` from that queue. The magic happens inside the Queue implementation. See my answer for details. – Fildor Aug 30 '17 at 13:56

1 Answers1

5

From http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/8u40-b25/java/util/concurrent/ScheduledThreadPoolExecutor.java#ScheduledThreadPoolExecutor.DelayedWorkQueue :

A Task will be decorated and put into the DelayedWorkQueue. Then it is ensured there is a Thread that tries to take the just added entry.

public ScheduledFuture<?>  schedule(Runnable command,
                                    long delay,
                                    TimeUnit unit) {
     if (command == null || unit == null)
         throw new NullPointerException();
     RunnableScheduledFuture<?> t = decorateTask(command,
         new ScheduledFutureTask<Void>(command, null,
                                       triggerTime(delay, unit)));
     delayedExecute(t);
     return t;
 }

private void delayedExecute(RunnableScheduledFuture<?> task) {
     if (isShutdown())
         reject(task);
     else {
// Enqueue Task vv
         super.getQueue().add(task);  
         if (isShutdown() &&
             !canRunInCurrentRunState(task.isPeriodic()) &&
             remove(task))
             task.cancel(false);
         else
// Make sure, there is a worker to be blocked
             ensurePrestart();
     }
 }

take() will be called in the worker thread's run(). The actual delay is in here: (See comments)

    public RunnableScheduledFuture<?> take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            for (;;) {
                RunnableScheduledFuture<?> first = queue[0];
                if (first == null)
                    // if queue is empty, wait (block) until signaled "not empty"
                    available.await();  
                else {
 // compute how long Task still has to wait in NANOSECONDS
                    long delay = first.getDelay(NANOSECONDS);
 // no further waiting? Then go on and execute!
                    if (delay <= 0)
                        return finishPoll(first);
                    first = null; // don't retain ref while waiting
                    if (leader != null)  // leader/follower Pattern: 
                                         // see Comments in DeleyedWorkQueue
                        available.await();
                    else {
                        Thread thisThread = Thread.currentThread();
                        leader = thisThread;
                        try {
// ########## HERE IT COMES vvvvvvvvvvvvvvvvvvvvvvvvvvvv
                            available.awaitNanos(delay); 
// Wait (block worker thread) for 'delay' nanos
                        } finally {
                            if (leader == thisThread)
                                leader = null;
                        }
                    }
                }
            }
        } finally {
            if (leader == null && queue[0] != null)
                available.signal();
            lock.unlock();
        }
    }

This variant of the Leader-Follower pattern (http://www.cs.wustl.edu/~schmidt/POSA/POSA2/) serves to minimize unnecessary timed waiting. When a thread becomes the leader, it waits only for the next delay to elapse, but other threads await indefinitely. The leader thread must signal some other thread before returning from take() or poll(...), unless some other thread becomes leader in the interim. Whenever the head of the queue is replaced with a task with an earlier expiration time, the leader field is invalidated by being reset to null, and some waiting thread, but not necessarily the current leader, is signalled. So waiting threads must be prepared to acquire and lose leadership while waiting.

From comments on grepcode.

Mind that this is for OpenJDK. Oracle or other JDKs might differ in implementation.

You can read about Condition.awaitNanos by following the link.

Fildor
  • 14,510
  • 4
  • 35
  • 67
  • Let's assume my thread pool size is 2. I schedule 2 tasks, both with a very long delay. Now both workers will call `take()` and `awaitNanos()` and will block until this point in the future. If I now schedule a third task with a very short delay, this task will be delayed longer - until at least 1 thread in the pool finishes waiting and gets to execute it's task. Is my thinking correct here? – a.ilchinger May 11 '21 at 16:34
  • Probably, yes. Haven't looked into this for quite a while. – Fildor May 11 '21 at 16:39
  • 2
    In case anyone else lands here: I tried it with OpenJDK14, and a later scheduled task with a short delay will be run first. So `take()` on a task in the queue will not block the worker thread until the "taken" task gets scheduled. – a.ilchinger May 12 '21 at 16:53