preface

After PK with Erdog’s “The strongest HashMap” last time, Erdog has always been bitter and often pestered me to revenge, so “Room 11, 11th floor, Single Dog Building” was another bloody storm.

The body of the

Why use thread pools? Isn’t it comfortable to just create a new thread?

If we directly new a thread in the method to handle, when this method is called frequently will create many threads, not only will consume system resources, but also reduce the stability of the system, accidentally crashed the system, you can directly go to the accounting that checkout.

If we use thread pools wisely, we can avoid crashing the system. In summary, there are several benefits to using thread pools:

  1. Reduce resource consumption. Reduce the cost of thread creation and destruction by reusing created threads.

  2. Improve response speed. When a task arrives, it can be executed immediately without waiting for the thread to be created.

  3. Add manageability to threads. Threads are a scarce resource that can be uniformly allocated, tuned, and monitored using thread pools.

What are the core attributes of the thread pool?

ThreadFactory: A factory used to create worker threads.

CorePoolSize (number of core threads) : When the thread pool runs with fewer threads than corePoolSize, a new thread is created to handle the request, even if other worker threads are idle.

WorkQueue: A blocking queue used to hold tasks and hand them off to worker threads.

MaximumPoolSize: The maximum number of threads allowed to open in the thread pool.

Handler (reject policy) : When adding tasks to the thread pool, the reject policy is triggered in one of the following situations: 1. 2) The thread pool has reached the maximum number of threads and the blocking queue is full.

KeepAliveTime (keepAliveTime) : if the number of threads in the thread pool exceeds corePoolSize, additional threads will be terminated if their idle time exceeds keepAliveTime.

Dog 2: Talk about how thread pools work

Let me draw you a picture.

What does each state in the thread pool mean?

Thread pools currently have five states:

  • RUNNING: Accept new tasks and handle queuing tasks.

  • SHUTDOWN: does not accept new tasks, but processes queued tasks.

  • STOP: does not accept new tasks, does not process queued tasks, and interrupts ongoing tasks.

  • TIDYING: All tasks terminated, workerCount is zero, and thread transitions to TIDYING state will run terminated() hook method.

  • TERMINATED: TERMINATED () is complete.

Two dog: how is the circulation between these several states?

I’ll draw you another picture. Watch!

What queues does the thread pool have?

Common blocking queues are the following:

ArrayBlockingQueue: A bounded blocking queue based on an array structure that sorts elements on a first-in, first-out basis.

LinkedBlockingQueue: A bounded/unbounded blocking queue based on a linked list structure that sorts elements on a first-in, first-out basis and typically has a higher throughput than ArrayBlockingQueue. Executors. NewFixedThreadPool USES the queue.

SynchronousQueue: Not a true queue, but a mechanism for hand-over between threads. To place an element in the SynchronousQueue, another thread must be waiting to accept the element. If there are no threads waiting and the current size of the thread pool is less than the maximum, a thread will be created by the thread pool, otherwise the task will be rejected according to the rejection policy. It is more efficient to use direct hand-off because the task is handed over directly to the thread executing it, rather than being queued and then pulled from the queue by a worker thread. The queue is of real value only if the thread pool is unbounded or can reject tasks. Executors. NewCachedThreadPool USES the queue.

PriorityBlockingQueue: An unbounded queue with a priority that sorts elements by priority. The priority of an element is defined by a natural order or Comparator.

Q: What should I pay attention to when using queues?

When using bounded queues, you need to pay attention to how rejected tasks are handled when the thread pool is full.

When using unbounded queues, note that memory may overflow if the task is submitted faster than the processing speed of the thread pool.

What are the rejection policies of the thread pool?

The following are common:

AbortPolicy: aborts the policy. The default refusal strategies, throw RejectedExecutionException directly. Callers can catch this exception and then write their own handling code as needed.

DiscardPolicy: Discards a policy. Do nothing and simply drop the rejected assignment.

DiscardOldestPolicy: Discards the oldest policy. Discarding the oldest task in the blocking queue is equivalent to the next task in the queue to be executed, and then resubmitting the rejected task. If the blocking queue is a priority queue, the “discard oldest” policy causes the highest priority task to be discarded, so it is best not to use this policy together with the priority queue.

CallerRunsPolicy: The caller runs the policy. The task is performed in the caller thread. The strategy implements a regulating mechanism, this strategy can neither abandon the task, also won’t throw an exception, but the task back to the caller (the main thread of the calling thread pool mission), because it takes time to perform a task, at least for a period of time so the main thread can’t submit a task, thus making the thread pool has time to finish executing task processing.

Dog 2: Can threads only start when tasks arrive?

By default, even core threads are created and started only when a new task arrives. However, we can use the prestartCoreThread (start one core thread) or prestartAllCoreThreads (start all core threads) methods to start the core thread ahead of time.

Two dogs: how to achieve the core thread has been alive?

There are four forms of blocking queue methods that handle operations in different ways, as shown in the table below.

An exception is thrown

Return special value

Has been blocked

Timeout exit

insert

add(e)

offer(e)

put(e)

offer(e,time,unit)

remove

remove()

poll()

take()

poll(time,unit)

check

element()

peek()

Do not use

Do not use

The core thread keeps blocking (staying alive) by blocking the queue’s take() method while it gets the task.

How do non-core threads die after keepAliveTime?

