Java Thread Pool Implementation Principles and Technologies, Read this article is enough

Posted by Glyde on Sat, 11 May 2019 15:05:51 +0200

01. Disadvantages of unrestricted threads

Multithread software design method can really maximize the computing power of multi-core processors and improve the throughput and performance of production systems. However, if threads are used arbitrarily without control and management, the performance of the system will be adversely affected.

One of the simplest ways to create and recycle threads is similar to the following:

new Thread(new Runnable() { 
            @Override 
            public void run() { 
                //do sth 
            } 
        }).start(); 

The above code creates a thread and automatically reclaims it after the run() method has finished. In a simple application system, there are not many problems with this code. But in the real production environment, the system may open many threads to support its application because of the need of the real environment. When the number of threads is too large, it will exhaust CPU and memory resources.

First of all, although threads are a lightweight tool compared with processes, it still takes time to create and close them. If a thread is created for every small task, it is likely that the time taken to create and destroy threads is longer than the time spent by the real work of the thread, but it will not be rewarded.

Secondly, threads themselves occupy memory space, and a large number of threads will occupy valuable internal resources.

Therefore, in the actual production environment, the number of threads must be controlled. Blindly creating a large number of threads is harmful to system performance.

02. Simple thread pool implementation

Here is the simplest thread pool. This thread pool is not a perfect thread pool, but it has realized the core function of a basic thread pool, which is helpful to quickly understand the implementation of thread pool.

1. Implementation of thread pool

public class ThreadPool { 
    private static ThreadPool instance = null; 
 
    //Free thread queues
    private List<PThread> idleThreads; 
    //Total number of threads available
    private int threadCounter; 
    private boolean isShutDown = false; 
 
    private ThreadPool() { 
        this.idleThreads = new Vector<>(5); 
        threadCounter = 0; 
    } 
 
    public int getCreatedThreadCounter() { 
        return threadCounter; 
    } 
 
    //Get an instance of thread pool
    public synchronized static ThreadPool getInstance() { 
        if (instance == null) { 
            instance = new ThreadPool(); 
        } 
        return instance; 
    } 
 
    //Put the thread pool into the pool.
    protected synchronized void repool(PThread repoolingThread) { 
        if (!isShutDown) { 
            idleThreads.add(repoolingThread); 
        } else { 
            repoolingThread.shutDown(); 
        } 
    } 
 
    //Stop all threads in the pool
    public synchronized void shutDown() { 
        isShutDown = true; 
        for (int threadIndex = 0; threadIndex < idleThreads.size(); threadIndex++) { 
            PThread pThread = idleThreads.get(threadIndex); 
            pThread.shutDown(); 
        } 
    } 
 
    //Execution of tasks
    public synchronized void start(Runnable target) { 
        PThread thread = null; 
        //If there are idle threads, use them directly.
        if (idleThreads.size() > 0) { 
            int lastIndex = idleThreads.size() - 1; 
            thread = idleThreads.get(lastIndex); 
            idleThreads.remove(thread); 
            //Implement this task immediately.
            thread.setTarget(target); 
        }//If there are no idle threads, create threads
        else { 
            threadCounter++; 
            //Create new threads
            thread = new PThread(target, "PThread #" + threadCounter, this); 
            //Start this thread
            thread.start(); 
        } 
    } 
 
} 

2. To achieve the above thread pool, we need a thread that never quits to cooperate with it. PThread is one such thread. Its main body is an infinite loop, which never ends until it is manually closed and waits for new tasks to arrive.

public class PThread extends Thread { 
    //Thread pool
    private ThreadPool pool; 
    //Task
    private Runnable target; 
    private boolean isShutDown = false; 
    private boolean isIdle = false; //Is it idle?
    //Constructor
    public PThread(Runnable target,String name, ThreadPool pool){ 
        super(name); 
        this.pool = pool; 
        this.target = target; 
    } 
 
    public Runnable getTarget(){ 
        return target; 
    } 
 
    public boolean isIdle() { 
        return isIdle; 
    } 
 
    @Override 
    public void run() { 
        //The thread is not terminated as long as it is not closed.
        while (!isShutDown){ 
            isIdle =  false; 
            if (target != null){ 
                //Operating tasks
                target.run(); 
            } 
            try { 
                //The task is over and idle.
                isIdle = true; 
                pool.repool(this); 
                synchronized (this){ 
                    //Threads are idle, waiting for new tasks to arrive.
                    wait(); 
                } 
            } catch (InterruptedException e) { 
                e.printStackTrace(); 
            } 
            isIdle = false; 
        } 
    } 
 
