Understand the principle of thread pool by hand

==> Learning summary (continuous update)
==> Build back-end infrastructure series from scratch (I) -- background introduction

Summary: Part I A simple lockless thread pool is realized, novelette This paper focuses on the analysis of java thread pool source code.

1, Basic concepts
In fact, to understand the following parameter concepts, it is as simple to see the source code of thread pool as to see helloworld.

  • corePoolSize
    Number of core threads, what is the core thread? As soon as it is created, it will reside in the memory and will not exit or destroy. Intuitively, the code is long
    public void run(){
        while(true){
    	     //To perform or wait for a task
    	}
    }
    
  • maximumPoolSize
    Maximum number of threads. What is the maximum number of threads? It is the maximum number of threads that can run simultaneously in this thread pool. For example, if the number of core threads is 5 and the maximum number of threads is 10, when the five core threads are not enough, a maximum of five more threads will be created to reach the maximum number of 10 threads. So the question is, will non core threads be recycled? The answer is not sure. It depends on the survival time you set.
  • keepAliveTime
    The time that a thread remains alive. Generally speaking, if a thread fails to take action within a given time, it will exit and be destroyed. Is this setting only valid for non core threads? The answer is No. the thread pool has a parameter that can set the core thread to be recycled.
  • BlockingQueue
    Blocking queue, what is blocking queue? What's different from a normal queue? The name of this person has already told you. The difference is that they can block. When the queue is empty, it can block and wait there. Take a look at the implementation of LinkedBlockingDeque, one of the blocking queues.
    public E takeFirst() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            E x;
            //If the queue is always empty, it's always blocked there
            //When the queue is not empty, notEmpty.await() will be awakened
            while ( (x = unlinkFirst()) == null)
                notEmpty.await();
            return x;
        } finally {
            lock.unlock();
        }
    }
    ......
    public E pollFirst(long timeout, TimeUnit unit)
        throws InterruptedException {
        long nanos = unit.toNanos(timeout);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            E x;
            //This is different from take. You can specify the waiting time and return null directly after timeout
            while ( (x = unlinkFirst()) == null) {
                if (nanos <= 0)
                    return null;
                nanos = notEmpty.awaitNanos(nanos);
            }
            return x;
        } finally {
            lock.unlock();
        }
    }
    
  • ThreadFactory
    Thread factory, it sounds like a production thread. Yes, it means literally. This is a simple example that the thread pool opens the creation of threads to users for customization.
    new ThreadFactory() {
       @Override
        public Thread newThread(Runnable r) {
            return new Thread(r, "I was born in a thread factory");
        }
    };
    
    Through the thread factory, we can customize the thread personalized, and then we will not show it in detail.
  • RejectedExecutionHandler
    Refuse to execute the strategy, the popular point is that when the task cannot be executed, you can customize a processing strategy, and the default strategy is to throw exceptions directly. But in practice, we should customize a friendly processing strategy.
  • ctl
    Is it almost unrecognized? This is a parameter that appears everywhere in the java thread pool.
     private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    
    You can see how the comments in the source code explain the abbreviated parameter, which is actually a parameter that controls the size and status of the thread pool.

    We can print out the state defined by it and see it at a glance.
    int COUNT_BITS = Integer.SIZE - 3;
    int CAPACITY = (1 << COUNT_BITS) - 1;
    int RUNNING = -1 << COUNT_BITS;
    int SHUTDOWN   =  0 << COUNT_BITS;
    int STOP       =  1 << COUNT_BITS;
    int TIDYING    =  2 << COUNT_BITS;
    int TERMINATED =  3 << COUNT_BITS;
    System.out.println("COUNT_BITS = " + COUNT_BITS);
    System.out.println("CAPACITY   = " + CAPACITY + "  = 000" + Integer.toBinaryString(CAPACITY));
    System.out.println("~CAPACITY  = " + ~CAPACITY + " = " + Integer.toBinaryString(~CAPACITY));
    System.out.println("RUNNING    = " + RUNNING + " = " + Integer.toBinaryString(RUNNING));
    System.out.println("SHUTDOWN   = " + SHUTDOWN + " = " + Integer.toBinaryString(SHUTDOWN));
    System.out.println("STOP       = " + STOP + "  = 00" + Integer.toBinaryString(STOP));
    System.out.println("TIDYING    = " + TIDYING + " = 0" + Integer.toBinaryString(TIDYING));
    System.out.println("TERMINATED = " + TERMINATED + " = 0" + Integer.toBinaryString(TERMINATED));
    

    As you can see, a 32-bit integer with 29 bits for capacity and 3 bits for status. As long as CTL < 0, the thread pool is running normally. If CTL > = 0, it will be closed.
  • spin
    The popular point is the dead cycle for(;;);, some people may ask, why haven't you ever seen it come from spinning with while? Actually, it's all right. From now on, the efficiency of both is basically the same.
  • CAS
    The full name of CAS is compareAndSet, which means compare and update. This is an atomic operation implemented by the underlying assembly instructions. The assembly instruction used is cmpxchg, which is used to compare and exchange operands. java can't use assembly directly, so what should we do? java can use this instruction indirectly through the C language, so it leads to the Unsafe class. Simply speaking, this class provides you with methods similar to C/C + + to operate memory, so it is very dangerous. If we want to use it, we can only get it through reflection.
  • AQS
    The full name of AQS is AbstractQueuedSynchronizer, which is a queue synchronizer. Its function is like a pipeline. It strictly controls the first in, first out of threads. At any time, only one thread can run, and others will be blocked. For example, if there are two threads competing for a resource at the same time, thread 1 will get it first, and thread 2 will not get it if it wants to get it again, and put it in the queue at the end of the queue. Only when thread 1 releases this resource and leaves the queue can thread 2 get it. And so on. AQS follow-up through other articles to further explore.

