What Are the Advantages of Using an Executorservice

What are the advantages of using an ExecutorService?

ExecutorService abstracts away many of the complexities associated with the lower-level abstractions like raw Thread. It provides mechanisms for safely starting, closing down, submitting, executing, and blocking on the successful or abrupt termination of tasks (expressed as Runnable or Callable).

From JCiP, Section 6.2, straight from the horse's mouth:

Executor may be a simple interface, but it forms the basis for a flexible and powerful framework for asynchronous task execution that supports a wide variety of task execution policies. It provides a standard means of decoupling task submission from task execution, describing tasks as Runnable. The Executor implementations also provide lifecycle support and hooks for adding statistics gathering, application management, and monitoring.
...
Using an Executor is usually the easiest path to implementing a producer-consumer design in your application.

Rather than spending your time implementing (often incorrectly, and with great effort) the underlying infrastructure for parallelism, the j.u.concurrent framework allows you to instead focus on structuring tasks, dependencies, potential parallelism. For a large swath of concurrent applications, it is straightforward to identify and exploit task boundaries and make use of j.u.c, allowing you to focus on the much smaller subset of true concurrency challenges which may require more specialized solutions.

Also, despite the boilerplate look and feel, the Oracle API page summarizing the concurrency utilities includes some really solid arguments for using them, not least:

Developers are likely to already
understand the standard library
classes, so there is no need to learn
the API and behavior of ad-hoc
concurrent components. Additionally,
concurrent applications are far
simpler to debug when they are built
on reliable, well-tested components.

Java concurrency in practice is a good book on concurrency. If you haven't already, get yourself a copy. The comprehensive approach to concurrency presented there goes well beyond this question, and will save you a lot of heartache in the long run.

Advantages of Executors over new Thread

Yes, executors will generally multiplex runnables onto the threads they create; they'll constrain and manage the number of threads running at once; they'll make it much easier to customize concurrency levels. Generally, executors should be preferred over just creating bare threads.

ExecutorService vs Casual Thread Spawner

While the question and the sample code do not correlate, I'll try clarifying both.

The advantage of ExecutorService over haphazardly spawning threads is that it behaves predictably and avoids the overhead of thread creation, which is relatively big on the JVM (it needs to reserve memory for each thread, for example).

By predictability, I mean you can control the number of concurrent threads, and you know when and how they might get created and destroyed (so your JVM won't blow up in case of sudden peaks, and threads won't be left abandoned leaking memory). You can pass an ExecutorService instance around so that various parts of your program can submit tasks to it, while you still manage it in a single location, fully transparently. An ExecutorService can also be precisely scoped, and shut down when the scope is exited (via shutdown()).

A fixedThreadPool uses a pool of threads that won't grow beyond what it is allocated.

A cachedThreadPool doesn't have a max, but will reuse cached threads for a period of time. It's mostly used in cases where many small tasks need to be executed on a separate thread.

A singleThreadExecutor is for async tasks executed serially.

There are others, like newScheduledThreadPool for periodically repeating tasks, newWorkStealingPool for tasks that can be forked into subtasks and a few others. Explore the Executors class for the details.

As of JVM 18, virtual threads are a thing, and these can be created cheaply, so they change the picture significantly. An ExecutorService that creates a new virtual thread each time can be obtained via newVirtualThreadPerTaskExecutor. The advantage of using this versus spawning threads manually is not performance-centric as much as structural in that it allows for scoping and other benefits explained above, and interoperability with existing APIs that expect an ExecutorService.

Now, on the topic of Runnable vs Callable, it is easy to see from your examples. Callables can return a value place-holder (Future) that will eventually be populated by an actual value in the future. Runnables can not return anything. Additionally, a Runnable also can't throw exceptions, while a Callable can.

SingleThreadExecutor VS plain thread

Executors#newSingleThreadExecutor() creates ThreadPoolExecutor object under the hood,

see the code here: http://www.docjar.com/html/api/java/util/concurrent/Executors.java.html

  133       public static ExecutorService newSingleThreadExecutor() {
134 return new FinalizableDelegatedExecutorService
135 (new ThreadPoolExecutor(1, 1,
136 0L, TimeUnit.MILLISECONDS,
137 new LinkedBlockingQueue<Runnable>()));
138 }

The documentation of ThreadPoolExecutor explains in what situations it gives advantages:

Thread pools address two different problems: they usually provide
improved performance when executing large numbers of asynchronous
tasks
, due to reduced per-task invocation overhead, and they provide a
means of bounding and managing the resources, including threads,
consumed when executing a collection of tasks. Each ThreadPoolExecutor
also maintains some basic statistics, such as the number of completed
tasks.

If all you need is to just run single thread only once in a while (say once an hour), then in terms of performance, using ThreadPoolExecutor may be slower, since you need to instantiate the whole machinery (pool + thread), then throw it away from memory.

But if you want to use this single thread often (say every 15 seconds), then the advantage is that you create the pool and thread only once, keeping it in memory, and use it all the time saving time creating a new thread every now and then (which might be quite expensive, if you want to use it say every 15 seconds or so).

