1. 为什么需要线程锁?
我第一次接触多线程编程时,曾经遇到过这样一个场景:开发了一个简单的银行转账系统,两个线程同时对一个账户进行存取款操作。理论上应该保持账户余额的正确性,但实际运行结果却经常出现金额对不上的情况。这就是典型的线程安全问题。
在Java中,当多个线程同时访问共享资源时,如果没有适当的同步机制,就可能出现数据不一致的问题。比如:
- 线程A读取账户余额为100元
- 线程B也读取账户余额为100元
- 线程A存入50元,余额变为150元
- 线程B取出30元,余额变为70元
最终账户余额应该是120元(100+50-30),但由于缺乏同步,实际结果却是错误的70元。这就是我们常说的"竞态条件"(Race Condition)。
2. synchronized的基本用法
2.1 同步方法
最简单的同步方式是在方法声明中添加synchronized关键字:
java复制public synchronized void transfer(int amount) {
// 转账逻辑
}
这种写法等同于:
java复制public void transfer(int amount) {
synchronized(this) {
// 转账逻辑
}
}
注意:同步方法锁定的是当前对象实例(this),如果不同线程操作的是同一个对象实例,那么这些同步方法会互斥执行;但如果操作的是不同实例,则不会互斥。
2.2 同步代码块
更细粒度的控制可以使用同步代码块:
java复制public void transfer(Account target, int amount) {
synchronized(this) {
synchronized(target) {
// 转账逻辑
}
}
}
这里需要注意避免死锁问题。我建议按照固定的顺序获取锁,比如先锁金额小的账户,再锁金额大的账户。
2.3 静态同步方法
对于静态方法,锁的是类的Class对象:
java复制public static synchronized void staticMethod() {
// 静态方法逻辑
}
等同于:
java复制public static void staticMethod() {
synchronized(MyClass.class) {
// 静态方法逻辑
}
}
3. synchronized的实现原理
3.1 对象头与Monitor
每个Java对象都有一个对象头,其中包含Mark Word用于存储对象的运行时数据。当对象作为锁时,Mark Word会指向一个Monitor对象(也称为管程或监视器锁)。
Monitor包含以下关键字段:
- Owner:持有锁的线程
- EntryList:等待获取锁的线程队列
- WaitSet:调用了wait()方法的线程队列
3.2 锁的升级过程
JDK1.6之后,synchronized实现了锁升级机制:
- 无锁状态:初始状态,没有线程竞争
- 偏向锁:第一个线程访问时,会在对象头和栈帧中记录线程ID
- 轻量级锁:当有第二个线程尝试获取锁时,升级为轻量级锁(通过CAS操作)
- 重量级锁:当线程竞争激烈时,最终会升级为重量级锁(传统的Monitor实现)
这种设计大幅提升了单线程重复获取锁的性能。
4. 实际应用中的注意事项
4.1 锁的范围要合理
常见错误是锁的范围过大或过小:
- 范围过大:影响并发性能
- 范围过小:无法保证线程安全
我个人的经验法则是:只锁必要的共享数据,且锁的粒度要尽可能小。
4.2 避免死锁
死锁的四个必要条件:
- 互斥条件
- 请求与保持条件
- 不剥夺条件
- 循环等待条件
避免死锁的实用技巧:
- 按固定顺序获取锁
- 使用tryLock()设置超时时间
- 避免在锁内调用外部方法(可能引入未知的锁)
4.3 性能考量
synchronized在JDK不断优化下性能已经很好,但在高并发场景仍需注意:
- 减少锁的持有时间
- 降低锁的粒度
- 考虑读写分离(ReadWriteLock)
- 对于极高并发,可以考虑CAS操作或并发容器
5. 常见问题排查
5.1 锁未被释放
典型症状:线程卡死,无法继续执行。可能原因:
- 同步代码块中抛出异常未捕获
- 递归调用导致重入次数不匹配
解决方法:
java复制synchronized(lock) {
try {
// 业务逻辑
} catch(Exception e) {
// 异常处理
}
}
5.2 锁竞争激烈
使用JVisualVM或Arthas等工具可以查看线程状态。如果大量线程处于BLOCKED状态,说明锁竞争激烈。
优化方案:
- 减小锁粒度
- 使用分段锁
- 考虑无锁编程
5.3 错误使用锁对象
常见错误:
java复制private String lock = "lock";
public void method() {
synchronized(lock) {
lock = "new lock"; // 错误!改变了锁对象引用
// 业务逻辑
}
}
解决方法:使用final修饰锁对象
java复制private final String lock = "lock";
6. 与其他同步机制对比
6.1 synchronized vs ReentrantLock
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 实现方式 | JVM内置 | JDK实现 |
| 可中断 | 不支持 | 支持 |
| 公平锁 | 非公平 | 可配置 |
| 条件变量 | 单一 | 多个 |
| 性能 | JDK6后优化好 | 高竞争时更优 |
6.2 synchronized vs volatile
volatile只能保证可见性,不能保证原子性。适合一写多读的场景。
synchronized既能保证可见性,也能保证原子性,但会带来性能开销。
7. 最佳实践建议
- 明确锁的边界:在代码中清晰标注哪些部分需要同步
- 文档化锁策略:在类文档中说明使用了哪些锁及其作用
- 避免锁泄露:不要将锁对象暴露给外部代码
- 性能测试:对关键同步代码进行压力测试
- 监控锁竞争:生产环境监控锁竞争情况
我在实际项目中总结的一个有用模式是"私有锁"模式:
java复制public class Account {
// 私有锁对象,不对外暴露
private final Object lock = new Object();
public void transfer(int amount) {
synchronized(lock) {
// 转账逻辑
}
}
}
这种方式比直接使用this作为锁更安全,因为外部代码无法获取到这个锁对象。