    public synchronized void setTarget(Runnable newTarget){ 
        target = newTarget; 
        //After setting up the task, notify the run method to start executing the task.
        notifyAll(); 
    } 
 
    //Close threads
    public synchronized void shutDown(){ 
        isShutDown = true; 
        notifyAll(); 
    } 
 
} 

3. Testing Main Method

public static void main(String[] args) throws InterruptedException { 
       for (int i = 0; i < 1000; i++) { 
           ThreadPool.getInstance().start(new Runnable() { 
               @Override 
               public void run() { 
                   try { 
                       //100 ms dormancy
                       Thread.sleep(100); 
                   } catch (InterruptedException e) { 
                       e.printStackTrace(); 
                   } 
               } 
           }); 
       } 
   } 

03ThreadPoolExecutor

In order to better control multithreading, JDK provides an Executor framework to help developers effectively control threads. Whether the new Fixed ThreadPool () method, the new Single ThreadExecutor () method or the new Cached ThreadPool () method, the internal implementation of the Executor framework uses the ThreadPool Executor:

public static ExecutorService newCachedThreadPool() { 
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 
                                      60L, TimeUnit.SECONDS, 
                                      new SynchronousQueue<Runnable>()); 
    } 
     
    public static ExecutorService newFixedThreadPool(int nThreads) { 
        return new ThreadPoolExecutor(nThreads, nThreads, 
                                      0L, TimeUnit.MILLISECONDS, 
                                      new LinkedBlockingQueue<Runnable>()); 
    } 
     
    public static ExecutorService newSingleThreadExecutor() { 
        return new FinalizableDelegatedExecutorService 
            (new ThreadPoolExecutor(1, 1, 
                                    0L, TimeUnit.MILLISECONDS, 
                                    new LinkedBlockingQueue<Runnable>())); 
    } 

As you can see from the implementation code of the thread pool above, they are just encapsulation of the ThreadPoolExecutor class. Why is the ThreadPoolExecutor class so powerful? Take a look at the most important construction method of ThreadPoolExecutor.

3.1 Construction Method

The most important construction methods of ThreadPool Executor are as follows:

public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) 

The method parameters are as follows:

ThreadPoolExecutor uses an example to submit tasks through the execute() method.

public static void main(String[] args) { 
        ThreadPoolExecutor executor = new ThreadPoolExecutor(4, 5, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>()); 
        for (int i = 0; i < 10; i++) { 
            executor.execute(new Runnable() { 
                @Override 
                public void run() { 
                    System.out.println(Thread.currentThread().getName()); 
                } 
            }); 
        } 
        executor.shutdown(); 
    } 

Or submit tasks through submit()

public static void main(String[] args) throws ExecutionException, InterruptedException { 
        ThreadPoolExecutor executor = new ThreadPoolExecutor(4, 5, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<>()); 
        List<Future> futureList = new Vector<>(); 
        //Perform the following methods 100 times in other threads
        for (int i = 0; i < 100; i++) { 
            futureList.add(executor.submit(new Callable<String>() { 
                @Override 
                public String call() throws Exception { 
                    return Thread.currentThread().getName(); 
                } 
            })); 
        } 
        for (int i = 0;i<futureList.size();i++){ 
            Object o = futureList.get(i).get(); 
            System.out.println(o.toString()); 
        } 
        executor.shutdown(); 
    } 

Operation results:

... 
pool-1-thread-4 
pool-1-thread-3 
pool-1-thread-2 

The following is mainly about the workQueue and Rejected Execution Handler parameters in the construction method of ThreadPool Executor. The other parameters are very simple.

3.2 workQueue task queue

A blocking queue used to save tasks awaiting execution. You can choose the following blocking queues.

  • Array BlockingQueue: A bounded blocking queue based on array structure, sorted according to FIFO principles
  • Linked Blocking Queue: A linked list-based blocking queue with higher throughput than Array Blocking Queue. The static factory method Excutors.newFixedThreadPool() uses this queue
  • SynchronousQueue: A blocking queue that does not store elements. Each insert operation must wait until another thread calls the removal operation, otherwise the insert operation is always blocked and throughput is higher than LinkedBlockingQueue, which is used by the static factory method Excutors.newCachedThreadPool().
  • Priority Blocking Queue: An infinite blocking queue with priority.

3.3 Rejected Execution Handler Saturation Strategy