2, Source code analysis of ThreadPoolExecutor
Based on the above explanation of the basic concept, the analysis of its constructor will not be repeated here.
1. Three ways of task submission

  • void execute(Runnable command)
    The task submitted in this way has no return value, and there is no Future asynchronously waiting for the task execution to finish.
  • Future<?> submit(Runnable task)
    For tasks submitted in this way, there is no return value, and Future asynchronously waits for the task execution to finish.
    public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }
    ......
    protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
        return new FutureTask<T>(runnable, value);
    }
    
    ......
    public interface RunnableFuture<V> extends Runnable, Future<V> {
    	void run();
    }
    
    The packaging of task here is quite ingenious. Because the interface can inherit more, a new interface RunnableFuture is wrapped, which inherits from Runnable and Future. In this way, the middleman FutureTask will wrap Runnable and Callable, perfect!
  • <T> Future<T> submit(Callable<T> task)
    For tasks submitted in this way, there must be a return value (that is, T cannot be Void) and a Future asynchronously waiting for the task execution to finish.

All three methods of submission are finally executed by execute.

2. Source code analysis of thread pool entry execute

  • code analysis
    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        //Determine whether the current thread pool size is less than the number of core threads
        if (workerCountOf(c) < corePoolSize) {
            //If the number of core threads has not been reached, continue to create threads 
            if (addWorker(command, true))
                return;
            //Try to get the latest number of threads here
            c = ctl.get();
        }
        //If the thread pool size has reached the number of core threads, it will be queued
        if (isRunning(c) && workQueue.offer(command)) {
            //No matter how many checks are made, the purpose is to sense the state changes under concurrency as much as possible
            int recheck = ctl.get();
            //If the thread pool is closed, do not add tasks to the queue. Here
            //Try to delete the task added before. Note that this operation may fail
            //false, because as soon as you add it in, it may be executed immediately. If the deletion is successful
            //Use the corresponding rejection policy
            if (! isRunning(recheck) && remove(command))
                reject(command);
            //This is interesting. When the thread pool is running and the size is 0, create a thread
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        //This is easy to understand. When the core thread is full and the queue is full, the non core thread is created
        //If the non core thread is full, the corresponding rejection policy will be executed
        else if (!addWorker(command, false))
            reject(command);
    }
    
  • Q&A
    1) The first if has determined that the current thread pool size has not reached the number of core threads. Why can addWorker return false?
    When I didn't have a thread pool by myself, I read it many times and didn't understand it. Let's assume that in such a scenario, the number of core threads is 2, and the maximum number of threads is 4. Is it possible for multithreaded concurrent tasks to be submitted at the same time= ctl.get (); or int c at very short intervals= ctl.get ();, what is the result of this? The thread has not been created, and the thread pool size has not been updated. What you get is a value, which is smaller than the corePoolSize. Therefore, you need to perform interception judgment in addWorker (to be specific, let's talk about it later). If two core threads have been created, the remaining two will return false and continue to perform the following branch judgment.
    2) When is this workerCountOf(recheck) == 0 true?
    When the thread pool is running, and after the creation of the previous core thread, the number of threads will be 0. I don't know for sure.

