Learning Java is bound to avoid the topic of concurrency, and thread related technology is one of the core of Java concurrent programming. Java native support for threads is also very simple to use. In Java, a new Thread() can be used to create a child Thread under the current main Thread, and the JVM will correspond to an operating system level Thread after calling the start() method of that instance, thus completing the creation of a Thread. However, in dachang’s development specifications, such as the famous << Alibaba Java Development Manual >>, it explicitly prohibits the creation of visible threads, that is, the above method of using new Threads (), and instead recommends the use of Thread pools to create threads.

Why use thread pools

Before we talk about the benefits of using thread pools, we can explain the disadvantages of creating threads directly. There are several main points:

  1. The overhead of creating and destroying threads is very high. Re-creating threads every time will increase the system burden and increase the response time.
  2. Manually creating threads is not easy to manage. If the number of threads is not controlled, creating a large number of threads will consume system memory and other resources (each thread needs to allocate stack space independently, the default size is 1 MB, depending on JDK version and OS). In addition, too many threads will waste CPU resources by frequently switching threads.
  3. Thread creation is not uniform across platforms, for example, the maximum limit for thread creation varies between platforms and OS.

Using thread pools can avoid the above problems to a certain extent, bringing a number of benefits:

  1. Reduce resource consumption: Reuse threads through pooling technology, reducing the overhead of thread creation and destruction;
  2. Improve the response speed: reduce the time to obtain threads, improve the response speed;
  3. Improved thread manageability: Threads can be uniformly allocated, tuned, and monitored based on thread pools;
  4. Provide additional power scalability: Thread pools are extensible, allowing developers to add more functionality to them. Such as delay timer thread pool ScheduledThreadPoolExecutor, allows a stay of execution or regular task execution.

Java’s Executor framework

The thread pool implementation in Java is the Executor framework in JUC, whose core implementation class is ThreadPoolExecutor.

Argument parsing

The ThreadPoolExecutor constructor is complex and has seven complete parameters, as follows

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
Copy the code
  1. CorePoolSize is the number of core threads. When submitting a task to the thread pool, if the number of current threads is smaller than the number of core threads, a new thread will be created to execute the task.
  2. MaximumPoolSize, the maximum number of threads that can be created by the thread pool;
  3. KeepAliveTime is the keepAliveTime for idle threads. After timeout, threads are destroyed. This takes effect only when the number of threads in the thread pool is greater than corePoolSize or allowCoreThreadTimeOut() is set to allow idle core threads to timeout.
  4. Unit, unit of thread lifetime;
  5. WorkQueue, a blocking queue that holds tasks waiting to be executed.
  6. ThreadFactory, a threadFactory, is used to create threads and can set related properties for threads, such as setting the thread name, whether it is a daemon thread.
  7. Handler, the reject policy, executes the reject policy when the thread pool exceeds the maximum number of threads and the task queue is full. Java provides four reject policies by default
    1. AbortPolicy: direct selling RejectedExecutionException anomaly, which is also the default thread pool strategy;
    2. CallerRunsPolicy: Use the caller’s thread to execute the task, usually the main thread. Note that this policy will disrupt the execution order of the task post (the task will be executed before other tasks in the queue).
    3. DiscardOldestPolicy: Discards the first entered task in the task queue and executes the current task.
    4. DiscardPolicy: Discards the current task without processing it. Therefore, each of the four rejection strategies has some problems, so we may need to implement RejectedExecutionHandler ourselves in practice.

Method of use

ThreadPoolExecutor is also very simple to use.

  1. Submit tasks There are two methods for submitting tasks to the thread pool. Execute (Runnable Runnable) is used to submit tasks that do not require a return value, and Submit (Callable Callable) is used to submit tasks that require a return value, which is a Future object.
  2. There are also two ways to close a thread pool: shutdown() blocks new tasks from being submitted to the thread pool and shuts them down after the submitted task has finished executing. ShutdownNow () not only blocks new tasks from being submitted, but also “violently” closes tasks that are being executed in the thread pool and removes tasks from the task queue.

Thread pool workflow

Having covered the basic parameters and simple usage of thread pools let’s take a look at the workflow of thread pools.

  1. Before corePoolSize task, a task to create a new thread;
  2. When you come back with a task, you add it to the task queue for all the threads;
  3. If the queue is full, temporary threads are created. If the total number of threads reaches maximumPoolSize, the reject policy is executed. For the specific process, please refer to the following flow chart:

Some problems encountered in practice

  1. Thread pool and ThreadLocal. When using thread pool and ThreadLocal, you need to be aware that threads in the thread pool will be reused, so the values in the ThreadLocal will not conform to expectations.
  2. Executors, although JUC kindly provides Executors factory class, Java development manuals don’t recommend using it. For example, newFixedThreadPool(int n) and newSingleThreadExecutor() used unbound queues (BlockingQueue = integer.max_size) under Executors. In extreme cases, if the load is too large, a large number of tasks will accumulate in the work queue, resulting in OOM. The maximum number of threads set by newCachedThreadPool() is integer.max_size. If the load is so high that the thread pool keeps creating new threads, the JVM will eventually throw an exception because the thread handles run out.

References:

  1. Implementation principle of Java thread pool and its practice in Meituan business
  2. Java concurrent programming practice
  3. The art of Concurrent programming in Java