Writing in the front

This article will break down the common interview question about Java thread pools, a question that is almost guaranteed to be asked in a Java programmer interview because it is so common in the workplace. Thread pools are the most widely used concurrency framework in Java, and can be used by almost any program that needs to perform tasks asynchronously or concurrently.

This article is based on the question and answer process of the interview. Readers can use this knowledge as a general way to answer this question. If you have mastered the knowledge listed in this article, then this one question will impress the interviewer.

The role of thread pools

When asked if you know anything about thread pools, no doubt you do, so where do you start? It has to be the benefit of using threads, that is, what problems can be solved.

Let’s start with why thread pools are needed, which is the background:

Since creating a thread requires calling the API of the operating system kernel, and then the operating system has to allocate a series of resources to the thread, the cost is very high, so the thread is a heavyweight object, should avoid frequent creation and destruction, so use thread pools

If the above problem is solved, there are three advantages to the thread pool:

  1. Reduce resource consumption;
  2. Improve response speed;
  3. Improving thread manageability;

Implementation principle of thread pool

After answering the question of the need to use thread pools, the main story is how thread pools are implemented.

The thread pool implementation class ThreadPoolExecutor is used to implement thread pooling.

ThreadPoolExecutor(int corePoolSize,
                   int maximumPoolSize,
                   long keepAliveTime,
                   TimeUnit unit,
                   BlockingQueue<Runnable> workQueue,
 ThreadFactory threadFactory,  RejectedExecutionHandler handler) Copy the code

To create a thread pool, the constructor is complex and contains seven parameters, each of which has the following meanings:

  1. corePoolSize: The base size of the thread pool. When the number of threads in the thread pool does not reach the corePoolSize size, a thread is created for each task submitted to the thread pool to execute the task, even if other threads are idle, until the data is equal to the base size of the thread pool. Note that if the thread pool’s prestartAllCoreThreads() method is called, the pool creates and starts all base threads ahead of time.

  2. MaximumPoolSize: Specifies the maximum number of threads that can be created in the thread pool. If a large number of tasks are submitted, the corePoolSize size can be added to the thread pool. As the number of tasks is reduced and no more multithreading is required, it is reduced up to the corePoolSize size.

  3. KeepAliveTime & unit: These two parameters indicate the maximum idle time and time unit for a thread. This means that as the thread pool grows to maximumPoolSize, the task is reduced and the thread is destroyed after the number of keepAliveTime is greater than the corePoolSize.

  4. WorkQueue: a work blocking queue, which means that when the basic size of the thread pool is running, the resubmitted task will be placed in the declared workQueue queue. You can select one of the following queues:

    • ArrayBlockingQueue: ArrayBlockingQueue is a bounded blocking queue based on an array structure that sorts elements in FIFO (first-in, first-out) order.
    • LinkedBlockingQueue: A blocking queue based on a linked list structure that sorts elements in FIFO and typically has a higher throughput than ArrayBlockingQueue. Static factory methodExecutors.newFixedThreadPool()This queue is used.
    • SynchronousQueue: A blocking queue that does not store elements. Each insert operation must wait until another thread calls the remove operation, otherwise the insert operation remains blocked and throughput is usually higher than the Linked-BlockingQueue, static factory methodExecutors.newCachedThreadPoolThis queue is used.
    • PriorityBlockingQueue: An infinite blocking queue with a priority.
  5. ThreadFactory: With this parameter you can customize how threads are created, for example you can give threads meaningful names.

  6. Handler: Saturation policy. When both the queue and the thread pool are full, the thread pool is saturated and a policy must be adopted to handle submitted new tasks. This policy is AbortPolicy by default, indicating that an exception is thrown when a new task cannot be processed. The Java thread pool framework provides the following four strategies in JDK 1.5.

    • AbortPolicy: direct throw an exception, throws RejectedExecutionException.
    • CallerRunsPolicy: The caller’s thread runs the task itself.
    • Discarding the oldest tasks means discarding the earliest tasks and adding new tasks to the work queue.
    • DiscardPolicy: Do not process, discard.

Let’s look at the Java thread pool design and see if you can string together the parameters required to create a thread pool:


When ThreadPoolExecutor executes the execute or Submit method, the thread pool will do something like this:

  1. If there are fewer threads in the current thread pool than corePoolSize, create a new thread to perform the task. Note that this step requires obtaining a global lock.
  2. If the number of threads running in the thread pool is greater than or equal to corePoolSize, the task is added to the workQueue, or blocked queue.
  3. If the blocking queue is also full and the task cannot be added, but the current number of threads is less than the maximum number of threads maximunPoolSize, then a thread is created to execute the task. This step requires obtaining the global lock.
  4. If the number of the current thread has equal maximunPoolSize, then submit the task will be rejected, and invoke RejectedExecutionHandler. RejectedExecution () method;

