[fundamentals of java high performance programming] - Application and implementation principle of thread pool

Posted by steanders on Mon, 17 Jan 2022 23:37:45 +0100

Why use thread pools?

Thread pool is a form of multi-threaded processing. Tasks are added to the queue during processing, and then automatically started after threads are created. Thread pool threads are background threads. Each thread uses the default stack size, runs at the default priority, and is in a multithreaded unit. If a thread is idle in managed code (such as waiting for an event), the thread pool inserts another worker thread to keep all processors busy. If all thread pool threads remain busy, but the queue contains pending work, the thread pool will create another worker thread after a period of time, but the number of threads will never exceed the maximum. Threads that exceed the maximum can queue, but they don't start until other threads finish.

Is the number of threads the better?

  1. Thread is not only an object in java, but also a resource of the operating system. It takes time to create and destroy threads. If (creation time + destruction time) > (task execution time), it is not cost-effective.
  2. java objects occupy heap memory, and operating system threads occupy system memory. According to JVM specification, the default maximum stack space of a thread is 1M, which needs to be allocated from system memory. Therefore, too many threads will consume a lot of memory.
  3. When threads execute tasks, the operating system needs to switch thread context frequently. Too many threads will affect performance.

The exit of thread pool is to conveniently control the number of threads.

Principle of thread pool

1. Thread pool manager:
It is used to create and manage thread pools, including creating thread pools, destroying thread pools, and adding new tasks.
2. Worker thread:
Threads in the thread pool are in a waiting state when there are no tasks, and each thread can execute tasks circularly.
3. Task interface:
The interface that each task must implement for the worker thread to schedule the execution of the task;
It mainly specifies the entry of the task, the closing work after the task is executed, the execution status of the task, etc.
4. Task queue:
Used to store unprocessed tasks. Provides a buffering mechanism.

Interface definition and implementation class API of thread pool

The api for operating thread pool in java is located in java util. concurrent; In the bag.
Executor interface
The top-level interface defines the execute() method to execute the task.

ExecutorService interface
It inherits the Executor interface and extends the Callable, Future and close methods.

ScheduledExecutorService interface
It inherits the ExecutorService interface and adds methods related to scheduled tasks.
ThreadPoolExecutor implementation class
Basic and standard thread pool implementation.

ScheduledThreadPoolExecutor implementation class
It inherits ThreadPoolExecutor and implements the methods related to scheduled tasks in ScheduledExecutorService.
The source code analysis of the above interfaces and implementation classes will be supplemented later...

Executors tool class

The Executors class provides a series of factory methods for creating thread pools, and the returned thread pools implement the ExecutorService interface.

  • newFixedThreadPool(int nThreads)
    Create a thread pool with fixed size and unbounded task queue capacity. Number of core threads = maximum number of threads.
  • newCachedThreadPool()
  • Created is an unbounded buffer thread pool. Its task queue is a synchronization queue. Tasks are added to the pool. If there are idle threads in the pool, they will be executed with idle threads. If there is no thread, a new thread will be created for execution. If the thread in the pool is idle for more than 60 seconds, it will be destroyed and released. The number of threads varies with the number of tasks. It is suitable for performing asynchronous tasks that take less time. Number of core threads in the pool = 0, maximum number of threads = integer MAX_ VALUE
  • newSingleThreadExecutor()
    A single thread pool with only one thread to execute unbounded task queues. The thread pool ensures that tasks are executed one by one in the order they are added. When the only thread aborts due to a task exception, a new thread will be created to continue executing subsequent tasks. The difference from newFixedThreadPool(1) is that the pool size of a single thread pool is hard coded in the newSingleThreadExecutor method and cannot be changed.
  • newScheduledThreadPool(int corePoolSize) is a thread pool that can execute tasks regularly. The number of core threads in the pool is specified by the parameter. Maximum number of threads = lnteger MAX_ VALUE.

Analyzing several ways of creating threads in java

Thread pool application example

Created through the ThreadPoolExecutor instance.
1. Create a task and give it to the thread pool for execution

/**
	 * Test: submit 15 tasks with execution time of 3 seconds to see the status of thread pool
	 * 
	 * @param threadPoolExecutor Pass in different thread pools and see different results
	 * @throws Exception
	 */
public void testCommand(ThreadPoolExecutor threadPoolExecutor) throws Exception{
        for(int i=0;i<15;i++){
            int n = i;
            threadPoolExecutor.submit(new Runnable() {
                @Override
                public void run() {
                    try{
                        System.out.println("Start execution" + n);
                        Thread.sleep(3000L);
                        System.out.println("end of execution" + n);
                    }catch (InterruptedException e){
                        e.printStackTrace();
                    }
                }
            });
            System.out.println("Task submitted successfully:" + i);
        }

        Thread.sleep(500L);
        System.out.println("The current number of threads in the thread pool is:" + threadPoolExecutor.getPoolSize());
        System.out.println("The current number of thread pool waits is: " + threadPoolExecutor.getQueue().size());
        Thread.sleep(15000L);
        System.out.println("The current number of threads in the thread pool is:" + threadPoolExecutor.getPoolSize());
        System.out.println("The current number of thread pool waits is:" + threadPoolExecutor.getQueue().size());
    }

