1. 为什么Java线程是进阶必备技能
在Java开发领域,线程技术就像厨师的刀工基本功一样重要。我见过太多开发者能熟练使用Spring框架,却在面对ConcurrentModificationException时手足无措。线程不仅是面试必考题,更是高并发系统的基石。
现代Java应用几乎都运行在多核CPU上,单线程程序就像只用了一个灶头做饭的大厨。以电商秒杀场景为例,2023年某平台大促期间,核心服务需要处理每秒12万次的并发请求,这靠单线程处理简直是天方夜谭。而合理使用线程技术,可以让你的程序像开了多灶头的专业厨房,各环节并行不悖。
注意:线程虽好但不要滥用,我曾见过一个配置了500个线程的支付服务,在流量激增时直接OOM崩溃。线程数量需要根据业务特性和硬件条件精心设计。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 线程基础:从生命周期到核心操作
2.1 线程的完整生命周期
Java线程的生命周期远比面试八股文描述的复杂。官方文档定义了6种状态,但实际开发中会遇到更多微妙情况:
java复制public enum State {
NEW, // 刚创建未启动
RUNNABLE, // 可运行(可能在运行也可能在等待CPU)
BLOCKED, // 等待监视器锁
WAITING, // 无限期等待
TIMED_WAITING, // 有限期等待
TERMINATED; // 终止
}
但真实场景中,我遇到过线程卡在WAITING状态导致服务雪崩的情况。通过jstack分析发现,是因为使用了不带超时的Object.wait(),而notify()调用被异常吞没了。
2.2 创建线程的三种方式对比
- 继承Thread类:适合简单场景,但Java单继承机制限制了扩展性
java复制class MyThread extends Thread {
public void run() {
System.out.println("继承方式创建的线程");
}
}
- 实现Runnable接口:更灵活,可以配合线程池使用
java复制class MyRunnable implements Runnable {
public void run() {
System.out.println("接口方式创建的线程");
}
}
- Callable+Future:需要返回结果时使用,支持异常抛出
java复制Callable<Integer> task = () -> {
Thread.sleep(1000);
return 42;
};
Future<Integer> future = executor.submit(task);
实测发现,在创建1000个线程的场景下,Runnable方式比继承Thread节省约15%的内存。这是因为Thread类本身携带了大量监控字段。
3. 线程安全与锁的深层机制
3.1 synchronized的优化演进
从JDK1.6开始,synchronized经历了多次优化:
- 偏向锁:单个线程重复获取锁时直接通行
- 轻量级锁:通过CAS操作避免OS层面阻塞
- 重量级锁:真正的互斥锁,会引发线程上下文切换
通过-XX:+PrintFlagsFinal参数可以看到,现代JVM默认开启偏向锁延迟(BiasedLockingStartupDelay=4000),这是因为启动时大量类加载会导致不必要的锁撤销。
3.2 ReentrantLock的实战技巧
相比synchronized,ReentrantLock提供了更灵活的控制:
java复制Lock lock = new ReentrantLock();
try {
lock.lockInterruptibly(); // 可中断的获取锁
// 临界区代码
} finally {
lock.unlock();
}
特别提醒:一定要在finally中释放锁!我在生产环境就遇到过因为异常导致锁未释放,最终形成死锁的惨痛教训。
3.3 原子类的底层原理
以AtomicInteger为例,其核心是Unsafe类的CAS操作:
java复制public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
但在ARM架构的服务器上,CAS性能会比x86下降约20%,这时可以考虑使用LongAdder替代。
4. 线程池的深度配置与调优
4.1 核心参数的关系矩阵
| 参数 | 影响维度 | 典型配置建议 |
|---|---|---|
| corePoolSize | 常驻线程数 | CPU核数+1 |
| maximumPoolSize | 应急线程上限 | corePoolSize*2 |
| keepAliveTime | 空闲线程存活时间 | 60秒 |
| workQueue | 任务缓冲策略 | LinkedBlockingQueue |
| handler | 拒绝策略 | CallerRunsPolicy |
我曾配置过queueCapacity=1000的ArrayBlockingQueue,结果在高并发时导致Full GC频繁。后来改用SynchronousQueue配合合适的maximumPoolSize,系统吞吐量提升了35%。
4.2 四种拒绝策略的选用场景
- AbortPolicy:直接抛出RejectedExecutionException
- CallerRunsPolicy:让提交任务的线程自己执行
- DiscardPolicy:静默丢弃任务
- DiscardOldestPolicy:丢弃队列中最老的任务
在订单系统中,我采用自定义策略:将拒绝的任务持久化到Redis,等线程池负载降低后再重新提交。这避免了高峰期订单丢失的问题。
5. 死锁预防与排查实战
5.1 死锁产生的四个必要条件
- 互斥条件:资源一次只能一个线程占用
- 占有且等待:持有资源的同时等待其他资源
- 不可抢占:资源只能由持有者释放
- 循环等待:多个线程形成资源请求环
5.2 诊断死锁的三种武器
- jstack:直接查看线程dump中的死锁信息
code复制Found one Java-level deadlock:
...
- Arthas:动态监控线程阻塞情况
bash复制thread -b
- VisualVM:图形化展示线程依赖关系
去年我们系统出现过一个隐蔽的死锁:事务A锁定了用户表后等待订单表,而定时任务B以相反顺序获取锁。通过jstack发现时,系统已经卡死了15分钟。
6. Java虚拟线程(Loom)的革命性变化
JDK19引入的虚拟线程彻底改变了Java并发模型:
java复制try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i -> {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return i;
});
});
}
测试表明,创建1万个虚拟线程仅消耗约50MB内存,而平台线程需要GB级别。但要注意:
- 虚拟线程不适合计算密集型任务
- synchronized会pin住虚拟线程使其无法卸载
- I/O操作会自动yield
7. 线程性能优化的七个关键点
- 上下文切换成本:Linux下约1-2微秒/次
- 缓存局部性:让关联任务由同一线程处理
- 锁粒度控制:从方法级缩小到代码块级
- 线程局部变量:合理使用ThreadLocal
- 并发容器选择:ConcurrentHashMap vs Collections.synchronizedMap
- 异步编程:CompletableFuture组合操作
- 协程应用:在I/O密集型场景替代线程
在日志收集服务中,我们通过ThreadLocal重用StringBuilder实例,使GC次数减少了70%。但切记:ThreadLocal使用后必须remove(),否则在线程池中会导致内存泄漏。
