多线程本质是提升CPU利用率与响应速度,通过让等待I/O或计算的线程让出CPU给就绪任务;典型场景如10个HTTP请求并发后耗时接近单次最慢者,吞吐近翻10倍。

多线程不是为了“炫技”,而是解决阻塞和吞吐瓶颈
Java 程序默认是单线程执行的,main 方法跑完就退出。一旦遇到 I/O(如读文件、发 HTTP 请求)、等待数据库响应、或计算密集型任务,线程就会卡住——这段时间 CPU 是空闲的,但用户在等。多线程的本质,是让 CPU 在某个线程等待时,去执行另一个就绪的任务,从而提升资源利用率和响应速度。
典型表现:不加线程时,10 个 HTTP 请求串行发,耗时 ≈ 10 × 单次耗时;用 ExecutorService 并发发,总耗时接近单次最慢的那个(理想情况下),吞吐翻了近 10 倍。
哪些场景必须考虑多线程(而非简单用异步库)
不是所有“看起来要快”的地方都该手写多线程。真正需要你主动管理线程的,往往是以下情况:
- 需要精确控制并发数(比如限制最多 5 个数据库连接同时查表,避免打爆 DB),这时
Executors.newFixedThreadPool(5)比全用CompletableFuture更可控 - 存在长时间运行且需外部干预的任务(如监控线程定期检查磁盘空间,支持随时
interrupt()停止) - 要复用线程上下文(如
ThreadLocal存用户身份、事务 ID),而框架自动管理的线程池(如 Web 容器的tomcat-exec)不保证复用逻辑可见 - 集成老系统或 JNI 调用,其内部依赖线程局部状态,无法被标准异步模型兼容
Runnable 和 Callable 的关键区别不只是“有没有返回值”
表面看,Runnable 无返回、不抛受检异常;Callable 有 return、可抛 Exception。但实际影响更深层:
立即学习“Java免费学习笔记(深入)”;
- 提交到线程池后,
Runnable对应返回Future>,调用get()只能得null;Callable返回Future,get()才真能取结果 -
Callable的异常会被封装进ExecutionException,必须显式catch;而Runnable内未捕获的异常会直接终止线程(若没设UncaughtExceptionHandler,就静默消失) - Spring 的
@Async默认只支持void或Future返回的方法,底层其实包装成了Callable—— 所以如果方法声明抛IOException,又没在方法内 try-catch,就会被吞掉
别直接 new Thread(),也别无脑用 Executors 工厂
new Thread(runnable).start() 看似简单,但线程创建销毁开销大,且无法复用、无队列缓冲、无拒绝策略,高并发下容易 OOM。
而 Executors.newCachedThreadPool() 在突发流量时可能无限创建线程;newSingleThreadExecutor() 看似安全,但内部队列是无界的 LinkedBlockingQueue,任务积压照样撑爆内存。
更稳妥的做法是手动构造 ThreadPoolExecutor:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
4, // corePoolSize
8, // maxPoolSize
60L, // keepAliveTime
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100), // 有界队列,防内存溢出
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝时由调用线程执行
);
核心点:队列要有界、拒绝策略要明确、线程数根据 CPU 密集型(≈ CPU 核数)或 I/O 密集型(可适当放大)来调。
真正难的从来不是“怎么启线程”,而是“怎么确保线程间共享数据不出错”、“怎么让一个业务操作在多线程下仍保持原子性”、“怎么避免死锁却还不牺牲性能”。这些不在启动层面,而在设计层面——比如该用 ConcurrentHashMap 还是 synchronized 块,该用 StampedLock 还是 ReentrantReadWriteLock,往往取决于读写比例和争用强度,而不是文档里写的“更先进”。











