1. Java多线程编程核心要点解析
从事Java开发十多年来,多线程始终是区分初级和高级程序员的重要分水岭。记得刚入行时,我负责的第一个生产事故就是由于线程同步不当导致的数据库连接耗尽。今天我们就来系统梳理Java多线程的核心知识体系,这些经验都来自真实项目中的教训总结。
多线程编程的本质是让程序能够"同时"处理多个任务(注意这里的引号,因为真正的并行取决于CPU核心数)。在Java中,实现多线程主要有三种方式:继承Thread类、实现Runnable接口,以及JDK5之后引入的Callable/Future机制。每种方式都有其适用场景,我们稍后会详细对比。
2. 线程创建与生命周期管理
2.1 三种创建方式对比
先看最基础的Thread类继承方式:
java复制class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程执行中...");
}
}
// 启动线程
new MyThread().start();
这种方式简单直接,但存在明显的局限性——Java不支持多重继承。因此更推荐实现Runnable接口:
java复制class MyTask implements Runnable {
@Override
public void run() {
System.out.println("实现Runnable的任务");
}
}
// 使用线程池执行
ExecutorService executor = Executors.newSingleThreadExecutor();
executor.execute(new MyTask());
而Callable则是Runnable的增强版,可以返回执行结果:
java复制class ComputeTask implements Callable<Integer> {
@Override
public Integer call() throws Exception {
return 42; // 返回计算结果
}
}
// 获取异步结果
Future<Integer> future = executor.submit(new ComputeTask());
int result = future.get(); // 阻塞获取结果
重要提示:生产环境中绝对不要直接new Thread(),而应该使用线程池。我在金融项目中就遇到过因频繁创建线程导致OOM的严重事故。
2.2 线程状态转换详解
Java线程的生命周期包含6种状态:
- NEW:刚创建未启动
- RUNNABLE:可运行状态(包括就绪和运行中)
- BLOCKED:等待监视器锁
- WAITING:无限期等待
- TIMED_WAITING:有限期等待
- TERMINATED:执行结束
状态转换的典型场景:
- 调用start():NEW → RUNNABLE
- 获取synchronized锁失败:RUNNABLE → BLOCKED
- 调用wait():RUNNABLE → WAITING
- 调用sleep(ms):RUNNABLE → TIMED_WAITING
3. 线程同步与通信机制
3.1 锁机制深度解析
synchronized是最基础的同步手段,但要注意其实现原理:
java复制// 对象锁
public synchronized void method() {...}
// 类锁
public static synchronized void staticMethod() {...}
// 同步代码块
synchronized(lockObj) {...}
JDK5之后,java.util.concurrent.locks包提供了更灵活的Lock接口:
java复制ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock(); // 必须手动释放
}
实战经验:在电商秒杀系统中,我们对比发现ReentrantLock的性能比synchronized高出约15%,特别是在高并发场景下。
3.2 线程通信方式
wait()/notify()机制:
java复制synchronized(sharedObj) {
while(conditionNotMet) {
sharedObj.wait(); // 释放锁并等待
}
// 处理业务
sharedObj.notifyAll(); // 唤醒其他线程
}
更现代的解决方案是使用Condition:
java复制Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
lock.lock();
try {
condition.await(); // 类似wait()
condition.signal(); // 类似notify()
} finally {
lock.unlock();
}
4. 并发工具类实战应用
4.1 CountDownLatch应用场景
典型的应用场景是并行任务等待:
java复制CountDownLatch latch = new CountDownLatch(3);
// 多个工作线程
new Thread(() -> {
// 执行任务
latch.countDown();
}).start();
// 主线程等待
latch.await(); // 阻塞直到计数器归零
在物流系统中,我们用这个机制实现了多仓库库存并行查询,响应时间从串行的2秒降低到800毫秒。
4.2 CyclicBarrier vs CountDownLatch
两者主要区别:
- CountDownLatch:一次性使用,计数递减
- CyclicBarrier:可循环使用,线程互相等待
java复制CyclicBarrier barrier = new CyclicBarrier(3, () -> {
System.out.println("所有线程到达屏障");
});
IntStream.range(0, 3).forEach(i ->
new Thread(() -> {
try {
barrier.await(); // 等待其他线程
} catch (Exception e) {
Thread.currentThread().interrupt();
}
}).start()
);
5. 线程池最佳实践
5.1 参数配置原则
ThreadPoolExecutor的核心参数:
- corePoolSize:核心线程数(长期保留)
- maximumPoolSize:最大线程数
- keepAliveTime:空闲线程存活时间
- workQueue:任务队列
- handler:拒绝策略
配置经验公式:
java复制int coreSize = Runtime.getRuntime().availableProcessors();
int maxSize = coreSize * 2;
long keepAlive = 60L;
BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(1000);
ThreadPoolExecutor executor = new ThreadPoolExecutor(
coreSize, maxSize, keepAlive, TimeUnit.SECONDS, queue,
new ThreadPoolExecutor.CallerRunsPolicy()
);
5.2 常见问题排查
-
线程泄漏:忘记关闭线程池
java复制executor.shutdown(); // 优雅关闭 -
任务堆积:队列设置不合理
- 根据业务特点选择队列类型:
- SynchronousQueue:直接传递
- LinkedBlockingQueue:无界队列
- ArrayBlockingQueue:有界队列
- 根据业务特点选择队列类型:
-
死锁检测:使用jstack工具
bash复制
jstack <pid> > thread_dump.log
6. 并发编程常见陷阱
6.1 内存可见性问题
典型例子:
java复制// 错误示范
public class VisibilityIssue {
private boolean flag = true;
public void run() {
new Thread(() -> {
while(flag) {} // 可能永远循环
}).start();
Thread.sleep(1000);
flag = false; // 主线程修改
}
}
解决方案:
- 使用volatile关键字
- 使用AtomicBoolean
- 通过synchronized保证可见性
6.2 死锁预防策略
死锁的四个必要条件:
- 互斥条件
- 占有且等待
- 不可抢占
- 循环等待
破解方法:
- 锁排序:按固定顺序获取锁
- 锁超时:tryLock(timeout)
- 死锁检测:定期检查线程依赖关系
7. Java内存模型(JMM)精要
7.1 happens-before原则
重要的happens-before关系:
- 程序顺序规则
- 锁规则
- volatile变量规则
- 线程启动规则
- 线程终止规则
- 中断规则
- 终结器规则
7.2 final字段的特殊语义
正确使用final可以保证线程安全:
java复制class ImmutableObject {
private final int value;
public ImmutableObject(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
8. 性能优化实战技巧
8.1 减少锁竞争
优化策略:
- 缩小同步范围
- 使用读写锁(ReentrantReadWriteLock)
- 采用无锁数据结构(ConcurrentHashMap)
- 锁分离技术
8.2 伪共享问题
CPU缓存行导致的性能问题:
java复制// 解决伪共享
@Contended // JDK8+
class Counter {
volatile long value1;
volatile long value2;
}
9. 常见面试问题解析
9.1 线程池执行流程
高频面试题答案要点:
- 核心线程是否已满?
- 队列是否已满?
- 最大线程是否已满?
- 执行拒绝策略
9.2 volatile关键字
必须掌握的三点特性:
- 可见性保证
- 禁止指令重排序
- 不保证原子性
10. 项目实战经验分享
在最近的风控系统中,我们遇到了这样的需求:需要实时处理来自多个数据源的交易事件,并进行风险评分。最终方案如下:
java复制// 1. 创建处理管道
ExecutorService[] processors = new ExecutorService[3];
Arrays.setAll(processors, i ->
Executors.newFixedThreadPool(4));
// 2. 构建处理流程
Disruptor<TransactionEvent> disruptor = new Disruptor<>(
TransactionEvent::new, 1024, Executors.defaultThreadFactory());
disruptor.handleEventsWith(
(event, sequence, endOfBatch) -> {
// 第一阶段处理
processors[0].submit(() -> validate(event));
}).then(
(event, sequence, endOfBatch) -> {
// 第二阶段处理
processors[1].submit(() -> score(event));
});
// 3. 启动处理
disruptor.start();
关键优化点:
- 使用Disruptor无锁队列提升吞吐量
- 不同阶段使用独立线程池隔离资源
- 采用背压机制防止OOM
这个方案将处理能力从原来的200TPS提升到了1500TPS,同时99%的请求延迟控制在50ms以内。
