1. 为什么并发编程如此重要?
记得刚入行时接手过一个电商促销系统,零点大促时服务器直接崩溃。排查发现是库存扣减时多个线程同时操作导致超卖。那次惨痛教训让我明白:不懂并发编程,就像开车不看红绿灯。Java作为企业级应用的首选语言,其并发能力直接影响系统稳定性和性能表现。
并发编程的三大核心特性(原子性、可见性、有序性)就像交通规则中的三色信号灯:
- 原子性是红灯:保证关键操作不可分割
- 可见性是黄灯:提醒线程间状态变化
- 有序性是绿灯:确保指令执行顺序可控
2. 原子性:不可分割的操作单元
2.1 什么是原子操作?
想象银行转账场景:A向B转账100元,需要执行两个操作:
- A账户减100
- B账户加100
如果这两个操作不是原子的,可能在A扣款后系统崩溃,导致钱凭空消失。原子性就是保证这两个操作要么都成功,要么都不执行。
2.2 Java中的原子性保障
Java提供了多种实现原子性的方式:
java复制// 方式1:synchronized关键字
public synchronized void transfer(Account from, Account to, int amount) {
from.debit(amount);
to.credit(amount);
}
// 方式2:Lock接口
private final Lock lock = new ReentrantLock();
public void transfer(Account from, Account to, int amount) {
lock.lock();
try {
from.debit(amount);
to.credit(amount);
} finally {
lock.unlock();
}
}
// 方式3:原子类
private AtomicInteger balance = new AtomicInteger(100);
public void withdraw(int amount) {
balance.getAndAdd(-amount);
}
实战经验:synchronized在JDK6后做了大量优化(偏向锁、轻量级锁等),不再是性能杀手。简单场景优先用synchronized,复杂锁策略再用Lock。
2.3 常见原子性问题案例
我曾遇到过一个计数器bug:
java复制// 错误示例
private int count = 0;
public void add() {
count++; // 这不是原子操作!
}
这个简单的++操作实际上包含:
- 读取count值
- 计算count+1
- 写入新值
多线程环境下会导致计数不准。修正方案:
java复制private AtomicInteger count = new AtomicInteger(0);
public void add() {
count.incrementAndGet(); // 原子操作
}
3. 可见性:线程间的状态同步
3.1 内存可见性问题本质
现代CPU有多级缓存架构,线程操作的是工作内存(CPU缓存)而非主内存。看这个典型问题:
java复制// 线程A
flag = true;
// 线程B
while(!flag) {
// 可能永远循环!
}
即使线程A已经修改flag,线程B可能永远看不到变化,因为值还在线程A的缓存中。
3.2 Java内存模型(JMM)解决方案
Java提供了多种保证可见性的机制:
- volatile关键字:
java复制private volatile boolean flag = false;
volatile变量写操作会立即刷新到主内存,读操作会从主内存重新加载。
- synchronized同步块:
java复制synchronized(lock) {
flag = true; // 退出同步块会强制刷新缓存
}
- final字段:
java复制final Map config = loadConfig(); // 正确构造后对所有线程可见
避坑指南:不要过度依赖volatile。它只能保证可见性,不保证原子性。比如volatile int的++操作仍需配合synchronized或原子类。
3.3 可见性实战案例
在开发消息队列时遇到过这样的问题:
java复制class Worker implements Runnable {
boolean running = true; // 错误:非volatile
public void run() {
while(running) {
// 处理任务
}
}
public void stop() {
running = false;
}
}
主线程调用stop()后,工作线程可能永远无法停止。解决方案:
java复制volatile boolean running = true;
// 或使用AtomicBoolean
4. 有序性:指令重排序的陷阱
4.1 什么是指令重排序?
CPU和编译器会优化指令执行顺序以提高性能。单线程下没问题,但多线程可能引发诡异bug。经典案例是双重检查锁定(DCL):
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;
}
}
问题在于new Singleton()不是原子操作,可能被重排序为:
- 分配内存空间
- 将引用指向内存(此时instance!=null)
- 初始化对象
导致其他线程拿到未初始化的实例。
4.2 保证有序性的方法
- volatile关键字:
java复制private static volatile Singleton instance;
volatile会禁止指令重排序,保证写操作前的操作不会重排序到写之后。
- final字段:
java复制final int x;
// 构造函数
public Foo() {
x = 42; // 保证对其他线程可见且有序
}
- happens-before原则:
- 线程start()前的操作对线程可见
- 解锁前的操作对后续加锁可见
- volatile写前的操作对后续读可见
4.3 有序性实战技巧
在开发连接池时遇到过这样的问题:
java复制class ConnectionPool {
private Map<String, Connection> pool;
private boolean initialized = false;
public void init() {
pool = createConnections(); // (1)
initialized = true; // (2)
}
public Connection get() {
if (!initialized) throw new IllegalStateException();
return pool.get(randomKey()); // 可能NPE
}
}
由于(1)和(2)可能重排序,导致get()看到initialized=true但pool还是null。解决方案:
java复制private volatile boolean initialized = false;
// 或使用final字段
private final Map<String, Connection> pool;
5. 三大特性的综合应用
5.1 线程安全计数器设计
结合三大特性实现高性能计数器:
java复制class ThreadSafeCounter {
private AtomicInteger count = new AtomicInteger(0);
private volatile boolean loggingEnabled = false;
public void increment() {
int newVal = count.incrementAndGet(); // 原子性
if (loggingEnabled) { // 可见性
System.out.println("New value: " + newVal);
}
}
public void setLogging(boolean enabled) {
this.loggingEnabled = enabled; // 有序性+可见性
}
}
5.2 并发集合类原理分析
以ConcurrentHashMap为例:
- 分段锁保证原子性
- volatile变量保证可见性
- final和volatile保证有序性
- CAS操作避免锁开销
5.3 性能优化权衡
三大特性的保障都有性能成本:
- 同步块:上下文切换开销
- volatile:禁用缓存,禁止重排序
- 原子类:CAS可能自旋
优化建议:
- 优先使用不可变对象
- 缩小同步范围
- 读写分离(CopyOnWriteArrayList)
- 无锁数据结构(ConcurrentLinkedQueue)
6. 常见问题排查指南
6.1 死锁问题
典型症状:线程卡住,CPU利用率低
排查工具:
- jstack查看线程栈
- VisualVM线程分析
预防措施:
- 按固定顺序获取锁
- 使用tryLock设置超时
- 避免嵌套锁
6.2 竞态条件
典型症状:结果不确定,偶尔出错
调试技巧:
- 使用ThreadLocalRandom重现问题
- 增加日志输出关键状态
- 使用断言检查不变式
6.3 内存一致性错误
典型症状:看到过期数据
解决方案:
- 检查所有共享变量是否恰当同步
- 使用final字段
- 考虑使用并发集合
7. 高级话题延伸
7.1 happens-before规则详解
Java内存模型定义了8种happens-before关系:
- 程序顺序规则
- 监视器锁规则
- volatile变量规则
- 线程启动规则
- 线程终止规则
- 中断规则
- finalizer规则
- 传递性
7.2 内存屏障原理
不同类型的屏障:
- LoadLoad屏障
- StoreStore屏障
- LoadStore屏障
- StoreLoad屏障
在x86上,volatile写相当于StoreStore + StoreLoad屏障。
7.3 无锁编程技巧
- CAS模式:
java复制do {
oldVal = atomic.get();
newVal = compute(oldVal);
} while (!atomic.compareAndSet(oldVal, newVal));
- 避免ABA问题:
- 使用版本号(AtomicStampedReference)
- 使用布尔标记(AtomicMarkableReference)
- 回退策略:
- 指数退避
- 随机延迟
8. 工具与最佳实践
8.1 必备诊断工具
- jstack:线程转储分析
- jconsole:可视化监控
- Arthas:在线诊断神器
- JProfiler:性能分析
8.2 代码审查要点
- 检查所有共享变量的访问
- 验证锁的范围是否足够
- 注意方法调用间的线程安全
- 检查异常处理是否释放锁
8.3 测试策略
- 压力测试:模拟高并发
- 随机性测试:使用不同线程调度
- 断言检查:验证不变式
- 代码覆盖:确保同步代码被测试
9. 个人实战心得
在金融支付系统开发中,我总结出几条黄金法则:
- 默认认为所有共享变量都是不安全的
- 优先使用并发工具类而非自己实现
- 同步块内尽量只做必要操作
- 文档中明确标注线程安全保证级别
- 性能优化前先证明存在瓶颈
最深刻的教训来自一个缓存实现:使用了ConcurrentHashMap但忽略了复合操作的原子性,导致缓存穿透。最终通过computeIfAbsent方法解决。这提醒我们:即使使用线程安全容器,也要注意操作组合的原子性。