Create a wheel thread pool

Time:2020-1-14

I’ve seen the source code of thread pool for a long time (I know the general operation principle), but I just know how to use it, and I didn’t go deep into it. In order to help me understand the thread pool in depth, I decided to write a simple (crude) thread pool manually and record the process of thinking and wheel building.

Although it is unlikely to be as perfect as the one provided by the JDK, the functions it should have are as follows:

  • New thread pool, with core threads and maximum threads, thread lifetime, queue
  • Add threads to the process pool. If the current number of threads does not exceed the number of core threads, a new thread will be created. If the number exceeds the number of core threads, a new thread will be created when the queue is full, reaching the maximum thread
  • After all threads run, the number of core threads will be reserved to support thread lifetime
  • Close thread pool now
  • Graceful shutdown of thread pool
1. Create a new wheel thread pool class, just a constructor, and pass in all the required parameters

Create a wheel thread pool

2. When the ThreadPoolExecutor is used, the new thread pool will submit threads to it. The same is true for what we write. When we add threads to the thread pool, we will judge whether the current number of running threads is greater than the number of core threads and the maximum number of threads. A variable of the current number of running threads is needed.

Therefore, a member variable activecount is added here, with the initial value of 0. When running a thread, it will be increased by 1, and when running a thread, it will be decreased by 1. In this case, it is in different threads, so for thread safety, atomicinteger type is used

/**Number of currently active threads*/
    private AtomicInteger activeCount = new AtomicInteger(0);

After reading the ThreadPoolExecutor source code, we should know that each thread in the thread submission method is wrapped with a worker class. When a new thread is added, a new worker will be created. Why do we do this?
The first time I saw it, I didn’t understand. I thought about how to start the incoming thread directly. If I created a new thread directly, I would immediately find out the problem. How to know what thread is running and how to reduce the activecount by 1?
So we can’t start directly here. We must create a new thread (asynchronous execution, nonsense). This thread must run the thread run method of the parameter (nonsense, otherwise, what’s the use of the parameter, how to execute our business logic). After the thread runs, the activecount is reduced by 1.

So here is the worker class, and the worker itself is a thread. By the way, the thread name is also solved. To create a new threadnum, start from 0 and add a worker + 1

Create a wheel thread pool

Commit thread

Create a wheel thread pool

3. Now it’s a little bit like this, but there are still many problems. For example, I built a thread pool with core = 2, max = 5, queue = 5. If I put 8 threads in the pool, there will be only 3 threads running, and the running will end. The remaining 5 threads will not be processed in the queue, and 2 core threads will not be reserved

Create a wheel thread pool

Results of wheelthreadpool2

Create a wheel thread pool

Now I want to think about how to run the threads in the queue. Before looking at the source code of ThreadPoolExecutor, I thought for the first time whether to start a thread by default to get and execute in the queue when creating the thread pool. However, I immediately denied that because the thread pool is multi-threaded, the threads in the queue need to execute at the same time with max (maximum number of threads in the parameter).
So in our new worker, we need to be able to get the threads of the queue to execute in a continuous loop. If the queue is empty, exit the loop and let the thread end

Modify the worker’s run method. After the worker thread created by the execute method executes the runnable passed in through the parameter, cycle to get the queue and execute the run method of the queue thread

Create a wheel thread pool

There is also a problem. If there is an exception in try, such as runnable.run exception or r.run exception, the thread exits and cannot keep Max threads executing in parallel

So if it’s abnormal, you need to create a thread again to continue running. After the transformation

Create a wheel thread pool

In this way, if the queue is empty, all threads will be terminated. So now we need to solve the problem of retaining core threads after the queue is executed. How to keep threads is actually realized by blocking the queue,

When the queue is empty, block the current thread through the queue. Take () method until another thread submits. If the current active thread exceeds the core, end the current thread

Create a wheel thread pool

After this transformation, the outline is outlined. Because the queue is a blocking queue and each method has thread locks, it is thread safe. It seems that this part of code does not need to be locked. It seems reasonable to run a test case