2. Create a thread pool to perform the above tasks

    /**
     * Number of core threads: 5
     * Maximum number of threads: 10
     * Thread lifetime exceeding the number of core threads: 5S
     * Queue size: unbounded queue
     * @throws Exception
     */
    public void pool1() throws Exception {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS,
                new LinkedBlockingQueue<Runnable>());
        testCommand(threadPoolExecutor);
    }

Task execute procedure

  1. Is the number of core threads reached? If not, create a worker thread to execute the task.
  2. Is the work queue full? If it is not full, the newly submitted task is stored in the work queue.
  3. Is the maximum number of thread pools reached? If not, a new worker thread is created to execute the task.
  4. Finally, execute the reject policy to handle this task.

Use of various thread pools

/** Use of thread pool */
public class Demo9 {

	/**
	 * Test: submit 15 tasks with execution time of 3 seconds to see the status of thread pool
	 * 
	 * @param threadPoolExecutor Pass in different thread pools and see different results
	 * @throws Exception
	 */
	public void testCommon(ThreadPoolExecutor threadPoolExecutor) throws Exception {
		// Test: submit 15 tasks that take 3 seconds to execute, and see the corresponding processing of 2 tasks that exceed the size
		for (int i = 0; i < 15; i++) {
			int n = i;
			threadPoolExecutor.submit(new Runnable() {
				@Override
				public void run() {
					try {
						System.out.println("Start execution:" + n);
						Thread.sleep(3000L);
						System.err.println("end of execution:" + n);
					} catch (InterruptedException e) {
						e.printStackTrace();
					}
				}
			});
			System.out.println("Task submitted successfully :" + i);
		}
		// View the number of threads and the number of queues waiting
		Thread.sleep(500L);
		System.out.println("The current number of threads in the thread pool is:" + threadPoolExecutor.getPoolSize());
		System.out.println("The current number of thread pool waits is:" + threadPoolExecutor.getQueue().size());
		// Wait for 15 seconds to check the number of threads and queues (theoretically, it will be automatically destroyed by threads exceeding the number of core threads)
		Thread.sleep(15000L);
		System.out.println("The current number of threads in the thread pool is:" + threadPoolExecutor.getPoolSize());
		System.out.println("The current number of thread pool waits is:" + threadPoolExecutor.getQueue().size());
	}

	/**
	 * 1,Thread pool information: the number of core threads is 5, the maximum number is 10, unbounded queue, the survival time of threads exceeding the number of core threads is 5 seconds, and the number of rejection policies is specified
	 * 
	 * @throws Exception
	 */
	private void threadPoolExecutorTest1() throws Exception {
		ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS,
				new LinkedBlockingQueue<Runnable>());