3. Source code analysis of addWorker

  • code analysis
    private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        for (;;) {
            int c = ctl.get();
            //Get thread pool running status
            int rs = runStateOf(c);
    		
    		//1. Thread pool status is greater than or equal to SHUTDOWN
    		//2.! (thread pool status is exactly equal to shutdown & & firsttask = = null & &! workQueue.isEmpty ())
    		//If the first and second conditions are met at the same time, you don't need to continue to create a worker thread
    		//The first condition is easy to understand, that is, the thread pool should
    		//Closed, unable to create worker thread. The second condition we have to think about is threads
    		//The pool status is exactly equal to SHUTDOWN, and the task is empty, and the queue is not empty. Then
    		//We must get another thread to handle the remaining tasks in the queue. Otherwise, if the thread pool status is greater than
    		//SHUTDOWN is to turn off immediately without any follow-up processing. Of course, it is not necessary
    		//Create another worker thread.
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;
    
            for (;;) {
                //Get the current thread pool size
                int wc = workerCountOf(c);
                //1. The thread pool size exceeds the theoretical maximum capacity
                //2. If you want to create a core thread, you need to determine whether the number of core threads is full. The same is true for non core threads
                //If one of the conditions 1 and 2 is met, false will be returned, and creation fails
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                //If all the previous levels have passed, it can be created, and then the thread pool size can be increased in advance
                //CAS is used to add   
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                //If the modification fails, get the latest value    
                c = ctl.get();  // Re-read ctl
                //Judge whether the state of one downline pool changes. If there is any change, skip to the outer loop
                //Return to continue to recycle CAS in the inner layer and try to increase the thread pool size
                if (runStateOf(c) != rs)
                    continue retry;
            }
        }
    
        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            //Create a worker thread. This step does not need to be locked. It can be created concurrently
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                final ReentrantLock mainLock = this.mainLock;
                //Now we start to lock, because it involves the reading and writing of HashSet, which is thread unsafe
                mainLock.lock();
                try {
             		
                    int rs = runStateOf(ctl.get());
    				//1. The thread pool is running normally
    				//2. The thread pool status is equal to SHUTDOWN and the first task is empty, which means to create a thread to consume the tasks in the queue
    				//Any of the above conditions can be met
                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        //Check whether this thread is running, because it has not yet reached the following t.start(), if it is running here, it is obviously illegal (I don't know what specific scenarios will be encountered)
                        if (t.isAlive()) // precheck that t is startable
                            throw new IllegalThreadStateException();
                        workers.add(w);
                        int s = workers.size();
                        //This is to record the maximum number of downline pools
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                if (workerAdded) {
                    //Start thread
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                //If the startup fails, some processing operations will be carried out
                addWorkerFailed(w);
        }
        return workerStarted;
    }
    
  • Q&A
    1) Why two layers of for loops?
    I've written a general idea in the code annotation. The first level loop is used to judge the status of the thread pool. The function of the second level loop is to determine whether threads can be created, and if so, use CAS to try to increase the size of the thread pool. If it fails and the state of the thread pool changes, skip to the outer loop for judgment processing, otherwise continue to repeat the above operations in the inner loop.
    2) Why use CAS to increase thread pool size instead of using + + or + 1 directly?
    This leads back to the source code analysis of execute. Remember the above analysis. If the task is submitted concurrently, it will enter addWorker in the first if branch. If the lock is not added at this time, four core threads will be created. With CAS operation, only two core threads can be created, and the other two will return false.
    3) Why do we need to add the pool size in advance?
    In this way, we can quickly let those uninvited guests leave addWorker, and let the tasks that are still trying to come in die.

