Introduction to Thread Pool Use

Keywords: Java Mobile

Reasons for introducing thread pools

Usually when we need to use threads to accomplish a task, we create a thread, which is usually written as follows:

Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        // TODO
    }
});
thread.start();

This operation is direct and simple, of course, there is no mistake, but there are some problems. It is sufficient to deal with the situation that some threads are not concurrent, but if the number of concurrent threads is large, the efficiency of the system will be reduced. The main impact will be as follows:

  • Frequently creating and destroying threads takes up a lot of unnecessary system processing time, affecting performance.
  • Frequent creation and destruction of threads can easily lead to frequent execution of GC, resulting in memory jitter, resulting in jamming of mobile devices.
  • A large number of threads concurrently consume memory, which is easy to cause OOM problems.
  • Not conducive to expansion, such as timed execution, periodic execution, thread interruption.

The solution to the above problem is to introduce the concept of thread pool. Thread pools allow threads to be reused, and they do not destroy threads after tasks are executed, but continue to perform other tasks. This can effectively reduce and control the number of creating threads, prevent concurrent threads from excessive, excessive memory consumption, thereby improving the performance of the system.

At the same time, thread pool can also easily control the number of concurrent threads, thread timing tasks, single thread sequential execution and so on.

ExecutorService interface

ExecutorService is commonly referred to as thread pool interface. It inherits Executor interface. It also provides some methods to manage termination and to track the execution status of one or more asynchronous task threads and generate Future s.

The real thread pool is ThreadPool Executor, which implements the Executor Service interface and encapsulates a series of interfaces to make it have the characteristics of thread pool.

Thread pool: ThreadPool Executor

After looking at the source code, we found that ThreadPoolExecutor has four constructors, all of which call one of them for initialization. The code is as follows:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) { ... }
parameter Effect
corePoolSize Number of core threads in thread pool
maximumPoolSize Maximum number of threads in thread pool
keepAliveTime Redundant idle threads that exceed the number of core threads are destroyed if they have no tasks in excess of keepAliveTime
unit Time unit of keep AliveTime
workQueue Task queues are used to store tasks submitted but not executed. Queuing strategies adopted by different thread pools are different.
threadFactory Thread factory, used to create threads in thread pools
handler A strategy for rejecting tasks when the maximum number of threads and task queues are saturated and new tasks are unacceptable

Thread pools with five different functions

As you can see, it is not easy to create a ThreadPoolExecutor object, so it is generally recommended to use the factory method of factory class Executors to create thread pool objects. Executors provides the following five different thread pools:

1. New Fixed ThreadPool

Create a thread pool with a fixed number of threads. Each time a task is submitted, a thread is created until the set number of threads is reached, after which the number of threads in the thread pool does not change. When a new task is submitted, if there is an idle thread, the idle thread will process the task. Otherwise, the task will be stored in the task queue. Once a thread is idle, the tasks in the queue will be processed according to FIFO. This thread pool is suitable for some stable regular concurrent threads, mostly for servers.

Definition:

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