When the queue and thread pool are full, indicating that the thread pool is saturated, a strategy must be taken to also handle newly submitted tasks. It can have the following four options:

  • AbortPolicy: Throw an exception directly, using this strategy by default
  • CallerRunsPolicy: Running tasks only with the thread in which the caller is located
  • Discard Oldest Policy: Discard the latest task in the queue and execute the current task
  • DiscardPolicy: No disposal, discard

More often, we should customize policies by implementing the RejectedExecutionHandler interface, such as logging or persistent storage.

3.4 submit() and execute()

Two methods, execute and submit, can be used to submit tasks to the thread pool.

The execute method is used to submit tasks that do not require a return value. Tasks submitted in this way cannot be known whether they are performing properly.

The submit method is used to submit a task with a return value, and this method returns a Future type object. This return object can be used to determine whether the task is successful or not, and the return value can be obtained by the future.get() method, which blocks the current thread until the task is completed.

3.5 shutdown() and shutdownNow()

You can close the thread pool by calling shutdown() or shutdownNow(). Their principle is to traverse the worker threads in the thread pool and then call interrupt methods of threads one by one to interrupt threads, so tasks that fail to respond to interrupts may never stop.

The difference between the two methods is that shutdownNow() first sets the state of the thread pool to STOP, then attempts to stop all threads that are executing or suspending tasks, and returns a list of tasks waiting to be executed, while shutdown() simply sets the state of the thread pool to SHUTDOWN state, and then interrupts all threads that are not executing tasks.

The isShutdown method returns true as long as either of the two closing methods is invoked. When all tasks are closed, the thread pool is closed successfully, and then calling the isTerminaced method returns true.

The shutdown() method is usually called to close the thread pool, and the shutdownNow() method can be called if the task is not necessarily finished.

3.6 Reasonable Thread Pool Configuration

To configure thread pools properly, first of all, we need to analyze task characteristics.

  • The nature of tasks: CPU-intensive tasks, IO-intensive tasks and hybrid tasks.
  • Task priority: high, medium and low.
  • Task execution time: long, medium and short.
  • Task Dependence: Dependence on other system resources, such as database connections.

Tasks of different nature can be handled separately by thread pools of different sizes.

CPU-intensive tasks should be configured with as few threads as possible, such as configuring N+1 threads and the number of N-bit CPUs.

While IO-intensive task threads are not always executing tasks, they should be configured as many threads as possible, such as 2*N.

Hybrid tasks can be split into one CPU-intensive task and one IO-intensive task. As long as the time difference between the two tasks is not too large, the throughput of decomposed tasks will be higher than that of serial tasks. If the execution time of these two tasks is very different, there is no need to decompose them. The number of CPUs of the current device can be obtained by Runtime.getRuntime().availableProcessors().

Tasks with different priorities can be processed using the priority queue Priority Blocking Queue. It allows high-priority tasks to be executed first.

3.7 Thread Pool Monitoring

Because thread pools are used extensively, it is necessary to monitor them. You can customize the thread pool by inheriting the thread pool, rewrite the beforeExecute, afterExecute, and terminated methods of the thread pool, and monitor by executing some code before, after and before the thread pool closes. When monitoring thread pools, you can use the following attributes:

(1) taskCount: Number of tasks to be performed by the thread pool

(2) Completed TaskCount: The number of tasks completed by the thread pool during operation is less than or equal to taskCount

(3) largestPoolSize: The largest number of threads ever created in a thread pool. From this data, you can know whether the thread pool has ever been full. If the value is equal to the maximum size of the thread pool, it indicates that the thread pool has been full.

(4) getPoolSize: Number of threads in the thread pool. If the thread pool is not destroyed, the thread in the thread pool will not be destroyed automatically, so the size will only increase.

(5) getActiveCount: Number of threads that get active

04Executor Multithread Framework

ThreadPool Executor represents a thread pool, while Executors class acts as a thread pool factory, through which a thread pool with specific functions can be obtained.

Using the Executors framework to implement the example in the previous section, the code is as follows:

public static void main(String[] args) { 
        //Create a new thread pool
        ExecutorService executor = Executors.newCachedThreadPool(); 
        //Perform the following methods 100 times in other threads
        for (int i = 0; i < 100; i++) { 
            executor.execute(new Runnable() { 
                @Override 
                public void run() { 
                    System.out.println(Thread.currentThread().getName()); 
                } 
            }); 
        } 
        //Closing after execution
        executor.shutdown(); 
    } 

4.1 Structure of Executors Framework

1. task

Includes the interfaces to be implemented by the task: Runnable interface or Callable interface.

2. Implementation of tasks

