一、面试
1、为什么要用线程池?
线程池通过复用已创建的线程,避免了频繁创建和销毁线程的开销(如内存分配、JVM调度),从而提升系统性能。它能有效控制并发线程数量,防止资源耗尽,同时提供任务队列和拒绝策略,增强系统的稳定性和响应速度。此外,线程池简化了线程管理,支持定时、周期性任务执行,适用于高并发场景。
2、Java 里还有哪些使用了池化思想的技术?
Java 中池化技术的典型应用包括:
- 数据库连接池(如 HikariCP、Druid):复用数据库连接,减少连接创建开销。
- 对象池(如 Apache Commons Pool):缓存对象实例,避免重复初始化。
- HTTP 连接池(如 Apache HttpClient):复用 HTTP 连接,提升请求效率。
- 线程池(如
ExecutorService
):复用线程资源。 - 内存池(如 Netty 的
PooledByteBufAllocator
):减少内存碎片和分配耗时。
3、Java 提供了哪几种线程池?
Java 通过 Executors
工厂类提供了四种线程池:
- FixedThreadPool:固定大小的线程池,使用无界队列,适用于负载稳定的场景。
- CachedThreadPool:可缓存的线程池,适合短时异步任务,线程数按需增长。
- ScheduledThreadPool:支持定时及周期性任务执行。
- SingleThreadExecutor:单线程顺序执行,保证任务无并发问题。
此外,ForkJoinPool
用于分治任务(如递归分解任务),WorkStealingPool
(JDK 8+)基于工作窃取算法提升并行效率。
4、它们的使用场景、区别及选择原则是什么?
- FixedThreadPool:适合已知并发量且任务稳定的场景(如 Web 服务器请求处理)。
- CachedThreadPool:适合执行大量短期异步任务(如批量数据处理),但需警惕无界队列导致 OOM。
- ScheduledThreadPool:用于定时任务调度(如日志清理、心跳检测)。
- SingleThreadExecutor:需保证顺序执行且无并发问题的场景(如事件回调)。
选择原则:根据任务类型(CPU/IO密集)、并发量、资源限制及任务特性(是否需顺序执行)综合评估。
5、线程池的核心参数有哪些?
- corePoolSize:核心线程数,即使空闲也保留。
- maximumPoolSize:最大线程数,任务队列满时扩容的极限。
- keepAliveTime:非核心线程的空闲存活时间。
- workQueue:任务队列(如
LinkedBlockingQueue
、SynchronousQueue
)。 - threadFactory:线程创建工厂,可自定义线程名称、优先级等。
- RejectedExecutionHandler:拒绝策略(如抛出异常、丢弃任务)。
6、如何根据 CPU 密集型和 IO 密集型任务设置参数?
- CPU 密集型:核心线程数 = CPU 核心数 + 1(避免线程切换开销),队列长度适中。
- IO 密集型:核心线程数 = CPU 核心数 × 2(因线程常阻塞等待 IO),队列可适当增大或使用有界队列。
实际需结合系统负载测试调整,例如通过Runtime.getRuntime().availableProcessors()
动态获取 CPU 核心数。
7、队列长度设置的依据是什么?
队列长度需权衡内存资源和任务处理效率:
- 有界队列(如
ArrayBlockingQueue
):防止内存溢出,但需配置合理的拒绝策略。 - 无界队列(如
LinkedBlockingQueue
):适合突发流量,但可能导致任务堆积。
依据包括:系统可用内存、任务到达速率、任务处理时间、队列类型(如优先级队列需固定长度)。通常通过压力测试确定最佳值。
8、动态调整线程池组件时,超出部分的线程怎么办?
会调用锁,获得到锁后才会中断线程。
9、如何快速定位线程泄露问题?
- 线程转储分析:通过
jstack
或 VisualVM 生成线程快照,检查线程状态(如BLOCKED
或WAITING
)及堆栈跟踪。 - 监控工具:使用 Prometheus + Grafana 监控线程数变化,或 Arthas 动态跟踪线程创建。
- 代码审查:检查线程池配置(如是否误用
Thread.sleep
或未释放资源)。 - 内存分析:MAT 工具分析
Thread
对象引用链,定位未回收的线程。
10、使用 Redis 实现动态调整的原因是什么?是否有替代方案?
Redis 支持原子操作和高性能读写,适合实时更新配置(如通过 SET
命令动态修改参数)。替代方案包括:
- ZooKeeper/Etcd:通过监听机制实现配置同步。
- 数据库:存储配置表,定时轮询更新。
- 本地缓存:结合 Guava Cache 或 Caffeine,但需处理一致性。
Redis 的临时节点(如结合 Sentinel)可用于故障转移,但更常见的动态调整是通过外部配置中心实现。
11、AQS 锁的实现原理是什么?
AQS(AbstractQueuedSynchronizer)基于 CLH 队列的变体,使用一个 volatile int state
表示同步状态,线程通过 CAS 竞争修改状态。
- 独占模式(如 ReentrantLock):线程尝试获取锁,失败则进入等待队列,释放时唤醒队首。
- 共享模式(如 Semaphore):允许多个线程同时获取锁。
AQS 内部维护双向链表(CLH 队列),通过Node
对象管理等待线程,结合自旋和阻塞减少 CPU 消耗。
12、ThreadLocal
为什么用 ThreadLocalMap
而不是 HashMap
?
- 线程隔离:每个线程持有独立的
ThreadLocalMap
,避免多线程竞争,无需同步开销。 - 内存泄漏预防:
ThreadLocalMap
的 Entry 继承WeakReference
,当ThreadLocal
实例被回收时,Entry 的键自动清除,防止内存泄漏(但值仍需手动清理)。 - 性能优化:直接访问线程内部的 Map,减少哈希冲突和锁竞争。
13、Runnable
和 Callable
接口的区别是什么?
- 返回值:
Runnable
的run()
无返回值,Callable
的call()
返回泛型结果。 - 异常处理:
Callable
允许抛出受检异常(checked exception),Runnable
的异常需在内部捕获。 - 用途:
Runnable
用于简单异步任务,Callable
适用于需返回结果或抛出异常的场景(如 FutureTask)。
14、数据库分片有哪些方式?水平拆分的缺点是什么?
-
分片方式:
- 垂直分片:按业务拆分表(如订单库、用户库)。
- 水平分片:按规则(哈希、范围)拆分同一表的数据。
-
水平拆缺点:
- 跨分片查询复杂(需聚合或冗余字段)。
- 数据分布不均可能导致热点问题。
- 扩容时需数据迁移,影响可用性。
15、SQL如何调优?
16、数据库如何解决主从延迟问题?
- 并行复制:从库多线程应用 relay log。
- 半同步复制:主库等待至少一个从库确认写入。
- 应用层优化:读写分离时,强一致性场景直接读主库,最终一致性场景异步读从库。
- 延迟监控:通过
SHOW SLAVE STATUS
监控Seconds_Behind_Master
,及时报警。
17、Redis 缓存穿透、雪崩、击穿的防御手段是什么?
- 穿透:布隆过滤器拦截非法请求,或缓存空值(设置较短 TTL)。
- 雪崩:随机过期时间,集群部署,永不过期+异步更新。
- 击穿:互斥锁(如 Redis 的
SETNX
)或永不过期,后台线程定时刷新。
18、Redis 集群如何避免脑裂问题?选举临时节点的机制是什么?
- 脑裂预防:多数派原则(Quorum),配置
cluster-require-full-coverage no
避免部分节点宕机导致整体不可用。 - 临时节点:通过 ZooKeeper/Etcd 等协调服务创建临时节点,节点消失时自动触发选举。
- 选举机制:基于 Raft 或类似算法,节点通过心跳检测故障,多数派确认新主节点。
19、平时常用的设计模式有哪些?
- 单例模式(Spring Bean 默认作用域)。
- 工厂模式(JDBC 驱动、Hibernate
SessionFactory
)。 - 代理模式(AOP、动态代理)。
- 观察者模式(Spring 事件监听)。
- 策略模式(多支付渠道切换)。
- 装饰器模式(Java I/O 流)。
- 模板方法模式(Spring JdbcTemplate)。
20、Spring 框架中用到了哪些设计模式?
- 模板方法:
JdbcTemplate
、RestTemplate
封装公共流程。 - 工厂模式:
BeanFactory
创建 Bean 实例。 - 代理模式:JDK 动态代理或 CGLIB 实现 AOP。
- 单例模式:默认 Bean 作用域。
- 观察者模式:
ApplicationEvent
和ApplicationListener
。 - 适配器模式:
HandlerAdapter
适配不同处理器。 - 装饰器模式:
BeanPostProcessor
增强 Bean 初始化过程。