面试官:线程池在实际工作中是如何使用的?例如,线程池中的核心线程数如何确定?
派大星:嗨,面试官!线程池在实际工作中被广泛应用。它可以管理和复用线程,提高程序的性能和效率。核心线程数的设置主要取决于几个因素,包括CPU核数、机器内存、IO支持的最大QPS以及任务类型。
面试官:那应该将线程池的核心线程数设置为多大呢?
派大星:根据以往的经验,对于CPU密集型任务,核心线程数应该等于机器的核数加一。这样可以充分利用多核CPU的计算能力,并保留一个额外的线程用于处理突发任务。对于IO密集型任务,核心线程数应该设置为两倍的CPU核数,因为IO操作通常需要较多的等待时间,可以利用多个线程同时处理。
面试官:如果任务是复合型的,既包含CPU密集型任务又包含IO计算,如何设置核心线程数呢?假设在一个请求中,计算操作需要5ms,DB操作需要100ms。对于一台有8核的机器,如果要求CPU利用率达到100%,应该如何设置线程数?
派大星:对于复合型任务,我们可以综合考虑CPU密集型和IO密集型的特点进行设置。在这种情况下,如果计算操作需要5ms(CPU操作5ms),而DB操作需要100ms,那么单核CPU的利用率就是5/(5 100)。为了达到理论上的100% CPU利用率,需要的线程数为1/(5/(5 100)),即21个线程。然而,现在我们只有8个核可用,所以根据这个理论,需要的线程数是168个(8核 * 21线程)。不过,这是一个理论值,实际情况可能受其他因素的限制。
面试官:如果DB操作的最大QPS是1000,应该设置多少核心线程数呢?
派大星:为了保持相对比例,可以根据比例减少线程数。如果有168个线程,那么DB的访问QPS将达到1600(168 * (1000/(5 100)))。然而,由于DB的最大QPS只能是1000,我们需要按比例减少线程数。因此,核心线程数的大小应该是105个线程(168 * 1000/1600 = 105)。
面试官:如何捕获线程池中的异常呢?
派大星:有几种方法可以捕获线程池中的异常。一种方法是通过手动使用try-catch块来捕获异常并打印出来,但这样的写法比较繁琐和不够优雅。
另一种方法是利用Thread类中的dispatchUncaughtException(Throwable e)方法。当线程抛出异常时,JVM最终会回调这个方法来进行最后的异常处理,而且该异常会被ThreadGroup类中的uncaughtException方法处理。我们可以在创建Thread对象时绑定一个自定义的异常捕获处理器,最终发生异常时会打印我们的错误日志。下面是一个示例代码:
代码语言:javascript复制public static void main(String[] args) {
Thread thread = new Thread(() -> {
log.info("------- info -------");
throw new RuntimeException("运行时异常~~~~~");
});
Thread.UncaughtExceptionHandler uncaughtExceptionHandler = (t, e) -> {
log.error("Exception in Thread..... ", e);
};
thread.setUncaughtExceptionHandler(uncaughtExceptionHandler);
thread.start();
}
然而,在项目中我们更常使用线程池而非单独的线程。线程池中的线程对象实际上是由线程工厂创建的。我们可以在线程工厂中设置一个异常捕获处理器。以下是使用ThreadPoolExecutor创建线程池时设置线程工厂的示例代码:
代码语言:javascript复制private static ExecutorService executor = new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(500),
new NamedThreadFactory("refresh-ipDetail", (ThreadGroup)null,false,
new GlobalUncaughtExceptionHandler()));
代码语言:javascript复制@Slf4j
public class GlobalUncaughtExceptionHandler implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
log.error("Exception in thread {} ", t.getName(), e);
e.printStackTrace();
}
}
但是在使用Spring的线程池时,由于其线程工厂无法设置任何值,我们可以采用装饰器模式。我们将Spring的线程池线程工厂传入装饰器中,并调用其创建线程的方法。然后,我们添加我们自定义的异常捕获处理器。在使用线程池时,我们替换掉Spring的线程工厂,并将本类的线程工厂进行包装传递进去,从而实现线程池的异常捕获。以下是具体实现方式的示例代码:
代码语言:javascript复制@Slf4j
@AllArgsConstructor
public class MyThreadFactory implements ThreadFactory {
private ThreadFactory factory;
@Override
public Thread newThread(Runnable r) {
Thread thread = factory.newThread(r);
thread.setUncaughtExceptionHandler(new GlobalUncaughtExceptionHandler());
thread.setDaemon(false);
thread.setPriority(5);
return thread;
}
}
代码语言:javascript复制@Bean
public ThreadPoolTaskExecutor websocketExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(16);
executor.setMaxPoolSize(16);
executor.setQueueCapacity(1000);
executor.setThreadNamePrefix("websocket-executor-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
executor.setThreadFactory(new MyThreadFactory(executor));
executor.initialize();
return executor;
}
面试官:非常清楚和详细的解释,谢谢你的分享。你的回答对于捕获线程池中的异常提供了多种方法和具体实现。