Creating a new Thread for every task is not an ideal approach for managing asynchronous work. The number of threads and associated resources can quickly get out of hand. One way to improve upon this is to make use of ExecutorService. With ExecutorService, we can submit tasks to be run asynchronously and let ExecutorService make decisions about how to manage which threads the tasks are run on.
We can define a specific number of threads to create and reuse for ExecutorService. This limits the amount of resources used, and makes it easier to understand the potential performance impacts of our code.
Creating ExecutorService with a fixed Thread count can be done quite simply, as shown in the following snippet:
val executor = Executors.newFixedThreadPool(3)
We can leverage the newFixedThreadPool() factory method to create ExecutorService with a specific thread count, in this case, 3.
Once we have an executor available, we can use that Executor to submit our tasks to be run in the background:
fun threadPoolExample() {
executor.submit {
Thread.sleep(500) // simulate network request
println("Runnable 1")
}
executor.submit {
Thread.sleep(200) // simulate db access
println("Runnable 2")
}
executor.submit {
Thread.sleep(300) // simulate network request
println("Runnable 3")
}
}
In this example, the executor will use its available thread pool to schedule the submitted Runnable tasks and execute the work. Upon execution of this code, we should see the following output:
Runnable 2
Runnable 3
Runnable 1
In this case, the first Runnable we submit to the Executor completes last because it delays its Thread for the longest duration. This simulates a long-running task such as a network request.
Because we've created this Executor with a thread pool of size 3, we can be sure that no more than three threads will be created and running at any given time, thereby limiting the amount of system resources available to run our async code.