You can see that thread pools are a producer-consumer model. One side that uses threads is the producer, and the thread pool itself is the consumer. This is because the user is throwing tasks (Runnable/Callable) into the thread pool, and the thread pool itself consumes these submitted tasks.

ThreadPoolExecutor uses the above implementation principle to avoid obtaining a global lock (performance bottleneck) when executing execute, Submit methods, because as long as the number of submitted tasks reaches corePoolSize, Almost all the subsequent execute and submit tasks go to step 2 again, without obtaining the lock. The design is quite awesome.

If you submit a task to a thread pool

Now that we’ve answered how thread pools work, how do we use them? You’ll be able to answer these two ways of submitting tasks to the thread pool, as well as the differences and usage scenarios between them.

There are two ways to submit a task:

  • execute()
  • submit()

The difference is that the execute method is used to submit tasks that do not require a return value. On the one hand, the execute method cannot determine whether the task is successfully executed or obtain the execution result of the thread.

public void execute(Runnable command)

As you can see, execute receives an instance of Runnable, and this method returns no results.

So you might ask, well, there are a lot of scenarios where we need to get the results of a task. In this case, the submit() method is used. ThreadPoolExecutor provides the following three submit methods with the following signatures:

// Submit the Runnable task
Future submit(Runnable task);
// Submit the Callable task
Future submit(Callable task);
// Submit a Runnable task and result reference
Future submit(Runnable task, T result); Copy the code

Submit method will return the Future object, through the Future object’s future.isDone method we can determine whether the task is completed, and get the return value of the task through get() and get(timeout, unit). Both get methods block the current calling thread until the task completes;

The FutureTask utility class implements both the Runnable and Future interfaces, meaning that it can be used to pass tasks to the thread pool or to retrieve the results of child threads.

The following is an example:

/ / create FutureTask
FutureTask futureTask = new FutureTask<>(()->"This is the return result." );
// Create a thread pool
ExecutorService es = Executors.newCachedThreadPool();
/ / submit FutureTask
es.submit(futureTask); // get the result String result = futureTask.get(); Copy the code

Closing the thread pool

To shutdown a thread pool, we can use the shutdown and shutdownNow methods of the thread pool. Both work by iterating through threads in a thread pool and calling the thread_interrupt method one by one to interrupt the thread. But just calling the interrupt method does not mean that the thread will terminate, as long as the task it is executing can respond to the interrupt. Otherwise, it may never terminate. If you are not familiar with thread interruption, you can take a look at this guide: a thread interruption caused by the Bug “liver explosion” investigation experience

However, there are some differences between the two methods. After the shutdownNow method is called, it first sets the state of the thread pool to STOP, then calls interrupt methods for all threads in the pool (including running threads) and returns a list of tasks in the queue waiting to be executed. The shutdown method simply sets the thread state to shutdown and then calls the interrupt method of the idle thread in the thread pool.

How are thread pool parameters configured

By the time you’ve answered all of the above thread pool questions, you’re probably pretty much done, but now that you know how it works, how does it work?

You need to think clearly about this question. This number is definitely not a headshot. Don’t rush to give a number, but start from the scene:

To make proper use of thread pools, you should first analyze the nature of the task, whether it is CPU intensive or IO intensive.

  1. For CPU-intensive tasks, the number of threads should be as small as possible. Generally, the number of cpus +1 thread thread pool should be configured.
  2. IO intensive tasks, because the task is not always executing, should be allocated as many threads as possible, generally configure 2*CPU number of threads;

To get the number of cpus on a machine, we can use Runtime.getruntime ().availableprocessors ();

If the task to be processed has a priority, use the blocking queue PriorityBlockingQueue as the work queue, with the highest priority being executed first.

It is worth mentioning that using a thread pool is recommended to use bounded queue, because can increase the stability of the system, because before we use the Executors newFixedThreadPool () to create a thread pool, Its default use of unbounded LinkedBlockingQueue results in a backlog of tasks, frequent online FGC in the event of a database exception, and eventually memory overruns and the entire service becomes unavailable. When changed to a bounded work queue, task abandonment exceptions are constantly thrown, which is easy for monitoring to detect and does not make the entire service unusable, just thread task exceptions.

conclusion

This article is based on the interview scenario, for the Java thread pool analysis, I believe that if you can understand the content of this article, the future interview encountered Java thread pool this question will not be discouraged, and confident.

Pay attention to my

Public number: seven elder brother love programming