4. Source code analysis of Worker

  • code analysis
    private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable
    {
        private static final long serialVersionUID = 6138294804551838833L;
    	
    	//Worker thread
        final Thread thread;
        //When creating a Worker, if the Task is transferred, the Task will be executed directly. Otherwise, go to the queue to get the Task
        Runnable firstTask;
        
        //This is a statistic
        volatile long completedTasks;
        
        Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            //For the thread created here, the incoming Runnable is the Worker itself
            //When t.start is called outside, the run method in Woker is run
            this.thread = getThreadFactory().newThread(this);
        }
    
        public void run() {
        	//Real execution
            runWorker(this);
        }
    
    	//state == 0 means it is not held by a thread at present, state == 1 means it is held by a thread at present, which is equivalent to being locked
        protected boolean isHeldExclusively() {
            return getState() != 0;
        }
    	
    	//Try to lock. If the current state == 0, try to set the state to 1, that is, lock
        protected boolean tryAcquire(int unused) {
            if (compareAndSetState(0, 1)) {
            	//Set the current thread as the exclusive thread of this Worker (exclusive lock)
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }
    
    	//Release lock
        protected boolean tryRelease(int unused) {
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }
    
        public void lock()        { acquire(1); }
        public boolean tryLock()  { return tryAcquire(1); }
        public void unlock()      { release(1); }
        public boolean isLocked() { return isHeldExclusively(); }
    	
    	//This code means that if the thread is started, it can be interrupted
        void interruptIfStarted() {
            Thread t;
            if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
                try {
                    t.interrupt();
                } catch (SecurityException ignore) {
                }
            }
        }
    }
    
  • Q&A
    1) Why is this line of setState(-1) code in the Worker constructor?
    See that there is such a judgment in Woker's interruptIfStarted method
    "Getstate() > = 0 & & (t = thread)! = null & &! T.isinterrupted()", state must be greater than or equal to 0 to interrupt this thread. Why? It's easy to understand. I haven't started it yet, so you interrupt me. Is this a human thing? When t.start is called outside, the thread starts, and state is set to 0, then you can interrupt.
    2) Why is the runWorker method out there?
    I didn't think of that. Was it just for the sake of the Worker code to be as concise as possible?
    3) Why inherit AbstractQueuedSynchronizer and not use ReentrantLock directly?
    As we all know, ReentrantLock is a reentrant lock, but the feature of Worker, it can't use reentrant lock, why? Because it needs to implement the non reentrant feature to reflect the current execution state of the thread. When we analyze the interrupt idleworkers method, we will be enlightened.

5. Source code analysis of runWorker

  • code analysis
    final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        //This is to prevent repeated execution of this task
        w.firstTask = null;
        //Here is the operation to place the state at 0
        w.unlock(); // allow interrupts
        //This variable is also interesting. It is considered comprehensively. This is to prevent the exception that cannot be caught in the while loop, that is, abnormal ending. Finally, there will be corresponding processing strategies.
        boolean completedAbruptly = true;
        try {
        	//Task! = null, execute it first, and then go to the queue to get task
        	//There are three possibilities for getTask, the first is to return task, the second is to return null, and the third is to block all the time (discuss after analyzing the source code of getTask)
            while (task != null || (task = getTask()) != null) {
            	//Lock first when performing tasks
                w.lock();
                
                //This kind of code is very common in the thread pool. It's just to judge the current status of the thread pool and do the corresponding processing. You can try to analyze it yourself. It's very simple
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                    //Methods open to subclass implementation
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                        //Real mission execution
                        task.run();
                    } catch (RuntimeException x) {
                        thrown = x; throw x;
                    } catch (Error x) {
                        thrown = x; throw x;
                    } catch (Throwable x) {
                        thrown = x; throw new Error(x);
                    } finally {
                     	//Methods open to subclass implementation
                        afterExecute(task, thrown);
                    }
                } finally {
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            //Run here, no unknown exception, set to false
            completedAbruptly = false;
        } finally {
            //This is the follow-up processing. There is nothing to discuss in it. You can analyze it if you are interested
            processWorkerExit(w, completedAbruptly);
        }
    }
    
  • Q&A
    1) Why do you need to lock before performing tasks?
    If from here alone, there is really no need to lock, this section is simply synchronous execution task.run Method, is serial. If there is such a method "interrupt idleworkers" from a global perspective, you can look at its code. In response to the above analysis, why to use AQS to implement a non reentrant lock is to reflect the state of the worker.
    //Try to interrupt idle threads
    private void interruptIdleWorkers(boolean onlyOne) {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            for (Worker w : workers) {
                Thread t = w.thread;
                //Here's the point. Before interruption, you need to judge whether the thread is idle. How to judge? Simple, we try to get the lock. If it succeeds, it means that the thread is not executing task.run , i.e. free.
                if (!t.isInterrupted() && w.tryLock()) {
                    try {
                        t.interrupt();
                    } catch (SecurityException ignore) {
                    } finally {
                        w.unlock();
                    }
                }
                if (onlyOne)
                    break;
            }
        } finally {
            mainLock.unlock();
        }
    }
    

