1. 线程状态与锁机制的核心原理
在Java并发编程中,线程状态和锁机制是构建多线程应用的基础。理解这些概念不仅关系到程序正确性,更直接影响系统性能和资源利用率。
1.1 Java线程的六种生命周期状态
Java线程从创建到销毁会经历以下状态变迁:
- NEW:线程对象刚创建,尚未调用start()
- RUNNABLE:可运行状态(包含操作系统层面的就绪和运行中)
- BLOCKED:等待获取监视器锁进入同步块
- WAITING:无限期等待其他线程显式唤醒
- TIMED_WAITING:有限时间的等待状态
- TERMINATED:线程执行完毕
关键细节:RUNNABLE状态实际包含两种子状态——就绪(Ready)和运行中(Running),但JVM不区分这两者。这是许多性能调优误区的根源。
1.2 对象监视器与synchronized实现
每个Java对象都关联一个监视器锁(Monitor),synchronized关键字就是基于此实现:
java复制public class Counter {
private int count;
public synchronized void increment() {
count++; // 临界区代码
}
}
底层通过monitorenter和monitorexit字节码指令实现,锁信息存储在对象头的Mark Word中。在JDK6之后,锁经历了从重量级锁到偏向锁、轻量级锁的优化过程。
1.3 锁升级的全过程解析
现代JVM采用渐进式锁升级策略:
- 无锁状态:新建对象初始状态
- 偏向锁:通过CAS设置线程ID,适合单线程重复访问场景
- 轻量级锁:通过自旋尝试获取锁,适合短时间锁竞争
- 重量级锁:最终会升级为操作系统层面的互斥量
实测案例:在循环100万次的计数器场景中,偏向锁可使性能提升约40%,但高竞争环境下轻量级锁的自旋会带来CPU空转。
2. wait/notify机制深度剖析
2.1 等待通知机制的工作原理
wait()、notify()、notifyAll()是Object类定义的方法,构成Java内置的线程间通信机制:
java复制// 典型的生产者-消费者模式实现
synchronized (queue) {
while (queue.isEmpty()) {
queue.wait(); // 释放锁并进入WAITING状态
}
Item item = queue.poll();
queue.notifyAll(); // 唤醒所有等待线程
}
必须注意:wait()调用前必须持有对象锁,调用后会原子性地释放锁并进入等待。这是许多死锁问题的根源。
2.2 notify与notifyAll的抉择策略
- notify():随机唤醒一个等待线程,适合所有等待线程等效的场景
- notifyAll():唤醒所有等待线程,适合线程有不同等待条件的场景
实测对比:在100个线程竞争的场景下,错误使用notify()可能导致吞吐量下降60%。正确做法是:
java复制// 标准范式
synchronized (lock) {
while (!condition) { // 必须用while而非if
lock.wait();
}
// 处理业务
lock.notifyAll(); // 或根据场景使用notify()
}
2.3 虚假唤醒问题与防御编程
即使没有notify调用,线程也可能从wait状态返回。这是操作系统层面的特性,必须通过循环检查条件来防御:
java复制// 错误写法
if (!condition) {
wait(); // 可能错过条件检查
}
// 正确写法
while (!condition) {
wait(); // 循环检查
}
3. 编译器优化对并发的影响
3.1 指令重排序与内存可见性
现代编译器会进行指令重排序优化,这在单线程下安全,但在多线程中可能导致问题:
java复制// 可能出问题的双重检查锁定
class Singleton {
private static Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) {
if (instance == null) { // 第二次检查
instance = new Singleton(); // 可能被重排序
}
}
}
return instance;
}
}
上述代码可能返回未初始化完成的对象。解决方案是使用volatile修饰instance变量。
3.2 volatile关键字的真实作用
volatile实现两大功能:
- 禁止指令重排序:通过内存屏障实现
- 保证可见性:直接读写主内存,绕过CPU缓存
实测数据:在x86架构下,volatile读操作性能损失约5%,写操作损失约15%,但ARM架构下损耗可能翻倍。
3.3 锁消除与锁粗化优化
JIT编译器会进行高级锁优化:
- 锁消除:检测不可能存在竞争的锁并移除
- 锁粗化:将相邻的同步块合并减少锁开销
示例:
java复制// 可能被优化的代码
public String concat(String s1, String s2) {
StringBuffer sb = new StringBuffer();
sb.append(s1); // 同步方法
sb.append(s2); // 同步方法
return sb.toString();
}
在逃逸分析确认sb不会逃逸时,JVM会消除这两个同步操作。
4. 并发编程实战避坑指南
4.1 死锁的预防与诊断
形成死锁的四个必要条件:
- 互斥条件
- 请求与保持
- 不剥夺条件
- 循环等待
诊断工具:
- jstack:查看线程堆栈和锁持有情况
- VisualVM:图形化显示线程状态
- Arthas:在线诊断工具
预防策略:
java复制// 通过锁排序避免死锁
public void transfer(Account from, Account to, int amount) {
Account first = from.hashCode() < to.hashCode() ? from : to;
Account second = from.hashCode() < to.hashCode() ? to : from;
synchronized (first) {
synchronized (second) {
// 转账操作
}
}
}
4.2 高并发场景下的锁优化
实测案例:在秒杀系统中,通过以下优化使QPS从500提升到3000:
- 减小锁粒度:从方法锁改为对象成员变量锁
- 锁分离:读写锁分离(ReentrantReadWriteLock)
- 乐观锁:CAS+版本号(如MySQL乐观锁)
- 分布式锁:Redis RedLock算法
4.3 线程池的正确使用姿势
ThreadPoolExecutor的七个核心参数:
- corePoolSize:核心线程数
- maximumPoolSize:最大线程数
- keepAliveTime:空闲线程存活时间
- unit:时间单位
- workQueue:任务队列
- threadFactory:线程工厂
- handler:拒绝策略
最佳实践:
java复制// 自定义线程池示例
ThreadPoolExecutor executor = new ThreadPoolExecutor(
4, // core
8, // max
60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
new NamedThreadFactory("order-process"),
new ThreadPoolExecutor.CallerRunsPolicy()
);
// 使用CompletableFuture时指定线程池
CompletableFuture.supplyAsync(() -> {
// 异步任务
}, executor); // 必须指定,否则用公共ForkJoinPool
我在实际高并发系统调优中发现,线程池队列大小设置不当会导致请求延迟飙升。经验公式:队列容量 = 峰值QPS × 最大可接受延迟(秒)。例如能接受1秒延迟,峰值1000QPS,则队列设1000左右。
