Executor framework implements thread pooling through an
Executor interface. Using it is fairly intuitive and draws on already pre-existing functionality found in the Thread class, and the executor framework provides additional interfaces for various implementation strategies.
Executor interfaces include:
Executor: launch a
ExecutorService: manages the lifecycle of tasks in a sub-interface of
ScheduledExecutor: schedules the execution of tasks in a sub-interface of
Let’s take a look at how we’ll be implementing an executor into our code. We’ll need the following imports:
import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors;
Additionally, since we need the tasks we enqueue into the thread pool to be
Runnable objects, we’ll be creating a
Runnable class accordingly. To use an executor, we need to create an
ExecutorService object and pass it the number of threads we’ll be allotting it:
private static final int N = 10; ExecutorService executor = Executors.newFixedThreadPool(N);
Since the thread pool is responsible for those threads, it will automatically handle creating those and managing how they run. We just need to tell it how many to create and handle. At this point, all we need to do now is pass tasks to the executor, which we do like this (as usual, we’ll probably be putting this into a loop much like how we loop the creation and assignment of manual threads):
Runnable task = new RunnableTask(); executor.execute(task);
In the above,
RunnableTask is the custom class you’ll be creating that implements
executor.execute(task) will enqueue the newly created
Runnable object into the thread pool, which will be processed by one of the waiting threads. Now, thanks to the
ExecutorService interface, we have some useful methods we can call to interact with the working threads.
If we want to prevent any new tasks from being added to our executor, we can call
executor.shutdown(). In this case, the threads will still work and clear out the queue, but this will ensure nothing new gets added after this point.
If we want to wait for the thread pool to finish executing everything in its queue, we can call
There are plenty more useful methods in
ExecutorService, but we’ll be using just these in our basic executor. Let’s get some practice with this!
We’re going to make a simple program that makes use of the Executor framework and visualize how the threads juggle a pooled queue.
Let’s start with the runnable task we’ll be creating and adding to the pool.
In RunnableTask.java, implement
Runnablerequires all implementing classes to include a method called
public void run(), so go ahead and add that with an attached
Now at the top of the class, create a
private final longvariable called
limitand set its value inside the constructor using a
run(), sum up the values from 1 to our passed in
limit. Then print out the final computed sum.
Let’s build our main runner now that creates these runnable tasks and houses the executor.
In Main.java, add the following imports:import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit;
private static final intcalled
Nand set its value to 10. This will be the number of threads that will populate the thread pool.
mainmethod, create an executor with the following line:ExecutorService executor = Executors.newFixedThreadPool(N);
forloop that loops from
500. In each iteration of the loop, create an instance of our
RunnableTaskwith the parameter
10000000L + i. Then after each task creation, add it to the executor by calling:executor.execute(task);
We want to make sure we wait for the executor to finish and also prevent the executor from accepting new tasks at this point, so after the
forloop we want to add these lines:executor.shutdown(); executor.awaitTermination(30, TimeUnit.SECONDS); System.out.println("Finished all threads");
shutdown() is what prevents new tasks from being enqueued. The
awaitTermination() method will wait for an allotted amount of maximum seconds for the tasks to finish, and if they finish before that time has passed it won’t throw an error. However, if the time passes and the threads still aren’t done, an error will be thrown.
mainto throw this error should it happen.
Run your code and check out the output.
To get a better visualization of how the threads are juggling the pooled queue, go back into RunnableTask.java and print out the current running thread’s name on the same line as the sum is being printed.
You can format it as follows:
[thread name]: [sum]
Run Main.java again and take note of the random order in which the pooled threads finish their tasks.