面试题答案
一键面试线程池配置
- 合理选择线程池类型:
- FixedThreadPool:适用于已知任务数量且对线程数有明确限制的场景。例如在一个图片处理的高并发系统中,假设服务器的CPU和内存资源有限,我们预估最多同时处理10个图片处理任务,就可以使用
FixedThreadPool(10)
。这样可以避免过多线程竞争资源,提高任务执行效率。 - CachedThreadPool:适合任务执行时间短、数量不确定的场景。例如在一个实时日志处理系统中,日志记录任务通常执行时间很短,但并发量可能随时变化。使用
CachedThreadPool
,线程池会根据需求动态创建和回收线程,减少线程创建和销毁的开销。 - ScheduledThreadPool:若任务中有需要定时执行或延迟执行的部分,如定时清理缓存任务。比如每隔1小时清理一次缓存数据,就可以使用
ScheduledThreadPool
,通过scheduleAtFixedRate
或scheduleWithFixedDelay
方法来安排任务执行。
- FixedThreadPool:适用于已知任务数量且对线程数有明确限制的场景。例如在一个图片处理的高并发系统中,假设服务器的CPU和内存资源有限,我们预估最多同时处理10个图片处理任务,就可以使用
- 调整线程池参数:
- 核心线程数:根据系统的CPU核心数、I/O特性等因素进行设置。对于CPU密集型任务,核心线程数一般设置为
CPU核心数 + 1
,例如在一个加密计算的高并发场景中,假设服务器是8核CPU,核心线程数可设置为9。对于I/O密集型任务,核心线程数可适当增加,比如设置为2 * CPU核心数
,如在一个文件上传下载的高并发系统中,8核CPU时核心线程数可设置为16。 - 最大线程数:要考虑系统的资源限制,如内存、网络带宽等。如果系统内存有限,过多的线程可能导致OOM(Out of Memory)错误。例如在一个内存较小的云服务器上,最大线程数不宜设置过高,可通过压力测试来确定合适的值,比如先设置为50,然后逐步调整,观察系统性能和稳定性。
- 队列容量:如果使用有界队列,如
ArrayBlockingQueue
,要根据任务的处理速度和到达速率来设置队列容量。例如在一个订单处理系统中,订单处理任务的平均处理时间为1秒,每秒平均有10个订单到达,假设希望系统能短暂承受一定的突发流量,队列容量可设置为100,即能容纳10秒的突发订单量。
- 核心线程数:根据系统的CPU核心数、I/O特性等因素进行设置。对于CPU密集型任务,核心线程数一般设置为
资源管理
- 限制任务数量:
- 可以使用信号量(
Semaphore
)来限制同时执行的任务数量。例如在一个数据库连接池有限的系统中,假设数据库连接池最大连接数为100,我们可以创建一个Semaphore(100)
,在每个任务执行前调用semaphore.acquire()
获取许可,任务执行完后调用semaphore.release()
释放许可。这样可以避免过多任务同时请求数据库连接,防止数据库连接池耗尽。
- 可以使用信号量(
- 及时释放资源:
- 在任务完成后,确保及时关闭或释放相关资源。比如在使用
HttpClient
进行网络请求的任务中,任务完成后要及时关闭HttpClient
连接,防止连接泄漏。可以使用try - finally
块来保证资源的正确释放,如下代码示例:
CloseableHttpClient httpClient = HttpClients.createDefault(); try { HttpGet httpGet = new HttpGet("http://example.com"); CloseableHttpResponse response = httpClient.execute(httpGet); // 处理响应 } catch (IOException e) { e.printStackTrace(); } finally { try { httpClient.close(); } catch (IOException e) { e.printStackTrace(); } }
- 在任务完成后,确保及时关闭或释放相关资源。比如在使用
- 监控资源使用:
- 使用工具如JMX(Java Management Extensions)来监控系统资源的使用情况,如内存、CPU使用率、线程数等。通过JMX可以实时获取线程池的运行状态,如活跃线程数、队列大小等信息。例如,可以在应用程序中注册一个
MBean
来监控线程池的相关指标,然后使用JConsole等工具连接到应用程序查看监控数据,根据数据来调整线程池参数或优化任务处理逻辑。
- 使用工具如JMX(Java Management Extensions)来监控系统资源的使用情况,如内存、CPU使用率、线程数等。通过JMX可以实时获取线程池的运行状态,如活跃线程数、队列大小等信息。例如,可以在应用程序中注册一个
任务调度
-
任务优先级:
- 根据任务的重要性和紧急程度设置任务优先级。可以自定义一个实现
Comparable
接口的任务类,例如在一个电商系统中,订单支付任务的优先级高于订单查询任务。在提交任务到线程池时,将任务放入一个PriorityQueue
中,然后由线程池从PriorityQueue
中获取任务执行。以下是一个简单示例:
class PriorityTask implements Comparable<PriorityTask> { private int priority; private Runnable task; public PriorityTask(int priority, Runnable task) { this.priority = priority; this.task = task; } @Override public int compareTo(PriorityTask other) { return Integer.compare(this.priority, other.priority); } public void run() { task.run(); } } PriorityQueue<PriorityTask> taskQueue = new PriorityQueue<>(); taskQueue.add(new PriorityTask(1, () -> System.out.println("High priority task"))); taskQueue.add(new PriorityTask(2, () -> System.out.println("Low priority task"))); // 线程池从taskQueue中获取任务执行
- 根据任务的重要性和紧急程度设置任务优先级。可以自定义一个实现
-
任务拆分与合并:
- 任务拆分:对于一些耗时较长的任务,可以将其拆分成多个小任务并行执行。例如在一个大数据文件处理任务中,可以将文件按行或按块拆分成多个小任务,每个小任务负责处理一部分数据,然后使用
CompletableFuture
的allOf
方法等待所有小任务完成后再进行合并处理。 - 任务合并:如果有多个相似的小任务,可以将它们合并成一个大任务执行。比如在一个用户信息查询系统中,多个用户同时请求查询自己的基本信息,这些查询操作类似,可以将这些查询合并成一个批量查询操作,减少数据库的查询次数,提高系统性能。
- 任务拆分:对于一些耗时较长的任务,可以将其拆分成多个小任务并行执行。例如在一个大数据文件处理任务中,可以将文件按行或按块拆分成多个小任务,每个小任务负责处理一部分数据,然后使用
-
动态任务调度:
- 根据系统的负载情况动态调整任务的调度策略。可以通过监控系统的CPU使用率、内存使用率等指标,当系统负载较高时,降低新任务的提交速率或调整任务优先级。例如,使用一个
ScheduledExecutorService
定时检查系统负载,当CPU使用率超过80%时,将新提交的非紧急任务放入一个延迟队列中,延迟一段时间后再执行,以减轻系统压力。
- 根据系统的负载情况动态调整任务的调度策略。可以通过监控系统的CPU使用率、内存使用率等指标,当系统负载较高时,降低新任务的提交速率或调整任务优先级。例如,使用一个