What's the advantage of a Java-5 ThreadPoolExecutor over a Java-7 ForkJoinPool?

ThreadPool (TP) and ForkJoinPool (FJ) are targeted towards different use cases. The main difference is in the number of queues employed by the different executors which decide what type of problems are better suited to either executor.

The FJ executor has n (aka parallelism level) separate concurrent queues (deques) while the TP executor has only one concurrent queue (these queues/deques maybe custom implementations not following the JDK Collections API). As a result, in scenarios where you have a large number of (usually relatively short running) tasks generated, the FJ executor will perform better as the independent queues will minimize concurrent operations and infrequent steals will help with load balancing. In TP due to the single queue, there will be concurrent operations every time work is dequeued and it will act as a relative bottleneck and limit performance.

In contrast, if there are relatively fewer long-running tasks the single queue in TP is no longer a bottleneck for performance. However, the n-independent queues and relatively frequent work-stealing attempts will now become a bottleneck in FJ as there can be possibly many futile attempts to steal work which add to overhead.

In addition, the work-stealing algorithm in FJ assumes that (older) tasks stolen from the deque will produce enough parallel tasks to reduce the number of steals. E.g. in quicksort or mergesort where older tasks equate to larger arrays, these tasks will generate more tasks and keep the queue non-empty and reduce the number of overall steals. If this is not the case in a given application then the frequent steal attempts again become a bottleneck. This is also noted in the javadoc for ForkJoinPool:

this class provides status check methods (for example getStealCount())
that are intended to aid in developing, tuning, and monitoring
fork/join applications.

Java Timer vs ExecutorService?

According to Java Concurrency in Practice:

  • Timer can be sensitive to changes in the system clock, ScheduledThreadPoolExecutor isn't.
  • Timer has only one execution thread, so long-running task can delay other tasks. ScheduledThreadPoolExecutor can be configured with any number of threads. Furthermore, you have full control over created threads, if you want (by providing ThreadFactory).
  • Runtime exceptions thrown in TimerTask kill that one thread, thus making Timer dead :-( ... i.e. scheduled tasks will not run anymore. ScheduledThreadExecutor not only catches runtime exceptions, but it lets you handle them if you want (by overriding afterExecute method from ThreadPoolExecutor). Task which threw exception will be canceled, but other tasks will continue to run.

If you can use ScheduledThreadExecutor instead of Timer, do so.

One more thing... while ScheduledThreadExecutor isn't available in Java 1.4 library, there is a Backport of JSR 166 (java.util.concurrent) to Java 1.2, 1.3, 1.4, which has the ScheduledThreadExecutor class.

When should I use a CompletionService over an ExecutorService?

With ExecutorService, once you have submitted the tasks to run, you need to manually code for efficiently getting the results of the tasks completed.

With CompletionService, this is pretty much automated. The difference is not very evident in the code you have presented because you are submitting just one task. However, imagine you have a list of tasks to be submitted. In the example below, multiple tasks are submitted to the CompletionService. Then, instead of trying to find out which task has completed (to get the results), it just asks the CompletionService instance to return the results as they become available.

public class CompletionServiceTest {

class CalcResult {
long result ;

CalcResult(long l) {
result = l;
}
}

class CallableTask implements Callable<CalcResult> {
String taskName ;
long input1 ;
int input2 ;

CallableTask(String name , long v1 , int v2 ) {
taskName = name;
input1 = v1;
input2 = v2 ;
}

public CalcResult call() throws Exception {
System.out.println(" Task " + taskName + " Started -----");
for(int i=0;i<input2 ;i++) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
System.out.println(" Task " + taskName + " Interrupted !! ");
e.printStackTrace();
}
input1 += i;
}
System.out.println(" Task " + taskName + " Completed @@@@@@");
return new CalcResult(input1) ;
}

}

public void test(){
ExecutorService taskExecutor = Executors.newFixedThreadPool(3);
CompletionService<CalcResult> taskCompletionService = new ExecutorCompletionService<CalcResult>(taskExecutor);

int submittedTasks = 5;
for (int i=0;i< submittedTasks;i++) {
taskCompletionService.submit(new CallableTask (
String.valueOf(i),
(i * 10),
((i * 10) + 10 )
));
System.out.println("Task " + String.valueOf(i) + "subitted");
}
for (int tasksHandled=0;tasksHandled<submittedTasks;tasksHandled++) {
try {
System.out.println("trying to take from Completion service");
Future<CalcResult> result = taskCompletionService.take();
System.out.println("result for a task availble in queue.Trying to get()");
// above call blocks till atleast one task is completed and results availble for it
// but we dont have to worry which one

// process the result here by doing result.get()
CalcResult l = result.get();
System.out.println("Task " + String.valueOf(tasksHandled) + "Completed - results obtained : " + String.valueOf(l.result));

} catch (InterruptedException e) {
// Something went wrong with a task submitted
System.out.println("Error Interrupted exception");
e.printStackTrace();
} catch (ExecutionException e) {
// Something went wrong with the result
e.printStackTrace();
System.out.println("Error get() threw exception");
}
}
}
}


Related Topics



Leave a reply



Submit