		testCommon(threadPoolExecutor);
		// Expected result: the number of threads in the thread pool is 5. For tasks exceeding the number, others enter the queue and wait to be executed
	}

	/**
	 * 2, Thread pool information: the number of core threads is 5, the maximum number is 10, the queue size is 3, the survival time of threads exceeding the number of core threads is 5 seconds, and the number of rejection policies is specified
	 * 
	 * @throws Exception
	 */
	private void threadPoolExecutorTest2() throws Exception {
		// Create a thread pool with 5 core threads, a maximum of 10, and a maximum of 3 waiting queues, that is, a maximum of 13 tasks.
		// The default policy is to throw RejectedExecutionException, Java util. concurrent. ThreadPoolExecutor. AbortPolicy
		ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS,
				new LinkedBlockingQueue<Runnable>(3), new RejectedExecutionHandler() {
					@Override
					public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
						System.err.println("A task was refused");
					}
				});
		testCommon(threadPoolExecutor);
		// Expected results:
		// 1. Five tasks are directly assigned to the thread to start execution
		// 2. 3 tasks enter the waiting queue
		// 3. If the queue is not enough, temporarily add 5 threads to execute the task (destroy if there is no work in 5 seconds)
		// 4. The queue and thread pool are full. There are two tasks left. They have no resources and are rejected for execution.
		// 5. The task is executed. After 5 seconds, if there is no task to execute, destroy the 5 temporarily created threads
	}

	/**
	 * 3, Thread pool information: the number of core threads is 5, the maximum number is 5, the queue is unbounded, and the survival time of threads exceeding the number of core threads is 5 seconds
	 * 
	 * @throws Exception
	 */
	private void threadPoolExecutorTest3() throws Exception {
		// And executors Newfixedthreadpool (int nthreads) is the same
		ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS,
				new LinkedBlockingQueue<Runnable>());
		testCommon(threadPoolExecutor);
		// Expected result: the number of threads in the thread pool is 5. For tasks exceeding the number, others enter the queue and wait to be executed
	}

	/**
	 * 4, Thread pool information:
	 * Number of core threads 0, maximum number integer MAX_ Value, SynchronousQueue queue, thread lifetime exceeding the number of core threads: 60 seconds
	 * 
	 * @throws Exception
	 */
	private void threadPoolExecutorTest4() throws Exception {

		// Synchronous queue, in fact, is not a real queue because it does not maintain storage space for the elements in the queue. Unlike other queues, it maintains a set of threads waiting to add or remove elements from the queue.
		// When the client code submits a task to the thread pool on the premise that the SynchronousQueue is used as the work queue,
		// There are no idle threads in the thread pool that can take a task from the SynchronousQueue queue instance,
		// Then the corresponding offer method call will fail (that is, the task is not stored in the work queue).
		// At this time, ThreadPoolExecutor will create a new worker thread to process the queued failed task (assuming that the size of the thread pool has not reached its maximum thread pool size at this time).

		// And executors Newcachedthreadpool()
		ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
				new SynchronousQueue<Runnable>());
		testCommon(threadPoolExecutor);
		// Expected results:
		// 1. The number of threads in the thread pool is 15. For tasks exceeding the number, others enter the queue and wait to be executed
		// 2. After all tasks are executed, 60 seconds later, if no tasks can be executed, all threads will be destroyed and the size of the pool will be restored to 0
		Thread.sleep(60000L);
		System.out.println("60 Seconds later, look at the number of threads in the thread pool:" + threadPoolExecutor.getPoolSize());
	}

	/**
	 * 5, Regularly execute the thread pool information: execute it after 3 seconds, one-time task, and execute it at the point < br / >
	 * The number of core threads is 5, and the maximum number is integer MAX_ Value, DelayedWorkQueue, delay queue, thread survival time exceeding the number of core threads: 0 seconds
	 * 
	 * @throws Exception
	 */
	private void threadPoolExecutorTest5() throws Exception {
		// And executors Same as newscheduledthreadpool()
		ScheduledThreadPoolExecutor threadPoolExecutor = new ScheduledThreadPoolExecutor(5);
		threadPoolExecutor.schedule(new Runnable() {
			@Override
			public void run() {
				System.out.println("The task was executed, now time:" + System.currentTimeMillis());
			}
		}, 3000, TimeUnit.MILLISECONDS);
		System.out.println(
				"Scheduled task, submitted successfully on:" + System.currentTimeMillis() + ", Number of threads in the current thread pool:" + threadPoolExecutor.getPoolSize());
		// Expected result: the task is executed once in 3 seconds
	}

	/**
	 * 6, Scheduled execution thread pool information: fixed number of threads 5, < br / >
	 * The number of core threads is 5, and the maximum number is integer MAX_ Value, DelayedWorkQueue, delay queue, thread survival time exceeding the number of core threads: 0 seconds
	 * 
	 * @throws Exception
	 */
	private void threadPoolExecutorTest6() throws Exception {
		ScheduledThreadPoolExecutor threadPoolExecutor = new ScheduledThreadPoolExecutor(5);
		// When a task is executed periodically, the thread pool provides two scheduling methods, which are demonstrated separately here. The test scenario is the same.
		// Test scenario: the submitted task takes 3 seconds to complete. Look at the difference between two different scheduling methods
		// Effect 1: after submission, the first execution starts 2 seconds later, and then it is executed once every 1 second (if it is found that the last execution has not been completed, wait for it to be completed, and execute it immediately after completion).
		// In other words, in this code, it is executed every 3 seconds (calculation method: three seconds for each execution, with an interval of 1 second. The next execution starts immediately after the execution, without waiting)
		threadPoolExecutor.scheduleAtFixedRate(new Runnable() {
			@Override
			public void run() {
				try {
					Thread.sleep(3000L);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println("task-1 Executed, current time:" + System.currentTimeMillis());
			}
		}, 2000, 1000, TimeUnit.MILLISECONDS);

		// Effect 2: after submission, the first execution will start 2 seconds later, and then it will be executed once every 1 second (if it is found that the last execution has not been completed, wait until it is completed, and then start timing after the last execution, wait for 1 second).
		// In other words, the effect of this code clock is to execute every 4 seconds. (calculation method: 3 seconds for each execution, with an interval of 1 second. After execution, wait for 1 second, so it is 3 + 1)
		threadPoolExecutor.scheduleWithFixedDelay(new Runnable() {
			@Override
			public void run() {
				try {
					Thread.sleep(3000L);
				} catch (InterruptedException e) {
					e.printStackTrace();
				}
				System.out.println("task-2 Executed, current time:" + System.currentTimeMillis());
			}
		}, 2000, 1000, TimeUnit.MILLISECONDS);
	}

	/**
	 * 7, Thread termination: thread pool information: the number of core threads is 5, the maximum number is 10, the queue size is 3, the survival time of threads exceeding the number of core threads is 5 seconds, and the number of rejection policies is specified
	 * 
	 * @throws Exception
	 */
	private void threadPoolExecutorTest7() throws Exception {
		// Create a thread pool with 5 core threads, a maximum of 10, and a maximum of 3 waiting queues, that is, a maximum of 13 tasks.
		// The default policy is to throw RejectedExecutionException, Java util. concurrent. ThreadPoolExecutor. AbortPolicy
		ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS,
				new LinkedBlockingQueue<Runnable>(3), new RejectedExecutionHandler() {
					@Override
					public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
						System.err.println("A task was refused");
					}
				});
		// Test: submit 15 tasks that take 3 seconds to execute, and see the corresponding processing of 2 tasks that exceed the size
		for (int i = 0; i < 15; i++) {
			int n = i;
			threadPoolExecutor.submit(new Runnable() {
				@Override
				public void run() {
					try {
						System.out.println("Start execution:" + n);
						Thread.sleep(3000L);
						System.err.println("end of execution:" + n);
					} catch (InterruptedException e) {
						System.out.println("Exception:" + e.getMessage());
					}
				}
			});
			System.out.println("Task submitted successfully :" + i);
		}
		// Thread pool terminated in 1 second
		Thread.sleep(1000L);
		threadPoolExecutor.shutdown();
		// Failed to submit prompt again
		threadPoolExecutor.submit(new Runnable() {
			@Override
			public void run() {
				System.out.println("Append a task");
			}
		});
		// Result analysis
		// 1. 10 tasks were executed, 3 tasks entered the queue for waiting, and 2 tasks were rejected for execution
		// 2. After calling shutdown, do not receive new tasks and wait for 13 tasks to finish executing
		// 3. After the process pool is closed, the added task cannot be submitted again and will be rejected for execution
	}

	/**
	 * 8, Terminate threads immediately: thread pool information: the number of core threads is 5, the maximum number is 10, the queue size is 3, the survival time of threads exceeding the number of core threads is 5 seconds, and the number of reject policies is specified
	 * 
	 * @throws Exception
	 */
	private void threadPoolExecutorTest8() throws Exception {
		// Create a thread pool with 5 core threads, a maximum of 10, and a maximum of 3 waiting queues, that is, a maximum of 13 tasks.
		// The default policy is to throw RejectedExecutionException, Java util. concurrent. ThreadPoolExecutor. AbortPolicy
		ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS,
				new LinkedBlockingQueue<Runnable>(3), new RejectedExecutionHandler() {
					@Override
					public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
						System.err.println("A task was refused");
					}
				});
		// Test: submit 15 tasks that take 3 seconds to execute, and see the corresponding processing of 2 tasks that exceed the size
		for (int i = 0; i < 15; i++) {
			int n = i;
			threadPoolExecutor.submit(new Runnable() {
				@Override
				public void run() {
					try {
						System.out.println("Start execution:" + n);
						Thread.sleep(3000L);
						System.err.println("end of execution:" + n);
					} catch (InterruptedException e) {
						System.out.println("Exception:" + e.getMessage());
					}
				}
			});
			System.out.println("Task submitted successfully :" + i);
		}
		// Thread pool terminated in 1 second
		Thread.sleep(1000L);
		List<Runnable> shutdownNow = threadPoolExecutor.shutdownNow();
		// Failed to submit prompt again
		threadPoolExecutor.submit(new Runnable() {
			@Override
			public void run() {
				System.out.println("Append a task");
			}
		});
		System.out.println("Unfinished tasks are:" + shutdownNow.size());

		// Result analysis
		// 1. 10 tasks were executed, 3 tasks entered the queue for waiting, and 2 tasks were rejected for execution
		// 2. After calling shutdown now, 3 threads in the queue will no longer execute and 10 threads will be terminated
		// 3. After the process pool is closed, the added task cannot be submitted again and will be rejected for execution
	}

	public static void main(String[] args) throws Exception {
		new Demo9().threadPoolExecutorTest1();
//		new Demo9().threadPoolExecutorTest2();
//		new Demo9().threadPoolExecutorTest3();
//		new Demo9().threadPoolExecutorTest4();
//		new Demo9().threadPoolExecutorTest5();
//		new Demo9().threadPoolExecutorTest6();
//		new Demo9().threadPoolExecutorTest7();
//		new Demo9().threadPoolExecutorTest8();
	}
}

Topics: Java Back-end thread thread pool