6. Get task getTask source code analysis

  • code analysis

    private Runnable getTask() {
    	//The design of this variable, as you can see in the second part, is very ingenious
        boolean timedOut = false; 
    
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);
    		
    		//Or judge the thread pool status
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                decrementWorkerCount();
                return null;
            }
    
            int wc = workerCountOf(c);
    
            //It is easy to understand whether to allow the recycling of threads. If the core threads are allowed to be recycled or the current thread pool size is larger than the number of core threads, the threads are allowed to be recycled.
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
    		
    		//In this section, judge whether the current thread can be recycled. If it can, first reduce the thread pool size by 1, and return null. The external loop will exit, and the thread will exit
    		//Let's look at the specific conditions
    		//1. The current thread pool size is greater than the maximum number of threads
    		//2. Allow the thread to be recycled and no task is obtained after the specified time (there are at least 2 threads in the current thread pool or the queue is empty)
    		//You can recycle threads if you meet any of the 1 and 2 conditions
            if ((wc > maximumPoolSize || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
                //CAS is also used to reduce the thread pool size by 1
                if (compareAndDecrementWorkerCount(c))
                    return null;
                //If it fails, other threads succeed. Repeat the above steps    
                continue;
            }
    
            try {
                //If recycling is allowed, call poll, which can set the timeout. If there is no task processing within the specified time, timedOut will be true, indicating that recycling is possible
                //On the contrary, call take and keep blocking
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }
    
  • Q&A
    1) Why decrease the thread pool size directly when getting Runnable null, and then return null?
    If you have manually rolled up the thread pool, you should encounter this scenario, and you will know why. Now let me give you a small example

    try {
         boolean timed;
         try {
             lock.lock();
             //Because it is locked here, N threads will be judged in order. Note that it will be very fast here
             timed = currentPoolSize.get() > corePoolSize;
             //if(timed && timeout) {
             //    currentPoolSize.decrementAndGet();
             //    return null;
             //}
         } finally {
             lock.unlock();
         }
         //The execution here will be slower than the above one. What problems will it cause?
         //In the worst case, N threads run directly here, and timed=true
         //Then there will be many runnable == null
         Runnable runnable = timed ? workerQueues.poll(keepTimeAlive, TimeUnit.NANOSECONDS) : workerQueues.take();
         if(runnable != null){
             return runnable;
         }
         //It's going to decrease too much here. I can't stop the car
         currentPoolSize.decrementAndGet();
         return null;
         //timeout = true;
     } catch (InterruptedException e) {
         //timeout = false;
     }
    

    Of course, there must be some methods, such as adding locks, judging, etc., which are troublesome and inefficient. It's better to reduce than acquire.

    2) What is the function of InterruptedException?
    From the name, we can see that the interrupt exception has obvious effect. When the call is a take block, when the thread is interrupted, the exception will be thrown. When do you usually interrupt a thread? When the thread pool is closed.