Create a wheel thread pool

Wheelthreadpooltest3 results

Create a wheel thread pool

First, let’s take a look at the next function. To support thread survival time, the meaning of this survival time is: for example, the thread pool in wheelthreadpooltest3 above has run 10 threads. After running, the remaining 2 threads, and the 3 threads have died (completed the task).
After that, submit another 10 threads and create three new threads (as can be seen from the thread name of the console). If we set a survival time, the three threads after the first batch of 10 runs will not die. For example, the survival time is 5 seconds. The 10 threads of the second batch can be reused when they run, and there is no need to re create threads.
Because threads are scarce resources, they can be reused if they can be reused, and new threads also affect efficiency

The current mark of code thread death is that queue.poll gets null, which causes the loop to exit and the thread to complete. The poll method of blocking queue has a polymorphic methodE poll(long timeout, TimeUnit unit), poll can be obtained in a certain period of time, and it will return when it is obtained. This time is just used to be the thread’s survival time (death countdown).

The construction method has passed the survival time and unit, and these two parameters are added directly

Create a wheel thread pool

In the next test, the survival time is set to 5 seconds, so the second batch can only submit 5 threads, otherwise the thread pool will be slow

Create a wheel thread pool

Result

Create a wheel thread pool

There is a big problem. At last, the thread is gone, and the main thread exits

Reason: after the execution of the first batch of 10 threads, because the threads live for 5 seconds, they are all reserved

Create a wheel thread pool

Create a wheel thread pool

Create a wheel thread pool

The stack print also confirms that all of them are blocked in the poll, and then the second batch of 5 threads have been submitted. The surviving 5 threads will start executing immediately, and block again after execution. After the survival time, all threads will end!

Print the stack again after the second batch of execution, and the result is as expected

Create a wheel thread pool

Create a wheel thread pool

The problem is solved

Create a wheel thread pool
Create a wheel thread pool

The lock here is a thread lock to prevent multiple threads from judging at the same time (at the same time, does it make sense to write this.)

/**Line lock*/
   private Lock lock = new ReentrantLock();

So far, the main part has been written and tested

Create a wheel thread pool

But there’s a place where I read the code by myself. I think it’s a problem

Create a wheel thread pool

I feel that there will be thread safety problems here. Assuming that the thread pool queue is empty, the current active count is greater than the core. In the case of concurrency, multiple threads meet the requirements at the same timeactiveCount.get()>coreCount, and then all threads will take the queue.poll branch. Because the queue is empty, all threads queue.poll return null, and all threads end, which conflicts with the reservation of core threads.
After a long struggle, it was found that this assumption was not true, and the queue was empty before entering the whileactiveCount>core, this situation will not occur, because the method (execute) in the submission thread has limited this situation, but the code looks ambiguous, or it decides to modify it

Create a wheel thread pool

This time, I feel much better. Finally, I have finished the main functions and can run normally

4. Close thread pool immediately

The threads in the thread pool run out, but there are still core threads blocked, so it’s not the way to block all the time, so there should be a way to close. First write the brute force shutdown, the current running thread is interrupted, and the queue is abandoned

Think time: interrupt thread is definitely a callThread.interruptMethod, so I have to get the running thread, so when I add a new thread, I have to save it in a collection, and when the thread is executing abnormally, it will also add a new thread, so this collection should be thread safe, and the access speed should be fast
A thread safe set set is needed here,ConcurrentHashMapThere’s a inside.newKeySetMethod. After reading the source code, it comes from the key of concurrenthashmap. It is thread safe and can be used directly

/**Save running threads*/
   private Set<Worker> workers = ConcurrentHashMap.newKeySet();

A state is also needed to identify whether the current thread pool is closed. This state can be judged when threads are concurrent (get queue threads). Therefore, it is decorated with volatile and running by default

/**Thread pool state, - 1: running, 0: violent shutdown, 1: graceful shutdown*/ 
   private volatile int status = -1;

