Play with Java thread pool: how to build a thread pool?

Keywords: Java network REST JDK

0 core problem of creating thread pool

According to one of Alibaba's Java development specifications,

This rule points out that when we want to use the thread pool, we'd better not be lazy. We'd better create the thread pool by ourselves manually. Then the problem is, how to create the thread pool manually?

1 how many core threads should I create?

1.1 why do we set the appropriate number of threads?

In order to drain the performance of the hardware, we know that when a program removes the time of network transmission on the server, the rest is the time of "calculation" and "I/O". The "I/O" here includes not only the time of data exchange with main memory and auxiliary memory, but also the kernel space of network data transmission to the server and server copy. We set the appropriate number of threads to make full use of each CPU and maximize the efficiency of disk data exchange.

1.2 discussion by type of procedure

  1. For example, to calculate the addition of 100000 random numbers is a real and computationally intensive task.
  2. For example, file upload is a typical I/O intensive task.
  3. There is also the third type of "I/O, computing hybrid tasks", that is, most of the current programs belong to this type, and both time-consuming tasks are involved.

Let's discuss one by one

  1. If it is a compute intensive task, the number of threads set is: server CPU + 1.
    Why? Because if a task is computationally intensive, the most ideal situation is that all CPUs are full, so that the resources of each CPU are fully utilized. As for why the number of CPUs needs to be + 1, the popular explanation on the Internet is that even for CPU intensive tasks, the execution thread may also wait for a certain reason at a certain time (such as page missing interrupt, etc.).
  2. If it is an IO intensive task, the best way is to calculate the time ratio between IO and calculation. If CPU calculation and I/O operation take 1:2 time, the appropriate thread is 3. As for why... This is illustrated by the following figure

    Image from: Geek time - 10 | Java thread (middle): how many threads are created is appropriate?

Therefore, if it is a CPU, the appropriate number of threads is:

1 + (IO time / CPU time)

But now they are all multi-core CPU s, so the appropriate number of threads is:

Number of CPUs * (when there is only one CPU, the appropriate number of threads)

Of course, it's difficult for us to calculate the time ratio between IO and computation perfectly every time in our work, so many predecessors have a more general thread count based on their work experience. For I/O intensive applications, the best thread count is: 2 * CPU core count + 1.
Note that there is also an important point here, that is, if you have more threads, the cost of thread switching will be more, so the number of core threads here is set to 1, so that we can ensure that we can use a small number of threads to complete the task in the process of task submission.

  1. If it is a hybrid task, then we will divide the task into computational task and IO task, and submit these subtasks to their own type of thread pool for execution.

2. Create thread factory by default or implement by yourself?

2.1 create with Guava's

@Slf4j
public class ThreadPoolExecutorDemo00 {
	public static void main(String[] args) {
		ThreadFactoryBuilder threadFactoryBuilder = new ThreadFactoryBuilder()
			.setNameFormat("My threads %d");
		ThreadPoolExecutor executor = new ThreadPoolExecutor(10,
															 10,
															 60,
															 TimeUnit.MINUTES,
															 new ArrayBlockingQueue<>(100),
															 threadFactoryBuilder.build());
		IntStream.rangeClosed(1, 1000)
			.forEach(i -> {
				executor.submit(() -> {
					log.info("id: {}", i);
				});
			});
		executor.shutdown();
	}
}

To see the effect:

2.2 implement ThreadFactory by yourself

public static void main(String[] args) {
	ThreadPoolExecutor executor = new ThreadPoolExecutor(10,
														 10,
														 60,
														 TimeUnit.MINUTES,
														 new ArrayBlockingQueue<>(100),
														 new MyNewThreadFactory("My thread pool"));
	IntStream.rangeClosed(1, 1000)
		.forEach(i -> {
			executor.submit(() -> {
				log.info("id: {}", i);
			});
		});
	executor.shutdown();
}
public static class MyNewThreadFactory implements ThreadFactory {
	private static final AtomicInteger poolNumber = new AtomicInteger(1);
	private final ThreadGroup group;
	private final String namePrefix;
	MyNewThreadFactory(String whatFeatureOfGroup) {
		SecurityManager s = System.getSecurityManager();
		group = (s != null) ? s.getThreadGroup() :
		Thread.currentThread().getThreadGroup();
		namePrefix = "From MineNewThreadFactory-" + whatFeatureOfGroup + "-worker-thread-";
	}

	@Override
	public Thread newThread(Runnable r) {
		String name = namePrefix + poolNumber.getAndIncrement();
		Thread thread = new Thread(group, r,
								   name,
								   0);
		if (thread.isDaemon()) {
			thread.setDaemon(false);
		}

		if (thread.getPriority() != Thread.NORM_PRIORITY) {
			thread.setPriority(Thread.NORM_PRIORITY);
		}
		return thread;
	}
}

To see the effect:

3 which is the rejection strategy?

3.1 let's first look at four basic rejection strategies

  • (1) CallerRunsPolicy: the task is not executed by the thread pool, but by the main thread of the submitting thread. See the code:

    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
         if (!e.isShutdown()) {
             r.run();
         }
    }
    
  • (2) AbortPolicy: throw the exception directly. This is also the default rejection policy. See the code directly:

    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
      throw new RejectedExecutionException("Task " + r.toString() +
                                           " rejected from " +
                                           e.toString());
     }
    
  • (3) DiscardPolicy: simply refuse and do nothing else. See the code:

     public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
     }
    
  • (4) Discard oldestpolicy: discard the oldest task. It's actually easy to understand. It's better to understand the source code directly

    public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
       if (!e.isShutdown()) {
            e.getQueue().poll();
            e.execute(r);
        }
    }
    

In summary, we can see that (1) and (4) reject strategies, although they are called reject, still perform tasks. But (2) and (3) will directly reject the task, making the task lost.

3.2 implement rejection strategy by yourself

For example, if we want to implement a rejection policy, and the tasks we submit are finally submitted to the queue, and the blocking waiting policy is used to complete them, how do we write the code? In fact, according to the code of JDK, we can write our own rejection strategy. First, we need to implement RejectedExecutionHandler.

public static class EnqueueByBlockingPolicy implements RejectedExecutionHandler {

	public EnqueueByBlockingPolicy() { }

	@Override
	public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
		if (e.isShutdown()) {
			return;
		}
		try {
			e.getQueue().put(r);
		} catch (InterruptedException interruptedException) {
			interruptedException.printStackTrace();
		}
	}
}

Of course, it is not recommended to write this in the work project, because this rejection policy will cause the main thread to block, and no timeout is set to exit. If you want to reject in this way, you'd better use the BlockingQueue ා offer (E, long, timeunit) method. But using offer does not guarantee that you will definitely submit tasks to the queue; the specific code of rejection strategy depends on the actual needs.

142 original articles won praise 15 visits 30000+
follow Private letter

Posted by jemgames on Mon, 04 May 2020 00:03:31 -0700