线程池与 Go 并发
ThreadPoolExecutor
ThreadPoolExecutor 是 Java 线程池的核心实现,用于复用线程、控制并发度、缓冲任务并提供过载保护。
七个核心参数
corePoolSize:核心线程数;maximumPoolSize:允许的最大线程数;keepAliveTime:非核心线程空闲存活时间;unit:存活时间单位;workQueue:等待执行任务的阻塞队列;threadFactory:创建线程,可统一命名、设置异常处理器;handler:拒绝策略。
任务执行流程
提交任务后按以下顺序处理:
- 当前工作线程数小于
corePoolSize:创建核心线程执行任务; - 否则尝试将任务放入
workQueue; - 队列已满且工作线程数小于
maximumPoolSize:创建非核心线程; - 队列已满且线程数达到上限:执行拒绝策略。
线程池不是“线程数未达到最大值就直接建线程”;它会优先使用核心线程,再入队,队列满后才扩容。
常用阻塞队列
ArrayBlockingQueue:有界数组队列,容量固定;LinkedBlockingQueue:链表队列,可有界;默认构造容量很大,使用时建议显式设定上限;SynchronousQueue:不缓存任务,任务必须直接交给空闲线程;PriorityBlockingQueue:按优先级出队;DelayQueue:延迟到期后才能取出。
拒绝策略
AbortPolicy:抛出RejectedExecutionException,默认策略;CallerRunsPolicy:由提交任务的线程执行,形成反压;DiscardPolicy:直接丢弃;DiscardOldestPolicy:丢弃队列中最旧任务后重试提交。
业务中通常建议自定义拒绝策略:记录指标、告警、按业务决定降级或持久化任务,避免静默丢失。
线程池大小配置
- CPU 密集型:线程数通常设为 CPU 核数附近,如
Ncpu或Ncpu + 1,避免过多上下文切换。 - I/O 密集型:可配置更多线程以覆盖 I/O 等待;不能机械地固定为 CPU 核数两倍,应根据任务计算/等待比例、下游容量、队列堆积和压测结果确定。
常用估算:线程数 ≈ Ncpu × (1 + 等待时间 / 计算时间),仅作起点,最终以监控和压测为准。
建议按任务类型隔离线程池,避免慢 I/O 任务挤占 CPU 任务资源。
常见 Executors 工厂方法
newFixedThreadPool:固定线程数,但默认使用无界队列;newCachedThreadPool:线程数可快速膨胀,不适合无边界流量;newScheduledThreadPool:定时与周期任务;newSingleThreadExecutor:单线程顺序执行。
生产环境优先显式构造 ThreadPoolExecutor,指定有界队列、线程名和拒绝策略,而非直接使用默认 Executors 工厂方法。
线程池关闭与状态
shutdown():不再接收新任务,继续执行队列中的已提交任务;shutdownNow():不再接收新任务,尝试中断执行中的任务,并返回队列中未执行任务;中断是否生效取决于任务是否正确响应中断。
线程池状态:
RUNNING:接收新任务并处理队列任务;SHUTDOWN:不接收新任务,继续处理队列任务;STOP:不接收、不处理队列任务,尝试中断工作线程;TIDYING:任务已终止,准备执行终止钩子;TERMINATED:彻底终止。
线程池使用要点
- 使用自定义、有边界的线程池;
- 明确任务类型与超时策略,避免在线程池中无限阻塞;
- 为线程设置有意义的名称,配置异常处理和监控;
- 监控活跃线程数、队列长度、任务耗时、拒绝次数;
- 防止任务互相等待造成线程池饥饿;
- 关闭应用时优雅停止线程池。
Go 并发编程
Go 并发的三项基础:
- Goroutine:轻量级执行单元,通过
go启动; - Channel:Goroutine 间传递数据与同步;
- select:同时监听多个 Channel 操作。
select {
case result := <-resultCh:
fmt.Println(result)
case <-ctx.Done():
return ctx.Err()
}
GMP 调度模型
- G(Goroutine):待执行的协程;
- M(Machine):操作系统线程;
- P(Processor):执行 Go 代码所需的逻辑处理器,数量通常由
GOMAXPROCS控制。
调度器将 G 分配给持有 P 的 M 执行,并通过本地队列、全局队列和工作窃取提高吞吐。Goroutine 遇到阻塞、系统调用或网络 I/O 时,运行时会尽量调度其他 G 继续运行。
Go 并发实践
- 使用
context.Context传递取消、超时和请求范围的元数据; - 用 Channel 或
sync.Mutex明确共享数据的访问方式; - 启动 Goroutine 后必须考虑退出路径,避免泄漏;
- 使用
sync.WaitGroup等待一组任务结束; - 使用
go test -race检测数据竞争。