1. 同步问题的本质与解决方案
在多线程编程中,最核心的挑战就是如何解决资源竞争问题。想象一下多个线程同时操作同一个银行账户的场景:如果没有适当的控制机制,存款和取款操作交叉执行就可能导致余额计算错误。这就是典型的竞态条件(Race Condition)问题。
Java 语言从设计之初就考虑到了线程安全问题,提供了多种同步机制。其中 synchronized 关键字是最基础、最常用的线程同步工具。它就像会议室的门锁——当一个人在里面开会时,其他人必须等待锁释放才能进入。
与其它同步机制相比,synchronized 有三大特点:
- 内置语言特性,无需额外导入类库
- 自动管理锁的获取和释放
- 可作用于方法或代码块,使用灵活
2. synchronized 的实现原理
2.1 JVM 层面的实现机制
在 JVM 中,每个对象都有一个内置锁(monitor lock),也称为监视器锁。当线程进入 synchronized 方法或代码块时,会自动获取这个锁;退出时自动释放。这个机制是通过 monitorenter 和 monitorexit 这两个字节码指令实现的。
对象头中的 Mark Word 存储了锁状态信息,包括:
- 锁标志位(2bit)
- 是否偏向锁(1bit)
- 锁记录指针
- 重量级锁指针
2.2 锁升级过程解析
为了提高性能,JVM 采用了锁升级策略:
- 无锁状态:对象刚创建时的初始状态
- 偏向锁:当第一个线程访问时,会记录线程ID,后续该线程可以直接进入
- 轻量级锁:当有第二个线程尝试获取锁时,升级为CAS自旋锁
- 重量级锁:当自旋超过一定次数(默认10次),升级为操作系统层面的互斥锁
注意:锁升级是不可逆的过程,一旦升级为重量级锁就无法降级
3. synchronized 的四种使用方式
3.1 实例方法同步
java复制public synchronized void transfer(Account target, int amount) {
this.balance -= amount;
target.balance += amount;
}
这种写法等价于:
java复制public void transfer(Account target, int amount) {
synchronized(this) {
this.balance -= amount;
target.balance += amount;
}
}
3.2 静态方法同步
java复制public static synchronized void log(String message) {
System.out.println(LocalDateTime.now() + ": " + message);
}
静态同步方法锁定的是类的Class对象,等价于:
java复制public static void log(String message) {
synchronized(MyClass.class) {
System.out.println(LocalDateTime.now() + ": " + message);
}
}
3.3 同步代码块(对象锁)
java复制public void addToInventory(Item item) {
// 非同步代码
System.out.println("Processing item: " + item.name());
synchronized(this) {
inventory.add(item);
count++;
}
// 非同步代码
System.out.println("Item added");
}
3.4 同步代码块(自定义锁)
java复制private final Object lock = new Object();
public void updateCache() {
// 其他操作...
synchronized(lock) {
// 更新缓存操作
}
}
4. 锁的粒度与性能优化
4.1 细粒度锁实践
错误的粗粒度锁示例:
java复制public class OrderService {
public synchronized void processOrder(Order order) {
// 处理订单逻辑(耗时操作)
}
}
改进后的细粒度锁:
java复制public class OrderService {
private final Map<Long, Object> orderLocks = new ConcurrentHashMap<>();
public void processOrder(Order order) {
Object lock = orderLocks.computeIfAbsent(order.getId(), k -> new Object());
synchronized(lock) {
// 处理订单逻辑
}
}
}
4.2 避免死锁的编码规范
死锁产生的四个必要条件:
- 互斥条件
- 占有且等待
- 不可剥夺
- 循环等待
避免死锁的最佳实践:
- 按固定顺序获取锁
- 设置锁超时时间(可结合Lock接口实现)
- 使用开放调用(避免在持有锁时调用外部方法)
5. synchronized 的局限性及替代方案
5.1 功能局限性对比
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 可中断锁 | ❌ | ✔️ |
| 公平锁 | ❌ | ✔️ |
| 尝试获取锁 | ❌ | ✔️ |
| 多条件变量 | ❌ | ✔️ |
| 性能 | 优化后较好 | 适中 |
5.2 适用场景建议
推荐使用 synchronized 的场景:
- 简单的线程安全控制
- 锁持有时间很短的操作
- 不需要高级锁特性的情况
推荐使用 Lock 的场景:
- 需要可中断的锁获取
- 需要尝试获取锁(tryLock)
- 需要公平锁机制
- 需要绑定多个条件变量
6. 常见问题排查与性能调优
6.1 典型问题排查表
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 性能突然下降 | 锁竞争激烈升级为重量级锁 | 减小同步块范围或拆分锁 |
| CPU占用高但吞吐量低 | 线程在轻量级锁自旋 | 检查是否持有锁时间过长 |
| 出现死锁 | 循环等待资源 | jstack分析线程转储 |
| 非预期的并发修改异常 | 漏加同步控制 | 检查所有共享变量访问路径 |
6.2 性能优化技巧
- 减小同步范围:只在必要的地方加锁
java复制// 不推荐
public synchronized void process() {
// 前置处理(不需要同步)
// 核心逻辑(需要同步)
// 后置处理(不需要同步)
}
// 推荐
public void process() {
// 前置处理
synchronized(this) {
// 核心逻辑
}
// 后置处理
}
- 降低锁粒度:使用多个锁保护不同资源
java复制class BankAccount {
private final Object balanceLock = new Object();
private final Object passwordLock = new Object();
// 分别用不同锁保护不同资源
}
- 避免锁嵌套:容易导致死锁
java复制// 危险代码
synchronized(lockA) {
synchronized(lockB) {
// ...
}
}
7. 现代JVM对synchronized的优化
7.1 锁消除(Lock Elimination)
JIT编译器通过逃逸分析,发现某些锁不可能被共享访问时,会直接移除同步操作。例如:
java复制public String concat(String s1, String s2) {
StringBuffer sb = new StringBuffer(); // 局部变量,不会逃逸
sb.append(s1);
sb.append(s2);
return sb.toString();
}
在这个例子中,StringBuffer的同步操作会被完全消除。
7.2 锁粗化(Lock Coarsening)
当JVM检测到一连串连续的对同一个对象加锁和解锁操作时,会把多个锁合并为一个更大的锁范围。例如:
java复制for(int i=0; i<100; i++) {
synchronized(this) {
// 小量操作
}
}
// 会被优化为:
synchronized(this) {
for(int i=0; i<100; i++) {
// 小量操作
}
}
8. 实战中的经验法则
-
优先使用同步块而非同步方法:可以更精确控制锁范围
-
对写操作加锁,读操作考虑CopyOnWrite:根据场景选择合适策略
-
避免在构造方法中同步:可能导致this引用逃逸
-
谨慎同步静态方法:类锁会影响所有实例的访问
-
使用private final对象作为锁:防止锁对象被意外修改
-
同步块内尽量简短:减少锁持有时间
-
考虑使用并发容器:如ConcurrentHashMap替代同步的HashMap
-
监控锁竞争情况:使用JVisualVM等工具观察线程阻塞情况