1. 线程基础与核心方法解析
多线程编程是现代软件开发中提升性能的利器,但也是一把双刃剑。记得我刚接触线程时,最困惑的就是那些看似简单的方法背后隐藏的陷阱。比如一个简单的sleep(),在不同场景下的表现可能天差地别。
1.1 线程生命周期关键方法
start()方法看似只是启动线程,实际上它完成了从NEW状态到RUNNABLE状态的转变。这里有个新手常踩的坑:重复调用start()会抛出IllegalThreadStateException。我曾在生产环境遇到过因为这个异常导致的流程中断,后来养成了用thread.getState()先检查状态的习惯。
java复制Thread thread = new Thread(() -> {
System.out.println("线程执行中");
});
thread.start();
// thread.start(); // 第二次调用会抛出异常
yield()是个有趣的方法,它提示调度器当前线程愿意让出CPU。但在实际使用中要注意:
- 它只是个提示,JVM不保证立即切换
- 过度使用可能导致上下文切换开销
- 我在性能测试中发现,在计算密集型任务中适当使用yield()能提升约15%的吞吐量
1.2 线程控制三剑客
join()方法是我调试多线程问题时最常用的工具之一。它的超时版本特别实用:
java复制Thread worker = new Thread(heavyTask);
worker.start();
worker.join(5000); // 最多等待5秒
if (worker.isAlive()) {
worker.interrupt(); // 超时后中断
}
interrupt()的误区最多。很多人以为调用它线程就会立即停止,实际上:
- 它只是设置中断标志
- 线程需要检查isInterrupted()来响应中断
- 阻塞方法(sleep/wait等)会抛出InterruptedException
重要经验:处理InterruptedException时,通常应该恢复中断状态(Thread.currentThread().interrupt()),除非你明确知道不需要。
2. 守护线程的实战应用
守护线程(Daemon Thread)就像系统的后勤人员,当所有非守护线程结束时,它们会自动退出。这个特性用好了能省去很多资源清理的麻烦。
2.1 典型使用场景
我常用守护线程来做:
- 日志轮转
- 内存监控
- 临时文件清理
- 心跳检测
配置方法很简单:
java复制Thread daemon = new Thread(cleanupTask);
daemon.setDaemon(true); // 必须在start()前调用
daemon.start();
2.2 使用注意事项
-
资源释放问题:守护线程被强制终止时,finally块可能不会执行。我曾因此导致文件锁未释放,后来改为用ShutdownHook辅助清理。
-
定时任务风险:如果守护线程是唯一活跃线程,ScheduledExecutorService也会停止。解决方案是混用非守护线程:
java复制ScheduledExecutorService executor = Executors.newScheduledThreadPool(2, r -> {
Thread t = new Thread(r);
t.setDaemon(true);
return t;
});
// 添加一个非守护线程保持活跃
executor.submit(() -> { while(true); });
- I/O操作慎用:网络连接等长时间I/O操作可能被意外中断,需要设计重试机制。
3. 线程状态监控实战
理解线程状态对排查问题至关重要。我整理了一个状态转换速查表:
| 状态 | 触发条件 | 恢复方式 |
|---|---|---|
| NEW | 线程刚创建 | start() |
| RUNNABLE | 就绪或运行中 | 系统调度 |
| BLOCKED | 等待监视器锁 | 获取锁 |
| WAITING | 无时限的wait()/join() | notify()/线程结束 |
| TIMED_WAITING | 有时限的sleep()/wait() | 超时或通知 |
| TERMINATED | 线程执行完毕 | 不可恢复 |
调试技巧:
- 用jstack或VisualVM抓取线程dump
- 关注BLOCKED状态的线程,可能是死锁征兆
- TIMED_WAITING状态过多可能提示配置不合理
4. 线程通信模式详解
4.1 wait/notify机制
经典的生产者-消费者模型实现:
java复制public class MessageQueue {
private final Queue<String> queue = new LinkedList<>();
private final int maxSize;
public synchronized void put(String msg) throws InterruptedException {
while(queue.size() == maxSize) {
wait(); // 释放锁并等待
}
queue.add(msg);
notifyAll(); // 唤醒所有等待线程
}
public synchronized String take() throws InterruptedException {
while(queue.isEmpty()) {
wait();
}
String msg = queue.poll();
notifyAll();
return msg;
}
}
关键点:始终在循环中检查条件(while不是if),避免虚假唤醒问题。
4.2 现代替代方案
我更推荐使用java.util.concurrent包的工具:
- BlockingQueue:ArrayBlockingQueue/LinkedBlockingQueue
- CountDownLatch:等待多个任务完成
- CyclicBarrier:可重复使用的同步屏障
- Phaser:更灵活的阶段同步器
示例:用CountDownLatch实现并行计算聚合
java复制CountDownLatch latch = new CountDownLatch(3);
ExecutorService executor = Executors.newFixedThreadPool(3);
List<Future<Result>> futures = new ArrayList<>();
for (int i = 0; i < 3; i++) {
futures.add(executor.submit(() -> {
try {
return computePartialResult();
} finally {
latch.countDown();
}
}));
}
latch.await(); // 等待所有任务完成
Result finalResult = mergeResults(futures);
5. 性能优化与避坑指南
5.1 上下文切换成本实测
我用JMH做了个基准测试,对比不同线程数下的吞吐量:
| 线程数 | 吞吐量(ops/ms) | 上下文切换次数(/秒) |
|---|---|---|
| 1 | 1250 | 0 |
| 2 | 2100 | 3500 |
| 4 | 2800 | 8900 |
| 8 | 3100 | 21500 |
| 16 | 2900 | 48000 |
结论:线程数不是越多越好,超过CPU核心数后收益递减。
5.2 线程池最佳实践
-
参数配置公式:
- CPU密集型:核心线程数 = CPU核心数 + 1
- I/O密集型:核心线程数 = CPU核心数 * (1 + 平均等待时间/平均计算时间)
-
队列选择策略:
- 直接交付:SynchronousQueue(无界线程池)
- 无界队列:LinkedBlockingQueue(固定大小线程池)
- 有界队列:ArrayBlockingQueue(需要拒绝策略)
-
拒绝策略对比:
- AbortPolicy:直接抛出异常(默认)
- CallerRunsPolicy:由调用线程执行
- DiscardPolicy:静默丢弃
- DiscardOldestPolicy:丢弃队列最老任务
示例:Web服务线程池配置
java复制int coreSize = Runtime.getRuntime().availableProcessors();
ThreadPoolExecutor executor = new ThreadPoolExecutor(
coreSize,
coreSize * 2,
60L, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
new ThreadPoolExecutor.CallerRunsPolicy());
5.3 常见死锁场景
- 锁顺序死锁:
java复制// 线程1
synchronized(lockA) {
synchronized(lockB) { ... }
}
// 线程2
synchronized(lockB) {
synchronized(lockA) { ... }
}
解决方案:全局定义锁获取顺序
- 资源死锁:
- 线程池任务等待另一个任务完成
- 但线程池已满,无法执行新任务
解决方案:使用ForkJoinPool或设置合理的线程数
- ThreadLocal内存泄漏:
- 线程池中线程会重用
- ThreadLocal值可能积累
解决方案:每次任务完成后调用ThreadLocal.remove()
6. Java内存模型深入
6.1 可见性问题重现
这个例子展示了典型的可见性问题:
java复制public class VisibilityDemo {
private static boolean ready = false;
private static int number;
public static void main(String[] args) {
new Thread(() -> {
while(!ready) {
// 可能永远循环
}
System.out.println(number);
}).start();
number = 42;
ready = true;
}
}
解决方案:对共享变量使用volatile或同步
6.2 happens-before规则
Java内存模型的关键规则:
- 程序顺序规则:线程内操作按程序顺序
- 锁规则:解锁先于后续加锁
- volatile规则:写先于后续读
- 线程启动规则:start()先于线程内操作
- 线程终止规则:线程内操作先于其他线程检测到它终止
- 中断规则:interrupt()调用先于检测中断
- 终结器规则:构造函数先于finalize()
- 传递性:A先于B,B先于C,则A先于C
7. 现代并发工具实战
7.1 CompletableFuture组合异步
链式调用示例:
java复制CompletableFuture.supplyAsync(() -> queryDatabase())
.thenApplyAsync(result -> transformData(result))
.thenAcceptAsync(transformed -> sendToAPI(transformed))
.exceptionally(ex -> {
logger.error("处理失败", ex);
return null;
});
7.2 StampedLock优化读写
比ReentrantReadWriteLock性能更好的选择:
java复制class Point {
private double x, y;
private final StampedLock sl = new StampedLock();
void move(double deltaX, double deltaY) {
long stamp = sl.writeLock();
try {
x += deltaX;
y += deltaY;
} finally {
sl.unlockWrite(stamp);
}
}
double distanceFromOrigin() {
long stamp = sl.tryOptimisticRead();
double currentX = x, currentY = y;
if (!sl.validate(stamp)) {
stamp = sl.readLock();
try {
currentX = x;
currentY = y;
} finally {
sl.unlockRead(stamp);
}
}
return Math.sqrt(currentX * currentX + currentY * currentY);
}
}
7.3 并行流注意事项
虽然parallelStream()用起来简单,但要注意:
- 默认使用ForkJoinPool.commonPool()
- 适合无状态操作
- 避免在内部进行I/O
- 大任务才值得并行化
示例:安全使用并行流
java复制List<Data> results = largeList.parallelStream()
.filter(this::cpuIntensivePredicate)
.collect(Collectors.toList());