Java thread pool

Posted by gple on Wed, 19 Jan 2022 11:23:04 +0100

1, Foreword

Threads are scarce resources. If they are created without restrictions, they will not only consume system resources, but also reduce the stability of the system. Rational use of thread pool to uniformly allocate, tune and monitor threads has the following advantages: reusing existing threads and effectively controlling the maximum number of concurrent threads

2, How

The simple usage of thread pool is as follows

ExecutorService executorService = new ThreadPoolExecutor(3, 5,
        60L, TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(10));
for (int i = 0; i < 10; i++) {
    executorService.execute(new Runnable() {
        @Override
        public void run() {
            System.out.println("thread id is: " + Thread.currentThread().getId());
        }
    });
}
executorService.shutdown();

1, ThreadPoolExecutor

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}

corePoolSize: the number of core threads in the thread pool. The core threads will survive even if there are no tasks to execute.

maximumPoolSize: the maximum number of threads allowed in the thread pool, the number of core threads + the number of temporary threads

keepAliveTime: the lifetime of a temporary thread when it is idle

workQueue: save the blocking queue of the task waiting to be executed, and the task must implement the Runable interface.

Let's look directly at executorservice Execute method.

public void execute(Runnable command) {
    if (command == null) {
        throw new NullPointerException();
    } else {
        int c = this.ctl.get();//Gets the value related to the current thread state
        if (workerCountOf(c) < this.corePoolSize) {  //1
            if (this.addWorker(command, true)) {
                return;
            }

            c = this.ctl.get();
        }

        if (isRunning(c) && this.workQueue.offer(command)) { //2
            int recheck = this.ctl.get();
            if (!isRunning(recheck) && this.remove(command)) {//3
                this.reject(command);
            } else if (workerCountOf(recheck) == 0) {//4
                this.addWorker((Runnable)null, false);
            }
        } else if (!this.addWorker(command, false)) {//5
            this.reject(command);
        }

    }
}

1.workerCountOf obtains the current number of threads in the thread pool according to the lower 29 bits of ctl. If the current number of threads is less than the number of core threads, add the task addWork directly

2. If addWork above fails, continue. First, judge that the current thread pool is in the RUNNING state, and successfully put the submitted task into the blocking queue workQueue

If the addition fails, the offer returns false. Otherwise, execute the reject method to process the task.

3. Judge the current thread pool status again. If the thread pool is not RUNNING and the task is successfully deleted from the blocking queue, execute the reject method to process the task;

4. If the current number of threads is equal to 0, a non core thread may be added when a task is just inserted into the queue and all threads end the task and are destroyed.

5. If the workQueue queue is full, try to create a non core thread to process the task

There are two main behaviors, one is this workQueue. Offer (command) adds the task queue, and the other is this addWorker(command,true)

Take a look at the source code of addWork method

private boolean addWorker(Runnable firstTask, boolean core) {
    int c = this.ctl.get();

    label247:
    //When the current thread pool is not in SHUTDOWN or STOP state, and firsttask = = null, workqueue is not empty, continue
    while(!runStateAtLeast(c, SHUTDOWN) || !runStateAtLeast(c, STOP) && firstTask == null 
    && !this.workQueue.isEmpty()) { 
        //2 check whether the total number of threads exceeds the capacity.
        while(workerCountOf(c) <((core ? this.corePoolSize:this.maximumPoolSize)&COUNT_MASK)) {
            if (this.compareAndIncrementWorkerCount(c)) {// Number of threads plus 1
                boolean workerStarted = false;
                boolean workerAdded = false;
                ThreadPoolExecutor.Worker w = null;

                try {
                    //Start to create a new thread and add the task firstTask
                    w = new ThreadPoolExecutor.Worker(firstTask);
                    Thread t = w.thread;
                    if (t != null) {
                        //Start locking, so as to lock in the minimum range and minimize lock competition
                        ReentrantLock mainLock = this.mainLock;
                        mainLock.lock();

                        try {
                            int c = this.ctl.get();
                            ///Check the thread status. Only when the thread pool is RUNNING or SHUTDOWN and firstTask==null,
                            
                            if (isRunning(c) || runStateLessThan(c, STOP) && firstTask == null) {
                                if (t.getState() != State.NEW) {
                                    throw new IllegalThreadStateException();
                                }
                                //Worker s is a HashSet. Add our new workers
                                this.workers.add(w);
                                workerAdded = true;
                                //Every time you add workers, you will judge the current workers Whether size() is greater than largestPoolSize,
                                //If greater than, the current maximum value is assigned to largestPoolSize
                                int s = this.workers.size();
                                if (s > this.largestPoolSize) {
                                    //Record the maximum value since the history of workers,
                                    this.largestPoolSize = s;
                                }
                            }
                        } finally {
                            mainLock.unlock();
                        }

                        if (workerAdded) {
                            t.start();//start-up
                            workerStarted = true;
                        }
                    }
                } finally {
                    if (!workerStarted) {
                        this.addWorkerFailed(w);
                    }

                }

                return workerStarted;
            }

            c = this.ctl.get();
            if (runStateAtLeast(c, 0)) {
                continue label247;
            }
        }

        return false;
    }

    return false;
}

