org.jtrim2.executor.TaskScheduler Maven / Gradle / Ivy
package org.jtrim2.executor;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicReference;
import org.jtrim2.utils.ExceptionHelper;
/**
* Allows tasks to be {@link #scheduleTask(Runnable) scheduled} then be
* {@link #dispatchTasks() executed} later by an {@link Executor} or
* {@link TaskExecutor}. The tasks will be submitted to the executor in the
* order they were scheduled.
*
* The main benefit of using this class is that scheduling a task is
* synchronization transparent and therefore can be called while a lock
* is held. Since submitting task happens in the same order as the scheduling
* this can be used to invoke event handlers because with events it is usually
* important to notify the listeners in the order the events actually occurred.
*
* It is important to note that to achieve the aforementioned goals tasks will
* never be submitted concurrently, they will be executed one after another.
* An additional useful side-effect is that tasks will not even be submitted
* while another task is being executed on the current thread avoiding another
* possible source of problems. To clarify this, see the following example:
*
{@code
* void doPrint() {
* final TaskScheduler scheduler;
* scheduler = new TaskScheduler(SyncTaskExecutor.getSimpleExecutor());
* scheduler.scheduleTask(() -> {
* System.out.print("2");
* scheduler.scheduleTask(() -> System.out.print("4"));
* scheduler.dispatchTasks(); // In this case, this is a no-op
* System.out.print("3");
* });
* System.out.print("1");
* // The next method call will execute both the above scheduled tasks.
* scheduler.dispatchTasks();
* System.out.print("5");
* }
* }
* The above {@code doPrint()} method will always print "12345" and the
* {@code scheduler.dispatchTasks()} call in the scheduled task will actually
* do nothing in this case but return immediately.
*
* The behaviour of this class can also be exploited to "synchronize" actions
* without locking, so even tasks not being synchronization transparent
* and also not thread-safe can be used safely using this class. This is the
* feature what the {@link TaskExecutors#inOrderExecutor(TaskExecutor)} and
* {@link TaskExecutors#inOrderSyncExecutor()} classes provide.
*
*
Dangers of using this class
* At first blink it seems tempting to use this class instead of locks because
* unlike with locks, methods of this class never block and cannot cause
* dead-locks unless the submitted tasks wait for each other. While this is
* true, there are three issues to consider:
*
* -
* Using {@code TaskScheduler} has a higher per task overhead than using a
* lock.
*
* -
* Usually there is no telling in which {@code dispatchTasks()} method will
* a particular task execute. This may add some additional non-determinism,
* making debugging possibly harder.
*
* -
* Using locks cause tasks (and the executing threads) to wait for each other,
* resulting in a natural throttling. {@code TaskScheduler} however maintains
* a list of not yet executed tasks and this can lead to
* {@code OutOfMemoryError} if tasks are scheduled faster than can be
* submitted to the executor by a single thread. Notice that with locks
* the number of such tasks waiting to be executed is limited by the number
* of threads (which should not be too high).
*
*
* Therefore only use this class when locks are not an option. That is, favor
* locks when they are safe to use. One of the good use of this class is to
* notify event listeners.
*
* Thread safety
* The methods of this class are safe to use by multiple threads concurrently.
*
* Synchronization transparency
* Other than the {@link #dispatchTasks() dispatchTasks()} method, methods of
* this class are synchronization transparent. The
* {@code dispatchTasks()} method is not synchronization transparent
* only because it submits tasks to the underlying {@code Executor}. If
* submitting tasks to the underlying executor is
* synchronization transparent then even this method is
* synchronization transparent.
*
* @see TaskExecutors#inOrderExecutor(TaskExecutor)
* @see TaskExecutors#inOrderSyncExecutor()
*/
public final class TaskScheduler {
/**
* A convenience method effectively equivalent to
* {@code new TaskScheduler(SyncTaskExecutor.getSimpleExecutor())}.
*
* @return a new task scheduler with a
* {@code SyncTaskExecutor.getSimpleExecutor()} underlying executor. This
* method never returns {@code null} and always returns a new instance.
*/
public static TaskScheduler newSyncScheduler() {
return new TaskScheduler(Runnable::run);
}
private final Executor executor;
private final AtomicReference dispatcherThread; // null means that noone is dispatching
private final BlockingQueue toDispatch;
/**
* Creates a new task scheduler (without any task scheduled) with the given
* backing executor.
*
* @param executor the executor to which tasks will be submitted to by the
* {@link #dispatchTasks() dispatchTasks()} method. This argument cannot
* be {@code null}.
*
* @throws NullPointerException thrown if the specified executor is
* {@code null}
*/
public TaskScheduler(Executor executor) {
Objects.requireNonNull(executor, "executor");
this.executor = executor;
this.dispatcherThread = new AtomicReference<>(null);
this.toDispatch = new LinkedBlockingQueue<>();
}
/**
* Schedules a single task for submitting to the executor specified at
* construction time.
*
* This method will not actually submit the task, to
* do this call the {@link #dispatchTasks() dispatchTasks()} method.
*
* @param task the task to be scheduled for submitting to the executor
* specified at construction time. This argument cannot be {@code null}.
*
* @throws NullPointerException thrown if the specified task is {@code null}
*/
public void scheduleTask(Runnable task) {
Objects.requireNonNull(task, "task");
toDispatch.add(task);
}
/**
* Schedules a list of tasks for submitting to the executor specified at
* construction time. The tasks will be scheduled in the order defined by
* the given list.
*
* This method will not actually submit the task, to
* do this call the {@link #dispatchTasks() dispatchTasks()} method.
*
* @param tasks the list of tasks to be scheduled for submitting to the
* executor specified at construction time. This argument cannot be
* {@code null} and cannot contain {@code null} elements.
*
* @throws NullPointerException thrown if the specified {@code tasks} is
* {@code null} or contains {@code null} elements
*/
public void scheduleTasks(List extends Runnable> tasks) {
ExceptionHelper.checkNotNullElements(tasks, "tasks");
for (Runnable task: tasks) {
scheduleTask(task);
}
}
/**
* Calling this method ensures that previously scheduled tasks will be
* submitted to the executor specified at construction time. Note that tasks
* may actually be submitted in different {@code dispatchTasks()} method
* call but calling this method ensures that they will be submitted.
*
* In case submitting a task causes an exception to be thrown, the
* {@code dispatchTasks()} method actually submitting that task will
* propagate that exception to the caller. Note however that a single
* {@code dispatchTasks()} call can submit multiple tasks and if more than
* one throws an exception, the exceptions after the first one will be
* suppressed (See: {@link Throwable#addSuppressed(Throwable)}).
*/
public void dispatchTasks() {
if (isCurrentThreadDispatching()) {
// Tasks will be dispatched there.
return;
}
Thread currentThread = Thread.currentThread();
Throwable toThrow = null;
while (!toDispatch.isEmpty()) {
if (dispatcherThread.compareAndSet(null, currentThread)) {
try {
Runnable task = toDispatch.poll();
if (task != null) {
executor.execute(task);
}
} catch (Throwable ex) {
if (toThrow == null) toThrow = ex;
else toThrow.addSuppressed(ex);
} finally {
dispatcherThread.set(null);
}
} else {
return;
}
}
ExceptionHelper.rethrowIfNotNull(toThrow);
}
/**
* Checks whether this method was invoked from a
* {@link #dispatchTasks() dispatchTasks()} method call (i.e.: a task is
* being submitted on the current calling thread).
*
* @return {@code true} if this method was invoked from a
* {@code dispatchTasks()}, {@code false} otherwise
*/
public boolean isCurrentThreadDispatching() {
return dispatcherThread.get() == Thread.currentThread();
}
/**
* Returns the string representation of this {@code TaskScheduler} in no
* particular format. The string representation will contain the number of
* tasks current waiting to be submitted.
*
* This method is intended to be used for debugging only.
*
* @return the string representation of this object in no particular format.
* This method never returns {@code null}.
*/
@Override
public String toString() {
return "TaskScheduler{Tasks to be executed: " + toDispatch.size() + '}';
}
}