一、顶层设计:为什么需要多线程?
在现代计算机架构中,CPU多核化(如Intel 12代酷睿的16核24线程)与高并发业务场景(如电商秒杀、实时聊天)催生了多线程技术的广泛应用。单线程与多线程的本质差异在于任务调度粒度与资源利用率:
// 单线程示例:顺序执行耗时任务
public void singleThreadProcess() {
downloadFile(); // 耗时2秒(I/O阻塞)
processData(); // 耗时3秒(CPU计算)
saveToDB(); // 耗时1秒
// 总耗时≈6秒
}
// 多线程优化:并行处理
public void multiThreadProcess() {
ExecutorService executor = Executors.newFixedThreadPool(3);
executor.submit(this::downloadFile); // 线程1
executor.submit(this::processData); // 线程2
executor.submit(this::saveToDB); // 线程3
// 总耗时≈3秒(取决于最慢任务)
}
二、单线程 vs 多线程核心对比
| 维度 | 单线程 | 多线程 |
|---|---|---|
| 执行模式 | 顺序执行 | 并行/并发执行 |
| 资源占用 | 内存消耗低 | 需要更多内存(线程栈) |
| 适用场景 | 简单脚本、低并发任务 | 高并发服务、计算密集型任务 |
| 调试难度 | 简单 | 复杂(竞态条件、死锁) |
| 典型框架 | Node.js(默认单线程) | Tomcat(线程池处理请求) |
三、创建线程的方式
| 创建方式 | 特点 | 适用场景 | JDK版本 |
|---|---|---|---|
继承Thread类 | 简单但缺乏扩展性 | 快速原型开发 | 1.0+ |
实现Runnable接口 | 解耦任务与执行 | 线程池任务提交 | 1.0+ |
实现Callable+Future | 支持返回值与异常抛出 | 需要获取异步结果 | 5.0+ |
线程池(ExecutorService) | 资源复用,管理精细化 | 高并发服务 | 5.0+ |
CompletableFuture | 异步编排,函数式编程 | 复杂任务链 | 8.0+ |
虚拟线程(VirtualThread) | 轻量级,支持百万级并发 | I/O密集型高吞吐 | 19+ (Loom) |
后续对于线程的创建详细步骤及解读都在这篇文章里:https://blog.youkuaiyun.com/jun2714/article/details/147950637?spm=1001.2014.3001.5501
四、项目中高频问题与解决方案
1. 线程安全问题(银行转账案例)
// 错误实现:多线程共享账户
class BankAccount {
private int balance = 1000;
public void transfer(int amount) {
if (balance >= amount) {
balance -= amount; // 此处可能被线程切换打断
}
}
}
// 正确方案1:synchronized同步
public synchronized void transfer(int amount) { /*...*/ }
// 正确方案2:使用ReentrantLock
private Lock lock = new ReentrantLock();
public void transfer(int amount) {
lock.lock();
try {
if (balance >= amount) balance -= amount;
} finally {
lock.unlock();
}
}
2. 死锁问题(哲学家就餐场景)
// 错误代码:可能陷入死锁
Object fork1 = new Object();
Object fork2 = new Object();
Thread t1 = new Thread(() -> {
synchronized(fork1) {
synchronized(fork2) { /* 就餐 */ }
}
});
Thread t2 = new Thread(() -> {
synchronized(fork2) {
synchronized(fork1) { /* 就餐 */ }
}
});
// 解决方案:资源顺序化获取
Thread t1 = ... // 先获取fork1再fork2
Thread t2 = ... // 同样按fork1→fork2顺序
3. 线程池参数配置误区
// 错误案例:无界队列导致OOM
ExecutorService executor = Executors.newCachedThreadPool();
// 正确方案:自定义ThreadPoolExecutor
new ThreadPoolExecutor(
4, // 核心线程数(按CPU核数设置)
8, // 最大线程数(突发流量缓冲)
60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100), // 有界队列
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
五、多线程效能优化黄金组合
1. 与并发容器的配合
-
场景:高并发计数器
// 错误:HashMap非线程安全
Map<String, Integer> counter = new HashMap<>();
// 正确:ConcurrentHashMap分段锁
Map<String, AtomicInteger> counter = new ConcurrentHashMap<>();
counter.computeIfAbsent(key, k -> new AtomicInteger()).incrementAndGet();
2. 结合CompletableFuture实现异步编排
// 并行调用多个微服务
CompletableFuture<User> futureUser = CompletableFuture.supplyAsync(this::getUserInfo);
CompletableFuture<Order> futureOrder = CompletableFuture.supplyAsync(this::getLatestOrder);
// 合并结果
futureUser.thenCombineAsync(futureOrder, (user, order) -> {
return buildUserProfile(user, order);
}).exceptionally(ex -> {
log.error("Failed to combine results", ex);
return null;
});
3. 响应式编程(Reactor/WebFlux)
// 非阻塞IO处理高并发请求
public Mono<ResponseEntity<User>> getUser(String id) {
return Mono.fromCallable(() -> userService.findById(id))
.subscribeOn(Schedulers.boundedElastic()) // IO线程池
.timeout(Duration.ofSeconds(3))
.onErrorResume(ex -> Mono.just(new User()));
}
六、选型决策与性能指标
| 指标 | 单线程 | 多线程 |
|---|---|---|
| 吞吐量(QPS) | 低(<100) | 高(可达10万+) |
| 响应时间 | 稳定但长 | 波动但短 |
| CPU利用率 | 单核满载 | 多核均衡负载 |
| 系统复杂度 | ★☆☆☆☆ | ★★★★☆ |
性能测试数据(4核CPU处理10万请求):
-
单线程:耗时 12.8秒,CPU使用率 25%
-
多线程(8线程池):耗时 2.3秒,CPU使用率 98%
七、最佳实践总结
-
线程使用铁律
-
IO密集型任务 → 多线程(线程数 ≈ 2 * CPU核数)
-
CPU密集型任务 → 线程数 ≈ CPU核数
-
混合型任务 → 根据公式:
线程数 = CPU核数 * (1 + 等待时间/计算时间)
-
-
避坑指南
-
避免在循环内创建线程(使用线程池)
-
同步代码块尽量缩小范围(减少锁竞争)
-
使用ThreadLocal代替静态变量实现线程隔离
-
-
未来趋势
-
协程(Coroutine):如Kotlin协程、Go的goroutine,轻量级线程切换
-
虚拟线程(Loom项目):JDK19+的轻量级线程,支持百万级并发
-
附录:常见面试题解析
-
Q:volatile关键字能保证线程安全吗?
A:不能!volatile仅保证可见性和有序性,无法保证复合操作的原子性。 -
Q:如何诊断死锁?
A:使用jstack <pid>导出线程栈,查找BLOCKED状态和锁持有链。 -
Q:线程池的workQueue有哪些类型?
A:-
SynchronousQueue:直接传递任务(无缓冲) -
LinkedBlockingQueue:无界队列(易OOM) -
ArrayBlockingQueue:有界队列(推荐)
-
结语
多线程是把双刃剑,合理使用能极大提升系统性能,滥用则会导致灾难性后果。建议结合Arthas、VisualVM等工具进行线程状态监控,并在代码审查中重点关注锁的使用。
1689

被折叠的 条评论
为什么被折叠?



