The core problem of creating a thread pool

According to a provision in Alibaba’s Java Development Specification,

How many core threads should I create?

1.1 Why do we set the appropriate number of threads?

To squeeze the performance out of the hardware, we know that a program on the server strips out network traffic time, leaving “computation” and “I/O” time. The “I/O” here includes both the time spent exchanging data with main and secondary storage, and the network data transfer to the server, which copies the kernel space. We set the appropriate number of threads so that each CPU can be fully utilized and the disk can exchange data efficiently.

1.2 Classification discussion according to the type of program

  1. For example, calculating the addition of 100,000 random numbers is a very computationally intensive task.
  2. File upload tasks, for example, are typically “I/O” intensive tasks.
  3. There is a third category of “hybrid I/O tasks,” which most programs today fall into, involving both types of time-consuming tasks.

Let’s talk about them one by one

  1. For computationally intensive tasks, set the number of threads to: number of server cpus + 1. Why is that? Because if a task is computationally intensive, it is ideal for all cpus to run full so that each CPU is fully utilized. A popular explanation on the web for why +1 is necessary is that even cpu-intensive tasks can have their execution threads waiting at some point for some reason (such as page breaks, etc.).

  2. If the task is IO intensive, the best way is to calculate the ratio of the IO to the time spent doing the calculation. If CPU computation and I/O operation time is 1:2, then the appropriate thread is 3, and why… Here is illustrated by the following figure

    Images from:Geek time — 10 | Java thread (in) : create many threads is appropriate?

    So, if it’s a CPU, the appropriate number of threads is:

    1 + (I/O time/CPU time)

    However, today’s cpus are multi-core, so the appropriate number of threads is:

    Number of cpus * (appropriate number of threads when only one CPU is available)

    Of course, in our work, it is difficult to calculate the time ratio of IO and calculation perfectly every time, so many predecessors have a more general calculation of the number of threads according to their work experience. For I/O intensive applications, the optimal number of threads is: 2 * CPU cores + 1.

    Note that there is another important point here: if you have more threads it is more expensive to switch threads, so we try to do it with a smaller number of threads

  3. If it is a hybrid task, then we split the task into computational and IO tasks and submit these subtasks to the thread pool of their own type.

Create thread factory by default or implement it yourself?

Alibaba’s Java Development Specification provides that:

2.1 Create with Guava’s

@Slf4j
public class ThreadPoolExecutorDemo00 {
	public static void main(String[] args) {
		ThreadFactoryBuilder threadFactoryBuilder = new ThreadFactoryBuilder()
			.setNameFormat("My thread %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(); }}Copy the code

Take a look at the results:

2.2 Implement ThreadFactory 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 finalString 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); }returnthread; }}Copy the code

Take a look at the results:

3. Which rejection strategy does it use?

3.1 Let’s take a look at the four basic rejection strategies

  • CallerRunsPolicy: the task is not executed by the thread pool, but is executed by the main thread of the submission thread.
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    if (!e.isShutdown()) {
        r.run();
    }
}
Copy the code
  • AbortPolicy: throws an exception directly. This is also the default reject policy.
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
 throw new RejectedExecutionException("Task " + r.toString() +
                                      " rejected from " +
                                      e.toString());
}
Copy the code
  • (3) DiscardPolicy: simply refuse to do anything else.
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {}Copy the code
  • Discarding the oldest task discarding the oldest task discarding the oldest task Discarding the oldest task Discarding the oldest task discarding the oldest task discarding the oldest task discarding the oldest task discarding the oldest task discarding the oldest task discarding the oldest task discarding
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
   if (!e.isShutdown()) {
        e.getQueue().poll();
        e.execute(r);
    }
}
Copy the code

In summary, it can be seen that the rejection strategy of (1) and (4) will still execute the task although it is called rejection. However, (2) and (3) will directly reject the task, causing the task to be lost.

3.2 Implement the Rejection Policy by yourself

For example, if we want to implement a rejection policy, we want our submitted task to be finally submitted to a queue, using a blocking wait strategy, then how do we write the code? RejectedExecutionHandler RejectedExecutionHandler RejectedExecutionHandler

public static class EnqueueByBlockingPolicy implements RejectedExecutionHandler {

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

Of course, it is not recommended to write this in real life, because this measurement will cause the main thread to block, and there is no timeout exit. It is best to use BlockingQueue#offer(E, long, TimeUnit). However, using offer does not guarantee that you will submit the task to the queue. It depends on the actual demand.

3.3 summarize

If you need to ensure that your submitted task is not lost, then it is recommended to use CallerRunsPolicy, DiscardOldestPolicy, or even use the self-implemented reject policy in 3.2. If your submitted task is not important, It is important to ensure the stability of your program, so it is recommended to use DiscardPolicy, AbortPolicy.