The 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 aRunnable
object taskExecutorService
: manages the lifecycle of tasks in a sub-interface ofExecutor
ScheduledExecutor
: schedules the execution of tasks in a sub-interface ofExecutorService
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 Runnable
. Calling 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 executor.awaitTermination()
.
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!
Instructions
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
Runnable
.Runnable
requires all implementing classes to include a method calledpublic void run()
, so go ahead and add that with an attached@Override
tag.Now at the top of the class, create a
private final long
variable calledlimit
and set its value inside the constructor using along
parameter.In
run()
, sum up the values from 1 to our passed inlimit
. 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;Declare a
private static final int
calledN
and set its value to 10. This will be the number of threads that will populate the thread pool.Inside the
main
method, create an executor with the following line:ExecutorService executor = Executors.newFixedThreadPool(N);Make a
for
loop that loops from0
to500
. In each iteration of the loop, create an instance of ourRunnableTask
with the parameter10000000L + 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
for
loop we want to add these lines:executor.shutdown(); executor.awaitTermination(30, TimeUnits.SECONDS); System.out.println("Finished all threads");
Calling 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.
- Add
throws InterruptedException
tomain
to 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.