It includes Executor, the core interface of task execution mechanism, and ExecutorService interface inherited from Executor. The Executor framework has two key classes that implement the Executor Service interface (ThreadPoolExecutor and Scheduled ThreadPoolExecutor).

3. Results of Asynchronous Computing

The FutureTask class includes the interface Future and the FutureTask class that implements the FutureInterface.

4.2 Executors Factory Method

The main methods of the Executors factory class are:

public static ExecutorService newFixedThreadPool(int nThreads)  

This method returns a thread pool with a fixed number of threads, and the number of threads in the thread pool remains unchanged. When a new task is submitted, if there are idle threads in the thread pool, they are executed immediately. If not, the new task will be temporarily stored in a task queue, and the task in the task queue will be processed when the thread is idle.

public static ExecutorService newSingleThreadExecutor() 

This method returns a thread pool with only one thread. If an extra task is submitted to the thread pool, the task will be saved in a task queue, waiting for the thread to be idle, and the tasks in the queue will be executed in FIFO order.

public static ExecutorService newCachedThreadPool() 

This method returns a thread pool that can adjust the number of threads according to the actual situation. The number of threads in the thread pool is uncertain, but if there are idle threads that can be reused, reusable threads will be preferred. But when all threads are working and new tasks are submitted, new threads are created to process tasks. All threads will return to the thread pool for reuse after the current task has been executed.

public static ScheduledExecutorService newSingleThreadScheduledExecutor()  

This method returns a Scheduled ExecutorService object with a thread pool size of 1. The Scheduled ExecutorService interface extends the function of executing a task at a given time, such as after a fixed delay, or periodically, on top of the ExecutorService interface.

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 

This method also returns a Scheduled ExecutorService object, but the thread pool can specify the number of threads.

4.3 ThreadPool Executor and Scheduled ThreadPool Executor

As mentioned earlier, the Executors class acts as a thread pool factory, through which a thread pool with a specific function can be obtained. The main method of the Executors factory class is to create ThreadPoolExecutor and Scheduled ThreadPoolExecutor thread pools.

With regard to ThreadPool Executor, the previous section 3 has been described in detail. Scheduled ThreadPoolExecutor is also an implementation class of the ExecutorService interface, which can run commands after a given delay or execute commands periodically. Scheduled ThreadPool Executor is more flexible and powerful than Timer.

4.4 Future and FutureTask

In the above example, the execute() method is used to submit tasks that do not require a return value. If we need to get the return value after the task is executed, we can use the submit() method.

Sample code:

public static void main(String[] args) throws InterruptedException, ExecutionException { 
        //Create a new thread pool
        ExecutorService executor = Executors.newCachedThreadPool(); 
        List<Future> futureList = new Vector<>(); 
        //Perform the following methods 100 times in other threads
        for (int i = 0; i < 100; i++) { 
            futureList.add(executor.submit(new Callable<String>() { 
                @Override 
                public String call() throws Exception { 
                    return Thread.currentThread().getName()+" "+System.currentTimeMillis()+" "; 
                } 
            })); 
        } 
        for (int i = 0;i<futureList.size();i++){ 
            Object o = futureList.get(i).get(); 
            System.out.println(o.toString()+i); 
        } 
        executor.shutdown(); 
    } 

Operation results:

... 
pool-1-thread-11 1537872778612 96 
pool-1-thread-11 1537872778613 97 
pool-1-thread-10 1537872778613 98 
pool-1-thread-10 1537872778613 99 

At this point, you have to mention the FutureInterface and FutureTask implementation classes, which represent the results of asynchronous computing.

Future<T> submit(Callable<T> task) 
Future<?> submit(Runnable task); 
Future<T> submit(Runnable task, T result); 

When we submit(), we return a Future object to JDK 1.8, which is actually the FutureTask implementation class. The submit() method supports parameters of Runnable or Callable type. The difference between the Runnable interface and the Callable interface is that Runnable does not return results and Callable does.

The main thread can execute the futureTask.get() method to block the current thread until the task is completed, and return the result of the task execution after the task is completed.

The futureTask.get(long timeout, TimeUnit unit) method blocks the current thread from returning immediately for a period of time, when the task may not be completed.

The main thread can also execute futureTask. cancel (boolean mayInterruptIf Running) to cancel the execution of this task.

The futureTask.isCancelled method indicates whether the task was cancelled successfully, and returns true if it was cancelled before the task was completed properly.

The futureTask.isDone method indicates whether the task has been completed or not, and returns true if the task has been completed.

Topics: JDK Database less