The principle is the same as above, which also uses the method of blocking queue to realize the delayed death through the poll(time,unit) method of blocking queue when obtaining tasks.

Can a non-core thread become a core thread?

While we’ve been talking about core and non-core threads, there’s actually no distinction between core and non-core threads in the thread pool. It just adjusts to the number of worker threads currently in the thread pool, so it looks like there are core threads and non-core threads.

How do I terminate the thread pool?

There are two main ways to terminate a thread pool:

Shutdown: Gently closes the thread pool. No new tasks are accepted, but previously submitted tasks are processed before closing.

ShutdownNow: A “rude” closing of the thread pool, that is, shutting down the thread pool directly, terminating all threads with the Thread#interrupt() method without waiting for previously committed tasks to complete. But the unprocessed tasks in the queue are returned.

Two dogs :(rough? Sure, BUT I love it.) So, what do you do to create a thread pool by Executors?

NewFixedThreadPool: a thread pool with a fixed number of threads. CorePoolSize = maximumPoolSize, keepAliveTime is 0, and work queues use unbounded LinkedBlockingQueue. This method is applicable to scenarios where the number of threads needs to be limited to meet resource management requirements. This method is applicable to heavily loaded servers.

NewSingleThreadExecutor: thread pool with only one thread. CorePoolSize = maximumPoolSize = 1, keepAliveTime is 0, work queues use unbounded LinkedBlockingQueue. This method is applicable to scenarios where tasks need to be executed in sequence.

NewCachedThreadPool: Thread pool to create new threads as needed. The number of core threads is 0, the maximum number of threads is intege.MAX_VALUE, the keepAliveTime is 60 seconds, and the work queue uses synchronousTransition queue. This thread pool can expand indefinitely, adding new threads when demand increases, and automatically reclaiming idle threads when demand decreases. It is ideal for performing a lot of short asynchronous tasks, or for servers with light loads.

NewScheduledThreadPool: Creates a thread pool that executes tasks in a deferred or timed manner, with the work queue listed as DelayedWorkQueue. Suitable for periodic tasks that require multiple background threads.

NewWorkStealingPool: New in JDK 1.8 for creating a stealable thread pool with the underlying ForkJoinPool implementation.

There is a CTL in the thread pool. Do you know how it is designed?

CTL is an atomic integer that wraps two concept fields.

1) workerCount: indicates the number of valid threads;

2) runState: indicates the state of the thread pool. The state is RUNNING, SHUTDOWN, STOP, TIDYING, and TERMINATED.

The int type has 32 bits. The lower 29 bits of CTL are used to represent workerCount and the higher 3 bits are used to represent runState, as shown in the following figure.

For example, when the RUNNING status of our thread pool is RUNNING and the number of worker threads is 3, then the source code of CTL is 1010 0000 0000 0000 0000 0000 0011

Two dogs :(this boy looks by secretly prepared, it seems to have to take out the bottom of the box of the topic) CTL why so designed? Are there any benefits?

In my opinion, the main benefit of CTL design is to encapsulate the operations on runState and workerCount into one atomic operation.

RunState and workerCount are the two most important properties for a thread pool to function properly, and what the pool should do at any given moment depends on the values of these two properties.

So whether we query or modify, we have to make sure that the operations on these two attributes are “simultaneous”, that is, atomic operations, otherwise we will get confused. If we store two variables separately, the atomicity requires additional locking, which obviously incurs additional overhead, whereas wrapping the two variables into one AtomicInteger does not incur additional locking overhead. And you can get runState and workerCount, respectively, using simple bit manipulation.

CAPACITY = (1 << 29) -1, 0001 1111 1111 1111 1111 1111 1111 1111 1111 1111 1111

To get runState from CTL, just do the bit operation: CTL & ~CAPACITY.

~ (by invert), and “~ CAPACITY” value is: 1110 0000 0000 0000 0000 0000 0000 0000, only high 3 to 1, with CTL & operation, the results for CTL high values of the three is the runState.

It is easier to get the workerCount by CTL, just by bit operation: c & CAPACITY.

Two dog: young man good good, that I finally ask a, in our actual use, the size of the thread pool configuration how much appropriate?

To properly configure the thread pool size, we first need to distinguish between computationally and I/O intensive tasks.

For computationally intensive cases, optimal utilization is usually achieved by setting the number of threads = number of cpus + 1.

For I/O intensive, the common saying on the Internet is to set the number of threads = number of cpus * 2. This is ok, but I think it is not optimal.

During the development of our everyday, our task is almost inseparable from the I/O, common network I/O call (RPC), disk I/O operations (database), and I/O waiting time usually accounted for a large part of the whole task processing time, in this case, open more threads can be more fully to the CPU use, A more reasonable calculation formula is as follows:

Number of threads = Number of cpus x CPU usage * (Task waiting time/Task calculation time + 1)

For example, if we have a scheduled task deployed on a 4-core server, and the task has 100ms of computing and 900ms of I/O waiting, the number of threads is about 4 x 1 x (1 + 900/100) = 40.

Of course, specific we should combine actual use scenario to consider.

The harder you work, the luckier you get. I am Jiong Hui, dedicated to sharing Java core knowledge, high-frequency interview questions, personal learning and growth experience.

Recommended reading

921 days, from small factory to ali

Two years of Java development work experience interview summary

4 years Java experience interview summary and experience

Interview Ali, HashMap is enough

Do you understand CAS?

What HAVE I done to get the offer of Meituan in two months