7. Source code analysis of thread pool shutdown

  • code analysis
    public void shutdown() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            //This is the status of the update thread pool
            advanceRunState(SHUTDOWN);
            //Interrupt the idle thread, and let it continue to finish the tasks that are still executing (this method has been analyzed above)
            interruptIdleWorkers();
            //Methods open to subclasses
            onShutdown(); // hook for ScheduledThreadPoolExecutor
        } finally {
            mainLock.unlock();
        }
        //Try to set the thread pool state to TERMINATED, and do something about it
        tryTerminate();
    }
    
    private void advanceRunState(int targetState) {
        for (;;) {
            int c = ctl.get();
            //Simple conditions, such as targetState == SHUTDOWN
            //If C > = shutdown, there is no need to modify it
            //If the thread pool is still running, CAS modifies its state
            if (runStateAtLeast(c, targetState) ||
                ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c))))
                break;
        }
    }
    
    final void tryTerminate() {
        for (;;) {
            int c = ctl.get();
            //1. For the shutdown method, when calling this method, the thread pool status cannot be running
            //2. It can't be > = tidying, it should be SHUTDOWN
            //3. If it is SHUTDOWN and there are tasks in the queue, it is not OK
            if (isRunning(c) ||
                runStateAtLeast(c, TIDYING) ||
                (runStateOf(c) == SHUTDOWN && ! workQueue.isEmpty()))
                return;
            
            if (workerCountOf(c) != 0) { // Eligible to terminate		
            	//I don't know why I only interrupt one here?    
                interruptIdleWorkers(ONLY_ONE);
                return;
            }
    
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
            	//This code is relatively simple, that is, the thread pool status changes from tidying - > terminated
            	//The subclass can be given a method of closing
                if (ctl.compareAndSet(c, ctlOf(TIDYING, 0))) {
                    try {
                        terminated();
                    } finally {
                        ctl.set(ctlOf(TERMINATED, 0));
                        termination.signalAll();
                    }
                    return;
                }
            } finally {
                mainLock.unlock();
            }
            // else retry on failed CAS
        }
    }
    
  • Q&A
    1) What is the role of tryTerminate?
    My understanding of this is to try to finish the final work and open a method to subclasses, which has no impact on the main process of thread pool shutdown.

8. Source code analysis of thread pool shutdown now

  • code analysis
    public List<Runnable> shutdownNow() {
        List<Runnable> tasks;
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            advanceRunState(STOP);
            //Unlike shutdown, all threads are interrupted, whether they are idle or not
            interruptWorkers();
            //This is to return the remaining unimplemented tasks to the user for self processing
            tasks = drainQueue();
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
        return tasks;
    }
    private void interruptWorkers() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            for (Worker w : workers)
                w.interruptIfStarted();
        } finally {
            mainLock.unlock();
        }
    }
    
  • Q&A
    1) Is shutdownNow sure to let the thread exit?
    Here I would like to emphasize that unless the program crashes, there is no external way to force the thread to exit. Only by setting the thread's interrupt position to 1, and then the thread can decide whether to process the interrupt signal or not. The only difference between shutdown and shutdown now is that the former only sends interrupt signals to idle threads and the latter to all threads. Here's an example
    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(2, 2, 60, TimeUnit.SECONDS, new LinkedBlockingDeque<>());
    
        for (int i = 0; i < 4; i++) {
            poolExecutor.execute(() -> {
                System.out.println("Current thread:" + Thread.currentThread().getName() + " start");
                while (true){
                    if(Thread.currentThread().isInterrupted()){
                        System.out.println("Receive interrupt signal, exit");
                        break;
                    }
                }
                System.out.println("Current thread:" + Thread.currentThread().getName() + " end");
    
            });
        }
        Thread.sleep(2000);
        System.out.println("Thread pool shutdown");
        List<Runnable> runnables = poolExecutor.shutdownNow();
        System.out.println(runnables.size());
    }
    

    If shutdown is used, the running thread will not receive interrupt information in any case.

3, Summary
To be honest, I've seen the source code of thread pool many times before, but I'm ignorant every time, and then I can't read it anymore. I think it's too difficult. Then suddenly, as like as two peas, I want to thread a thread pool to see the difficulty geometry, write the process, encounter many problems, and then try to solve it. Finally, I find that some ideas are exactly the same as the java thread pool. Those problems that bother me are also solved. So I really should say that, practice out the truth, and the ultimate pain of laziness is to bear by myself.

Tags: Java less C

Posted on Sat, 13 Jun 2020 06:38:28 -0400 by drummerboy