New violence closure methodstopNow

Create a wheel thread pool

When you create a new worker, add it to workers. At this time, think about whether activecount and workers.size are duplicate. Delete activecount by the way, replace it with workers.size, and finish workers.remove by thread execution

New method addworker

Create a wheel thread pool

Submit thread method transformation, delete activecount, replace with workers.size

Create a wheel thread pool

In the transformation of worker’s run method, activecount is deleted, workers.size is used instead, workers.remove (this) is used for activecount-1

Create a wheel thread pool

After the transformation, check that violent shutdown needs to immediately interrupt the thread and discard the queue, so judgment should be added in the while queue

Create a wheel thread pool

Submit thread method to add state judgment

Create a wheel thread pool

Transformation completed, violent closure under test

Create a wheel thread pool

As a result, the whole program exits, the thread pool ends, and the queue only runs one thread

Create a wheel thread pool

5. Close the thread pool gracefully

To close the thread pool gracefully, you need to let all threads and queues run and then close all threads. In this way, you can’t interrupt threads directly. First, set status = 1 to not close gracefully

Add elegant closing method

Create a wheel thread pool

Submit thread method increases state and restricts submission

Create a wheel thread pool

When the stop method is called here:

  • 1. the thread is still executing (the queue or the current thread). After execution, the interrupt of all workers threads is invoked to prevent the presence of thread blocking at queue.take.
  • 2. No thread is still executing (queue or current thread). Call interrupt of all threads of workers to prevent thread blocking at queue.take

So here we need a flag to indicate whether there are threads executing. We can use a number to identify the number of threads that need to be executed at present. After a thread is executed, it will be – 1

Increase the member variable remainingcount to identify the number of remaining threads

/**Number of threads remaining*/
   private AtomicInteger remainingCount = new AtomicInteger(0);

One thread at a time + 1

Create a wheel thread pool

One thread at a time – 1

Create a wheel thread pool

In the first case, if the thread is still executing, judge whether the remainingcount is 0 after execution

Create a wheel thread pool

In the second case, there is no thread still executing, and judgment is added during stop

Create a wheel thread pool

Methods under Abstract encapsulation

Create a wheel thread pool
Create a wheel thread pool

By the way, the worker’s run method is also optimized, and no screen can be cut off

Create a wheel thread pool
Create a wheel thread pool

Run a test case in the first case

Create a wheel thread pool

It turns out that unexpectedly, it didn’t end all threads

Create a wheel thread pool

Add log debugging

Create a wheel thread pool

There are more exaggerated mistakes

Create a wheel thread pool

According to the console information, it can be imagined that the original five threads are all interrupted, creating threads and being interrupted.
The only place where threads will be created here is in the exception of the worker’s run method, in the finally code section, and when no log is added, this situation does not occur. When the log is added, it will appear.
After reading the code several times, it is found that after the first five threads consume the queue at the same time, two threads enter the take block, and three threads start to enter the interruptworkers method, which causes the two threads to have exceptions. After the exceptions, the thread exits, and a new thread is created again, and the new thread interrupts, which leads to a dead cycle

Modify the getqueuetask method, do not throw an exception, and return null in case of an exception. By the way, I read the situation of status = 0 and found that it does not affect

Create a wheel thread pool

Run the test case again, and the result is as expected. The exception stack here can be ignored

Create a wheel thread pool

Retest the second case of graceful shutdown

Create a wheel thread pool

Normal result

Create a wheel thread pool

Remove the debug log, so far, the wheel thread pool is completed, and has the basic function of thread pool

summary

The process of writing this thread pool is tortuous, and various problems continue to appear. In particular, the two methods of closing the thread pool are annoying to judge. It takes a long time for the code to be read and debugged before it can be solved. Therefore, it is associated with how powerful and simple the thread pool executor is

There are many pictures, and the code can be found on GitHub

Resources: ThreadPoolExecutor source code

Thank you for your article: https://crossvergie.top/2019

This article is from the blog of chentiefeng