The use, expansion and optimization of thread pools for concurrent programming

foreword

The multi-threaded software design method can indeed maximize the computing power of modern multi-core processors and improve the throughput and performance of production systems. However, if a system creates a large number of threads at the same time, the overhead caused by frequent context switching between threads will slow down the entire system. In severe cases, it may even cause memory exhaustion to cause OOM exceptions. Therefore, in the actual production environment, the number of threads must be controlled, and blindly creating a large number of new cars is harmful to the system.

So, how can we maximize the use of CPU performance, but also maintain the stability of the system? One way to do this is to use a thread pool.

In short, after using a thread pool, creating a thread handles getting idle threads from the thread pool, and closing a thread becomes returning a thread to the pool. That is, the reuse of threads is improved.

And JDK provides me with ready-made thread pool tools after 1.5, let's learn how to use them today.

  1. What thread pools can the Executors thread pool factory create?
  2. How to manually create a thread pool
  3. How to scale the thread pool
  4. How to optimize the exception information of the thread pool
  5. How to design the number of threads in the thread pool

1. Which thread pools can the Executors thread pool factory create?

Let's start with the simplest example of thread pool usage:

  static class MyTask implements Runnable {

    @Override
    public void run() {
      System.out
          .println(System.currentTimeMillis() + ": Thread ID :" + Thread.currentThread().getId());
      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }

  public static void main(String[] args) {
    MyTask myTask = new MyTask();
    ExecutorService service1 = Executors.newFixedThreadPool(5);
    for (int i = 0; i < 10; i++) {
      service1.submit(myTask);
    }
    service1.shutdown();
  }

operation result:

operation result

We created a thread pool instance, set the default number of threads to 5, submitted 10 tasks to the thread pool, and printed the current millisecond time and thread ID respectively. From the results, we can see that there are 5 with the same ID in the results. The thread prints the millisecond time.

This is the simplest example.

Next, let's talk about other thread creation methods.

1. Fixed thread pool ExecutorService service1 = Executors.newFixedThreadPool(5); This method returns a thread pool with a fixed number of threads. The number of threads in this thread pool is always the same. When a new task is submitted, if there is an idle thread in the thread pool, it will be executed immediately. If not, the new task will be temporarily stored in a task queue (the default unbounded queue int maximum number), when a thread is idle , the task in the task queue is processed.

2. Singleton thread pool ExecutorService service3 = Executors.newSingleThreadExecutor(); This method returns a thread pool with only one thread. If more than one task is submitted to the thread pool, the task will be stored in a task queue (the default unbounded queue int maximum number), and when the thread is idle, the tasks in the queue will be executed in the order of first-in, first-out.

3. Cached thread pool ExecutorService service2 = Executors.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, it will be used first Reusable threads, all threads are working, if a new task is submitted, a new thread will be created to process the task. All threads will return to the thread pool for reuse after the current task is executed.

4. The task calls the thread pool ExecutorService service4 = Executors.newScheduledThreadPool(2); This method also returns a ScheduledThreadPoolExecutor object, the thread pool can specify the number of threads.

The usage of the first three threads is no different, the key is the fourth one. Although there are many thread task scheduling frameworks, we can still learn the thread pool. How to use it? Here's an example:

class A {

  public static void main(String[] args) {
    ScheduledThreadPoolExecutor service4 = (ScheduledThreadPoolExecutor) Executors
        .newScheduledThreadPool(2);

    // 如果前面的任务没有完成,则调度也不会启动
    service4.scheduleAtFixedRate(new Runnable() {
      @Override
      public void run() {
        try {
          // 如果任务执行时间大于间隔时间,那么就以执行时间为准(防止任务出现堆叠)。
          Thread.sleep(10000);
          System.out.println(System.currentTimeMillis() / 1000);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }// initialDelay(初始延迟) 表示第一次延时时间 ; period 表示间隔时间
    }, 0, 2, TimeUnit.SECONDS);


    service4.scheduleWithFixedDelay(new Runnable() {
      @Override
      public void run() {
        try {
          Thread.sleep(5000);
          System.out.println(System.currentTimeMillis() / 1000);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }// initialDelay(初始延迟) 表示延时时间;delay + 任务执行时间 = 等于间隔时间 period
    }, 0, 2, TimeUnit.SECONDS);

    // 在给定时间,对任务进行一次调度
    service4.schedule(new Runnable() {
      @Override
      public void run() {
        System.out.println("5 秒之后执行 schedule");
      }
    }, 5, TimeUnit.SECONDS);
  }
  }

}

The above code creates a ScheduledThreadPoolExecutor task scheduling thread pool, which calls three methods respectively. It is necessary to focus on the explanation of the scheduleAtFixedRate and scheduleWithFixedDelay methods. The functions of these two methods are very similar. The former time interval algorithm is based on the specified period time and the task execution time, and the latter is the specified delay time + task execution time. If students are interested, you can run the above code to see. The same can be seen.

Well, JDK encapsulates 4 methods for creating thread pools for us. However, please note that because these methods are highly encapsulated, if they are used improperly, there will be no way to troubleshoot problems. Therefore, I suggest that programmers should Manually create a thread pool, and the premise of manual creation is a high degree of understanding of the parameter settings of the thread pool. So let's take a look at how to manually create a thread pool.

2. How to manually create a thread pool

The following is a template for manually creating a thread pool:

  /**
   * 默认5条线程(默认数量,即最少数量),
   * 最大20线程(指定了线程池中的最大线程数量),
   * 空闲时间0秒(当线程池梳理超过核心数量时,多余的空闲时间的存活时间,即超过核心线程数量的空闲线程,在多长时间内,会被销毁),
   * 等待队列长度1024,
   * 线程名称[MXR-Task-%d],方便回溯,
   * 拒绝策略:当任务队列已满,抛出RejectedExecutionException
   * 异常。
   */
  private static ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 20, 0L,
      TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>(1024)
      , new ThreadFactoryBuilder().setNameFormat("My-Task-%d").build()
      , new AbortPolicy()
  );

We see that ThreadPoolExecutor , which is the thread pool, has 7 parameters. Let's take a look at it together:

  1. The number of core threads in the corePoolSize thread pool
  2. maximumPoolSize maximum number of threads
  3. keepAliveTime idle time (when the thread pool combing exceeds the number of cores, the survival time of the excess idle time, that is, the idle threads that exceed the number of core threads, will be destroyed within how long)
  4. unit time unit
  5. workQueue When the core thread is full of work, the queue that needs to store tasks
  6. threadFactory creates a factory for threads
  7. handler 's rejection policy when the queue is full

We won't talk about the first few parameters, it's very simple, mainly the last few parameters, queue, thread factory, and rejection policy.

Let's look at the queue first. The thread pool provides 4 queues by default.

  1. Unbounded queue: The default size is int maximum value, so it may exhaust system memory and cause OOM, which is very dangerous.
  2. Directly submitted queue: There is no capacity, it will not be saved, and new threads are created directly, so a large number of thread pools needs to be set. Otherwise, it is easy to implement the rejection strategy, and it is also very dangerous.
  3. Bounded queue: If the core is full, it is stored in the queue. If the core is full and the queue is full, a thread is created until the maximumPoolSize is reached. If the queue is full and the maximum number of threads has been reached, the rejection policy is executed.
  4. Priority queue: Execute tasks according to priority. Size can also be set.

The landlord used an unbounded queue in his own project, but set the task size to 1024. If you have a lot of tasks, it is recommended to divide into multiple thread pools. Don't put your eggs in one basket.

Let's take a look at the rejection policy. What is the rejection policy? When the queue is full, what to do with those tasks that are still submitted. JDK has 4 strategies by default.

  1. AbortPolicy : Throws an exception directly, preventing the system from working properly.
  2. CallerRunsPolicy : As long as the thread pool is not closed, this policy runs the currently discarded task directly in the caller thread. Obviously doing this will not actually drop the task, however, the performance of the task submitting thread will most likely drop drastically.
  3. DiscardOldestPolicy: This policy will discard the oldest request, that is, a task that is about to be executed, and try to submit the current task again.
  4. DiscardPolicy: This policy silently discards tasks that cannot be processed without any processing. If tasks are allowed to be lost, I think this is the best solution.

Of course, if you are not satisfied with the rejection policy provided by JDK, you can implement it yourself, just implement the RejectedExecutionHandler interface and rewrite the rejectedExecution method.

Finally, the thread factory, all threads of the thread pool are created by the thread factory, and the default thread factory is too single, let's see how the default thread factory creates threads:

/**
     * The default thread factory
     */
    static class DefaultThreadFactory implements ThreadFactory {
        private static final AtomicInteger poolNumber = new AtomicInteger(1);
        private final ThreadGroup group;
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String namePrefix;

        DefaultThreadFactory() {
            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGroup() :
                                  Thread.currentThread().getThreadGroup();
            namePrefix = "pool-" +
                          poolNumber.getAndIncrement() +
                         "-thread-";
        }

        public Thread newThread(Runnable r) {
            Thread t = new Thread(group, r,
                                  namePrefix + threadNumber.getAndIncrement(),
                                  0);
            if (t.isDaemon())
                t.setDaemon(false);
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    }

As you can see, the thread name is pool- + thread pool number + -thread- + thread number. Set to non-daemon thread. Priority is default.

What if we want to change the name? Yes, implement the ThreadFactory interface and rewrite the newThread method. But there are already man-made wheels, such as the ThreadFactoryBuilder factory provided by google's guaua used in our example. You can customize the thread name, whether it is guarded, priority, exception handling, etc., with powerful functions.

3. How to expand the thread pool

So can we extend the functionality of the thread pool? For example, record the execution time of thread tasks. In fact, the thread pool of the JDK has reserved interfaces for us. Among the core methods of the thread pool, two methods are empty, which are reserved for us. There is also a method that is called when the thread pool exits. Let's look at an example:

/**
 * 如何扩展线程池,重写 beforeExecute, afterExecute, terminated 方法,这三个方法默认是空的。
 *
 * 可以监控每个线程任务执行的开始和结束时间,或者自定义一些增强。
 *
 * 在 Worker 的 runWork 方法中,会调用这些方法
 */
public class ExtendThreadPoolDemo {

  static class MyTask implements Runnable {

    String name;

    public MyTask(String name) {
      this.name = name;
    }

    @Override
    public void run() {
      System.out
          .println("正在执行:Thread ID:" + Thread.currentThread().getId() + ", Task Name = " + name);
      try {
        Thread.sleep(100);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }


  public static void main(String[] args) throws InterruptedException {
    ExecutorService es = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS,
        new LinkedBlockingQueue<>()) {
      @Override
      protected void beforeExecute(Thread t, Runnable r) {
        System.out.println("准备执行:" + ((MyTask) r).name);
      }

      @Override
      protected void afterExecute(Runnable r, Throwable t) {
        System.out.println("执行完成: " + ((MyTask) r).name);
      }

      @Override
      protected void terminated() {
        System.out.println("线程池退出");
      }
    };

    for (int i = 0; i < 5; i++) {
      MyTask myTask = new MyTask("TASK-GEYM-" + i);
      es.execute(myTask);
      Thread.sleep(10);

    }

    es.shutdown();
  }

}

We override the beforeExecute method, which is called before the task is executed, and the afterExecute method is called after the task is executed. There is also a terminated method, which is called when the thread pool exits. What is the execution result?

As you can see, the before and after methods are called before and after each task is executed. Equivalent to executing a slice. The terminated method is called after the shutdown method is called.

4. How to optimize the exception information of the thread pool

How to optimize the exception information of the thread pool? Before talking about this problem, let's talk about a bug that is not easy to find:

Look at the code:

  public static void main(String[] args) throws ExecutionException, InterruptedException {

    ThreadPoolExecutor executor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 0L,
        TimeUnit.MILLISECONDS, new SynchronousQueue<>());

    for (int i = 0; i < 5; i++) {
      executor.submit(new DivTask(100, i));
    }


  }


  static class DivTask implements Runnable {
    int a, b;

    public DivTask(int a, int b) {
      this.a = a;
      this.b = b;
    }

    @Override
    public void run() {
      double re = a / b;
      System.out.println(re);
    }
  }

Results of the:

Note: There are only 4 results, one of which is swallowed and has no information. why? If you look at the code carefully, you will find that an error will definitely be reported when performing 100/0, but there is no error message, which is a headache. Why? In fact, if you use the execute method, an error message will be printed, and when you use the submit method without calling its get method, the exception will be swallowed because, if an exception occurs, the exception is returned as the return value.

How to do it? Of course we can use the execute method, but we can have another way: rewrite the submit method, the landlord wrote an example, let's take a look:

  static class TraceThreadPoolExecutor extends ThreadPoolExecutor {

    public TraceThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime,
        TimeUnit unit, BlockingQueue<Runnable> workQueue) {
      super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
    }

    @Override
    public void execute(Runnable command) {
//      super.execute(command);
      super.execute(wrap(command, clientTrace(), Thread.currentThread().getName()));
    }

    @Override
    public Future<?> submit(Runnable task) {
//      return super.submit(task);
      return super.submit(wrap(task, clientTrace(), Thread.currentThread().getName()));
    }

    private Exception clientTrace() {
      return new Exception("Client stack trace");
    }


    private Runnable wrap(final Runnable task, final Exception clientStack,
        String clientThreaName) {
      return new Runnable() {
        @Override
        public void run() {
          try {
            task.run();
          } catch (Exception e) {
            e.printStackTrace();
            clientStack.printStackTrace();
            throw e;
          }
        }
      };
    }
  }

We have rewritten the submit method to encapsulate the exception information. If an exception occurs, the stack information will be printed. Let's see what is the result of using the rewritten thread pool?

From the results, we can clearly see the reason for the error message: by zero! And the stack information is clear, which is convenient for troubleshooting. Optimized the default thread pool strategy.

5. How to design the number of threads in the thread pool

The size of the thread pool has a certain impact on the performance of the system. The number of threads that are too large or too small cannot exert optimal system performance, but the determination of the size of the thread pool does not need to be very precise. Because as long as the maximum and minimum conditions are avoided, the impact of the size of the thread pool on performance will not be too great. Generally speaking, the number of CPUs, memory size and other factors need to be considered when determining the size of the thread pool. The book Practice gives an empirical formula for estimating the size of the thread pool:

The formula is still a bit complicated. In short, if you are a CPU-intensive operation, then the number of threads and the number of CPU cores should be the same, avoiding a lot of useless switching thread contexts. If you are IO-intensive, you need a lot of waiting , then the number of threads can be set more, such as CPU cores multiplied by 2.

As for how to get the number of CPU cores, Java provides a method:

Runtime.getRuntime().availableProcessors();

Returns the number of cores of the CPU.

Summarize

Well, here, we have an understanding of how to use the thread pool. Here, the landlord recommends that you create a thread pool manually, so that you can have a precise understanding of the various parameters in the thread pool, and debug or adjust the system. It's good when it's good. For example, set the appropriate number of core threads, the maximum number of threads, the rejection policy, the thread factory, the size and type of the queue, etc. It can also be a custom thread of the thread factory of the G family.

In the next article, we will dive into the source code to see how the JDK's thread pool is implemented. Therefore, first familiarize yourself with the use of thread pools! ! !

good luck!!!

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325388264&siteId=291194637