Multithreading – Thread Pool

1. What is a thread pool

Although the creation of threads is more lightweight than processes, the overhead is still not negligible in the case of frequent creation and destruction, so thread pools are introduced. Thread pool is a form of multi-thread processing. It creates some threads in advance, stores them, and can be used directly when used, which can avoid frequent creation and destruction of threads. Each thread uses the default stack size, runs at the default priority, and is in a multi-threaded apartment. Too many threads will bring scheduling overhead, which will affect cache locality and overall performance. The thread pool maintains multiple threads, waiting for the supervisory manager to assign tasks that can be executed concurrently. This avoids the cost of creating and destroying threads when processing short-term tasks.

2. Introduction to thread pool parameters

The thread pool contains seven parameters:

1. corePoolSize: Number of core threads
2. maximumPoolSize: maximum number of threads
3. keepAliveTime: Maximum idle time
4. TimeUnit unit: time unit
5. BlockingQueue workQueue: blocking queue
6. ThreadFactory threadFactory: Thread factory
7. RejectedExecutionHandler handler: Rejection strategy

3. Thread pool workflow

1. When we submit a task to the thread pool for execution, the thread pool will first determine whether there are idle core threads. If so, let the core thread execute the task. If not, then will be added to the blocking queue

2. When the blocking queue is also filled with tasks, non-core threads will be created to execute new tasks (not taken from the blocking queue, but newly added tasks). tasks)

3. If the number of core threads and non-core threads reaches the maximum number of threads, that is, there are no idle threads at this time, and the blocking queue is full , the thread pool will execute the rejection policy and reject the task.

Question: Why is it that after the core thread is full, the task is first placed in the blocking queue instead of directly creating a non-core thread to execute the task?

Answer: School (thread pool), all math teachers (core threads), math teachers from other schools (non-core threads)
Suppose this is a question-and-answer lecture. When there are too many students asking questions (tasks) in the school, normal people’s thinking is to ask students to queue up and wait for which math teacher in the school is available, instead of directly calling the math teacher from another school. So the logic is the same. Creating threads also consumes resources. It is better to queue the tasks first and then execute them slowly.

4. Advantages of thread pool

1. Reduce resource consumption: Reduce the performance overhead caused by thread creation and destruction.
2. Improve response speed: you can use it directly when a task comes, without waiting for thread creation.
3. Manageability: Carry out unified allocation and monitoring to avoid blocking caused by a large number of threads seizing system resources.

5. Application scenarios of thread pool

1. The scenario requires a large number of processes to complete the task, and the time to complete the task is relatively short. (In this case, after using the thread pool, the time consumed by creating threads can be greatly improved)
2. Applications with demanding performance requirements. For example: requiring the server to respond quickly to customer requests

6. Use Executors to create common thread pools

(1) What are Executors?

Executors are essentially encapsulation of the ThreadPoolExecutor class.

(2) Introduction to using Executors to create common thread pools

1. Use Executors.newFixedThreadPool(n) to create a thread pool with a fixed number of n threads, which will not be destroyed immediately after use;

2. The return value type is ExecutorService;

3. You can register a task to the thread pool through ExecutorService.submit.

public class ThreadPool {
    public static void main(String[] args) {
        ExecutorService pool= Executors.newFixedThreadPool(10);//n=10
        pool.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("Hello");
            }
        });//This code block adds tasks to the thread
    }
}

7. Several ways for Executors to create thread pools

1.newFixedThreadPool: Create a thread pool with a fixed number of threads
2.newCachedThreadPool: Create a thread pool with a dynamically growing number of threads.
3.newSingleThreadExecutor: Create a thread pool containing only a single thread.
4.newScheduledThreadPool: Set the delay time to execute the command, or execute the command regularly. It is an advanced version of Timer.