There are five kinds of thread pool states,

  • RUNNING: receive new tasks and continue to process tasks in workQueue
  • SHUTDOWN: no new tasks will be received, but tasks in workQueue can continue to be processed
  • STOP: no new tasks will be received and no tasks in workQueue will be processed, and the thread processing the task will be interrupted
  • TIDYING: when all tasks are completed and the number of threads (workCount) is 0, it is in this state. After entering this state, the hook method terminated() will be called to enter the TERMINATED state
  • TERMINATED: this state occurs when the terminated() method is called

The addwork process is to check whether the thread pool status and the total number of threads meet the conditions. If so, create a new Worker, add the task firstTask, and put the Worker

Add it to the workers hashset, and finally start the thread t.start () in the Worker. This. In the source code thread = getThreadFactory(). newThread(this); This is the current runnable, this Runnable in the thread is the current Worker, which executes t.start()

Is to execute the run method in the current Worker

Worker inherits AbstractQueuedSynchronizer to implement Runnable

public void run() {
    ThreadPoolExecutor.this.runWorker(this);
}

final void runWorker(ThreadPoolExecutor.Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;//Gets the Runnable of the current thread task
    w.firstTask = null;
    w.unlock();
    boolean completedAbruptly = true;

    try {
        while(task != null || (task = this.getTask()) != null) {
            //Current thread shackle
            w.lock();
          //If (thread pool status > = stop or (thread interrupted and thread status > = stop)) and the current thread is not interrupted.
          // Two cases:
          //1) If the status of the current thread pool is > = stop and the current thread is not interrupted, the interrupt must be executed.
          //2) Or the current thread is in the interrupted state, and the state of the thread pool is > = stop (note that Thread.interrupted will erase the interrupt identifier),
          //Then, because the interrupt identifier has been erased, then! wt.isInterrupted() must return true. At this time, the current thread should be interrupted.
          //The second execution of runStateAtLeast(ctl.get(), STOP) is equivalent to a second check.
            if ((runStateAtLeast(this.ctl.get(), STOP) || Thread.interrupted() && 
            runStateAtLeast(this.ctl.get(), STOP)) && !wt.isInterrupted()) {
                wt.interrupt();//Interrupt the current thread
            }

            try {
                this.beforeExecute(wt, task);//Pre operation, empty method, can be implemented by the business itself

                try {
                    task.run();//Execute run, that is, the following method
                    //new Runnable() {
                    //   @Override
                    //  public void run() {
                    //      System.out.println("thread id is: " + Thread.currentThread().getId());
                    //   }
                    //}
                    this.afterExecute(task, (Throwable)null);//Post operation, empty method, can be implemented by the business itself
                } catch (Throwable var14) {
                    this.afterExecute(task, var14);
                    throw var14;
                }
            } finally {
                task = null;//Finally, set the task to null
                ++w.completedTasks;//Completed tasks counter + 1
                w.unlock();//Release the exclusive lock of the current thread
            }
        }

        completedAbruptly = false;
    } finally {
        this.processWorkerExit(w, completedAbruptly);
    }

}

runWorker starts a thread and executes getTask in a loop until task==null.

Take a look at the getTask source code