Running examples:

ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 1; i <= 10; i++) {
    final int index = i;
    fixedThreadPool.execute(new Runnable() {
        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            System.out.println("Thread: " + threadName + ", running Task" + index);
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
}

Operation results:

02-24 08:15:20 I/System.out: Thread: pool-1-thread-1, running Task1
02-24 08:15:20 I/System.out: Thread: pool-1-thread-2, running Task2
02-24 08:15:20 I/System.out: Thread: pool-1-thread-3, running Task3
02-24 08:15:23 I/System.out: Thread: pool-1-thread-1, running Task4
02-24 08:15:23 I/System.out: Thread: pool-1-thread-2, running Task5
02-24 08:15:23 I/System.out: Thread: pool-1-thread-3, running Task6
02-24 08:15:26 I/System.out: Thread: pool-1-thread-1, running Task7
02-24 08:15:26 I/System.out: Thread: pool-1-thread-2, running Task8
02-24 08:15:26 I/System.out: Thread: pool-1-thread-3, running Task9
02-24 08:15:29 I/System.out: Thread: pool-1-thread-1, running Task10

Observation thread name found that only three thread processing tasks were created, when all threads were running, the resubmitted tasks would enter the waiting, and the tasks in the waiting queue would be reused after the three threads were processed, so the observation time found that each time three tasks were running at the same time, after 3 seconds interval, the next three tasks were run, and the tasks were executed. Order is the order of submission.

2. Cached ThreadPool

Create 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, new threads are created when needed, and idle threads are reused when threads are idle. Generally, programs that perform a large number of short-term asynchronous tasks can improve their efficiency.

Of course, there will not be more and more threads in the thread pool. Each thread has a parameter to set the time to keep active. Once the idle time of the thread exceeds that time, the thread will be destroyed immediately. The default time to keep active is 60 seconds. We can see this default parameter in its definition.

Definition:

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}

Running examples:

ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 1; i <= 10; i++) {
    try {
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    final int index = i;
    cachedThreadPool.execute(new Runnable() {
        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            System.out.println("Thread: " + threadName + ", running Task" + index);
            try {
                Thread.sleep(index * 500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
}

Operation results:

02-24 08:25:29 I/System.out: Thread: pool-1-thread-1, running Task1
02-24 08:25:30 I/System.out: Thread: pool-1-thread-1, running Task2
02-24 08:25:31 I/System.out: Thread: pool-1-thread-2, running Task3
02-24 08:25:32 I/System.out: Thread: pool-1-thread-1, running Task4
02-24 08:25:33 I/System.out: Thread: pool-1-thread-2, running Task5
02-24 08:25:34 I/System.out: Thread: pool-1-thread-1, running Task6
02-24 08:25:35 I/System.out: Thread: pool-1-thread-3, running Task7
02-24 08:25:36 I/System.out: Thread: pool-1-thread-2, running Task8
02-24 08:25:37 I/System.out: Thread: pool-1-thread-1, running Task9
02-24 08:25:38 I/System.out: Thread: pool-1-thread-4, running Task10

We allow tasks to be submitted one second apart, and the time of each task increases gradually. As a result, a task is executed every 1 second. At first, a thread can be processed. But as the task time increases, the thread pool creates new threads to handle those submitted tasks. When the previous threads are processed, tasks are also assigned. Execution. Finally, four threads were created.

3. Single thread newSingle ThreadExecutor

Create a thread pool with only one thread. Only one thread task can be executed at a time, and the redundant tasks will be saved to a task queue. When the thread is idle, the tasks in the queue will be executed in FIFO order.

Definition:

public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>(),
                                threadFactory));
}

Running examples:

ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 1; i <= 10; i++) {
    final int index = i;
    singleThreadExecutor.execute(new Runnable() {
        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            System.out.println("Thread: " + threadName + ", running Task" + index);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
}

Operation results:

02-24 09:00:18 I/System.out: Thread: pool-1-thread-1, running Task1
02-24 09:00:19 I/System.out: Thread: pool-1-thread-1, running Task2
02-24 09:00:20 I/System.out: Thread: pool-1-thread-1, running Task3
02-24 09:00:21 I/System.out: Thread: pool-1-thread-1, running Task4
02-24 09:00:22 I/System.out: Thread: pool-1-thread-1, running Task5
02-24 09:00:23 I/System.out: Thread: pool-1-thread-1, running Task6
02-24 09:00:24 I/System.out: Thread: pool-1-thread-1, running Task7
02-24 09:00:25 I/System.out: Thread: pool-1-thread-1, running Task8
02-24 09:00:26 I/System.out: Thread: pool-1-thread-1, running Task9
02-24 09:00:27 I/System.out: Thread: pool-1-thread-1, running Task10

The result is obvious: from start to finish, only one thread executes, and the thread executes the submitted threads sequentially.

4. Scheduled thread pool new Scheduled ThreadPool

Create a thread pool of specified sizes that can schedule threads to execute according to schedule delays, or periodically.

Running examples:

System.out.println("Start Task");
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
for (int i = 1; i <= 10; i++) {
    final int index = i;
    scheduledThreadPool.schedule(new Runnable() {
        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            System.out.println("Thread: " + threadName + ", running Task" + index);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }, 3, TimeUnit.SECONDS);
}

Operation results:

02-24 09:47:45 I/System.out: Start Task
02-24 09:47:48 I/System.out: Thread: pool-1-thread-1, running Task1
02-24 09:47:48 I/System.out: Thread: pool-1-thread-2, running Task2
02-24 09:47:48 I/System.out: Thread: pool-1-thread-3, running Task3
02-24 09:47:50 I/System.out: Thread: pool-1-thread-1, running Task5
02-24 09:47:50 I/System.out: Thread: pool-1-thread-2, running Task4
02-24 09:47:50 I/System.out: Thread: pool-1-thread-3, running Task6
02-24 09:47:52 I/System.out: Thread: pool-1-thread-1, running Task7
02-24 09:47:52 I/System.out: Thread: pool-1-thread-3, running Task8
02-24 09:47:52 I/System.out: Thread: pool-1-thread-2, running Task9
02-24 09:47:54 I/System.out: Thread: pool-1-thread-1, running Task10

It works basically like the new Fixed ThreadPool, but there is a three-second delay at the start of the run.

5. Scheduled singleton thread new Single Thread Scheduled Executor

Create a thread pool that contains only one thread and can schedule threads to execute according to schedule delays or cycles.

Running examples:

System.out.println("Start Task");
ScheduledExecutorService singleThreadScheduledExecutor = Executors.newSingleThreadScheduledExecutor();
singleThreadScheduledExecutor.scheduleAtFixedRate(new Runnable() {
    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        System.out.println("Thread: " + threadName + ", running Task1");
    }
}, 1, 2, TimeUnit.SECONDS);

Operation results:

02-24 10:01:36 I/System.out: Start Task
02-24 10:01:37 I/System.out: Thread: pool-1-thread-1, running Task
02-24 10:01:39 I/System.out: Thread: pool-1-thread-1, running Task
02-24 10:01:41 I/System.out: Thread: pool-1-thread-1, running Task
02-24 10:01:43 I/System.out: Thread: pool-1-thread-1, running Task
02-24 10:01:45 I/System.out: Thread: pool-1-thread-1, running Task
02-24 10:01:47 I/System.out: Thread: pool-1-thread-1, running Task
02-24 10:01:49 I/System.out: Thread: pool-1-thread-1, running Task
02-24 10:02:51 I/System.out: Thread: pool-1-thread-1, running Task
02-24 10:02:53 I/System.out: Thread: pool-1-thread-1, running Task
02-24 10:02:55 I/System.out: Thread: pool-1-thread-1, running Task

It works basically like the new Single ThreadExecutor, but when it starts running, it has a second delay and periodically executes tasks every two seconds.

Custom thread pool

If you look closely at the definitions of the five thread pools above, you will find that the functions of the thread pools are different, depending on the internal BlockingQueue. If we want to implement custom thread pools with different functions, we can start with BlockingQueue.

For example, there is now an implementation class of BlockingQueue, PriorityBlockingQueue, which can sort queues by priority, so we can use it to implement a thread pool that processes tasks by priority.

First, an abstract class Priority Runnable, which implements the interface between Runnable and Comparable, is created to store tasks. Comparable interface is implemented to compare priorities.

public abstract class PriorityRunnable implements Runnable, Comparable<PriorityRunnable> {
    private int mPriority;

    public PriorityRunnable(int priority) {
        if (priority < 0 ) {
            throw new IllegalArgumentException();
        }
        mPriority = priority;
    }

    @Override
    public int compareTo(PriorityRunnable runnable) {
        int otherPriority = runnable.getPriority();
        if (mPriority < otherPriority) {
            return 1;
        } else if (mPriority > otherPriority) {
            return -1;
        } else {
            return 0;
        }
    }

    public int getPriority() {
        return mPriority;
    }
}

Then you can create a thread pool based on Priority BlockingQueue implementation, whose core number is defined as 3 to facilitate testing results. Then create 10 Priority Runnable tasks with increased priorities and submit them to the thread pool for execution.

ExecutorService threadPool = new ThreadPoolExecutor(3, 3, 0L, TimeUnit.SECONDS, new PriorityBlockingQueue<Runnable>());
for (int i = 1; i <= 10; i++) {
    final int priority = i;
    threadPool.execute(new PriorityRunnable(priority) {
        @Override
        public void run() {
            String name = Thread.currentThread().getName();
            System.out.println("Thread: " + name + ", running PriorityTask" + priority);
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    });
}

Operation results:

02-25 11:34:45 I/System.out: Thread: pool-1-thread-1, running PriorityTask1
02-25 11:34:45 I/System.out: Thread: pool-1-thread-2, running PriorityTask2
02-25 11:34:45 I/System.out: Thread: pool-1-thread-3, running PriorityTask3
02-25 11:34:47 I/System.out: Thread: pool-1-thread-1, running PriorityTask10
02-25 11:34:47 I/System.out: Thread: pool-1-thread-2, running PriorityTask9
02-25 11:34:47 I/System.out: Thread: pool-1-thread-3, running PriorityTask8
02-25 11:34:49 I/System.out: Thread: pool-1-thread-1, running PriorityTask7
02-25 11:34:49 I/System.out: Thread: pool-1-thread-2, running PriorityTask6
02-25 11:34:49 I/System.out: Thread: pool-1-thread-3, running PriorityTask5
02-25 11:34:51 I/System.out: Thread: pool-1-thread-1, running PriorityTask4

As you can see, the results are very similar to those of the new Fixed ThreadPool. The only difference is that the waiting task is not executed in FIFO mode, but the task with higher priority is executed first.

Thread pool optimization

Thread pool can customize the number of internal threads. The number of defined threads affects the performance of the system. If the definition is too large to waste resources, the concurrency of defined threads is too low and the processing speed is too low, it is important to set the number of threads reasonably. Usually we need to consider the core number of CPU s, the size of memory, the number of concurrent requests and other factors.

Usually the number of core threads can be set to CPU core + 1, and the maximum number of threads can be set to CPU core * 2 + 1.

The method of obtaining the CPU core number is as follows:

Runtime.getRuntime().availableProcessors();

Call mode of thread pool

The following methods can be invoked thread pool execution, but the effect is different, you can refer to the document or source code to understand:

  • void execute(Runnable command);
  • Future<?> submit(Runnable task);
  • <T> Future<T> submit(Runnable task, T result);
  • <T> Future<T> submit(Callable<T> task);
  • <T> T invokeAny(Collection<? extends Callable<T>> tasks);
  • <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)

Thread pool closure

void shutdown();

This method no longer accepts new task submissions, and allows previously submitted tasks to be executed before terminating the thread pool.

List<Runnable> shutdownNow();

This method no longer accepts new task submissions, and removes tasks from the task queue directly, trying to stop the task being executed. Returns the waiting task list.

Posted by pagod on Fri, 05 Apr 2019 14:51:30 -0700