public static void main(String[] args) {
        
        // 1. Create an operation unbounded queue and fixed-size thread pool
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
       // 2. Create a thread pool with a dynamically growing number of threads
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        // 3. Create a thread pool that operates on an unbounded queue and has only a single worker thread.
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
    
        // 4. Create a thread pool of a specified size, which can be executed after a given time or periodically.
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
     

8. Implement the thread pool yourself – thread pool simulation implementation

1. Use Runnable to describe a task, and use a BlockingQueue blocking queue to organize all tasks (tasks are stored in the blocking queue);

 private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();

2. Use the Worker class to describe a worker thread; (What each worker thread has to do: constantly fetch tasks from the BlockingQueueblocking queue and execute them)

 // Describe the thread, whose function is to obtain tasks from the queue and execute them
    static class Worker extends Thread {

        private BlockingQueue<Runnable> queue = null;//There are several tasks in the current thread pool, all of which hold the above queue and obtain tasks from it.

        public Worker(BlockingQueue<Runnable> queue) {
            this.queue = queue;
        }

        @Override
        public void run() {
            while (true) {
                try {
                    // Loop to obtain the tasks in the task queue. If the queue is empty, it will be blocked. If it is not empty, it will be executed.
                    Runnable runnable = queue.take();
                    // Execute after obtaining
                    runnable.run();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

3. Use the submit method to add tasks;

//Create a method to provide the function of adding tasks
    public void submit (Runnable runnable) {
        try {
            queue.put(runnable);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

4. Organize these threads, and these threads are stored in the List linked list;

 //Organize threads
    private List<Worker> workers = new ArrayList<>();

    // Provide a constructor to receive the number of threads in the thread pool
    public ThreadPool(int num) {
        for (int i = 0; i < num; i + + ) {
            //Create thread
            Worker worker = new Worker(queue);
            //Start thread
            worker.start();
            //Add threads to List for organization and management
            workers.add(worker);
        }
    }

5. CodeTesting

 public static void main(String[] args) {
        //Create thread pool
        ThreadPool pool = new ThreadPool(10);
        //Add task
        for (int i = 0; i < 100; i + + ) {
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello, thread pool");
                }
            });
        }
    }
}

9. Complete code of simulation implementation

 private BlockingDeque<Runnable> queue=new LinkedBlockingDeque<>();
   class Worker extends Thread{
       private BlockingDeque<Runnable> queue=null;
       public Worker(BlockingDeque<Runnable> queue){
           this.queue=queue;
       }

       @Override
       public void run() {
           while (true) {
               try {
                   Runnable runnable = queue.take();
                   runnable.run();
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
           }

       }
   }
   public void submit(Runnable runnable){
       try {
           queue.put(runnable);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
   }
   private List<Worker> workers=new ArrayList<>();
   public ThreadPool(int num){
       for (int i = 0; i < num; i + + ) {
           Worker worker=new Worker(queue);
           worker.start();
           works.add(worker);
       }
   }

    public static void main(String[] args) {
        ThreadPool pool=new ThreadPool(10);
        for (int i = 0; i <100; i + + ) {
            pool.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("hello threadpoll1");
                }
            });
           pool.submit(new Runnable() {
               @Override
               public void run() {
                   System.out.println("hello threadpoll2");
               }
           });
        }
    }
}

Note: The thread pool must specify the maximum number of threads (core threads + temporary threads) maxWorkerCount. If the current number of threads exceeds this maximum number of threads, no new threads will be added. If you forcibly add threads or tasks at this time, you should start the thread pool Rejection strategy~

10. Thread pool rejection policy

Four rejection strategies are provided in the standard library~

1.ThreadPoolExecutor.AbortPolicy: If the blocking queue is full, continue to add tasks and throw an exception directly

2.ThreadPoolExecutor.CallerRunsPolicy: The added new task makes the original thread responsible for executing the task and refuses to add new threads

2.ThreadPoolExecutor.DiscardOldestPolicy: Discard the tasks in the original list and complete the newly added tasks first

4.ThreadPoolExecutor.DiscardPolicy: Regardless of newly added tasks, only execute old tasks that exist in the original list