private Runnable getTask() {
    boolean timedOut = false;

    while(true) {
        int c = this.ctl.get();
        // If the current status is > = shotdown status & & (the running status is STOP or the queue is empty)
        // 1) If the status of the thread pool is > = stop, the tasks in the queue will not be processed at this time, and the number of worker records will be reduced,
        //The returned task is null. At this time, processWorkerExit will be executed in the runRWorker method to exit the worker
        // 2) If the status of the thread pool is > = shutdown and the workQueue is empty, it indicates that it is in a state above shutdown,
        //If there is no task waiting, the task cannot be obtained, and getTask returns null
        if (runStateAtLeast(c, SHUTDOWN) && (runStateAtLeast(c, STOP) || this.workQueue.isEmpty())) {
            this.decrementWorkerCount();
            return null;
        }

        int wc = workerCountOf(c);
        //If the number of thread threads allowed to timeout is greater than the core thread capacity, the timeout mechanism is enabled. timed=true
        boolean timed = this.allowCoreThreadTimeOut || wc > this.corePoolSize;
        //If the current number of threads is less than or equal to the maximum thread capacity and timeout is not allowed, or there is no timeout, or the current number of threads is less than or equal to 1 and the current task queue is not empty
        if (wc <= this.maximumPoolSize && (!timed || !timedOut) || wc <= 1 && !this.workQueue.isEmpty()) {
            try {
                //Judge whether timeout is allowed. If timeout is allowed, use poll to set the timeout time. If not, use take dependent timeout mechanism
                Runnable r = timed ? (Runnable)this.workQueue.poll(this.keepAliveTime, TimeUnit.NANOSECONDS) 
                : (Runnable)this.workQueue.take();
                if (r != null) {
                    return r;
                }

                timedOut = true;
            } catch (InterruptedException var6) {
                timedOut = false;
            }
        } else if (this.compareAndDecrementWorkerCount(c)) {
            return null;
        }
    }
}

getTask is mainly to continuously fetch Runnable tasks from the workQueue queue.

Continue to look at the source code of the processWorkerExit method

private void processWorkerExit(ThreadPoolExecutor.Worker w, boolean completedAbruptly) {
    if (completedAbruptly) {
        this.decrementWorkerCount();
    }

    ReentrantLock mainLock = this.mainLock;
    mainLock.lock();

    //
    try {
        this.completedTaskCount += w.completedTasks;
        this.workers.remove(w);
    } finally {
        mainLock.unlock();
    }

    this.tryTerminate();
    int c = this.ctl.get();
    if (runStateLessThan(c, 536870912)) {
        if (!completedAbruptly) {
            int min = this.allowCoreThreadTimeOut ? 0 : this.corePoolSize;
            if (min == 0 && !this.workQueue.isEmpty()) {
                min = 1;
            }

            if (workerCountOf(c) >= min) {
                return;
            }
        }

        this.addWorker((Runnable)null, false);
    }

}

3, Question

So far, we also have a general understanding of the operation mode of thread pool, but we don't seem to realize the benefits of thread pool: reusing existing threads and concurrency

Reviewing the code, it seems that the reuse thread pool is not directly reflected. The Worker is new every time.

Let's take a look at executorservice Execute code, which has such logic,

If the current number of threads is less than the number of core threads, execute this Addworker (command, true) operation. This method is mainly to create a Worker object, run the thread in the Worker, directly start, start the thread and execute the firstTask task task. If the number of core threads is 4, start 4 threads.

If the current thread is larger than the number of core threads, go this workQueue. In the step of offer (command), add the task to the workqueue queue.

Let's take a look at the runWorker method, which is the main method for executing thread tasks. Take a look at while (task! = null | (task = this. Gettask())= Null) this condition when we call this The addworker (command, true) method is that task means that command is not equal to null, then execute the content. After execution, finally {task = null; + + w.completedtasks; w.unlock(); / /} task = null, then the task should be completed and the thread should be destroyed.

However, we should also note that there is a getTask method in the while condition, which is to get the Runnable task from the blocking queue, that is, the workQueue gets the Runnable. If the workQueue has content, Runnable will be executed in the current Worker thread. If there is no content, it will be blocked. The thread will not be destroyed.

How to achieve efficient concurrency in thread pool.

Looking at the workflow of the whole thread pool, there are several concurrency points that need special attention

Changes in thread pool status and number of worker threads. This problem is solved by an AtomicInteger variable ctl.

When adding a new Worker to the Worker container workers, the thread pool is locked.

Thread shackles when performing specific tasks.

When the Worker thread fetches a task from the waiting queue. The thread safety is guaranteed by the work queue itself, such as linked blocking queue.