1. 深入理解synchronized的两种锁机制
在Java多线程编程中,synchronized关键字是最基础的线程同步工具。很多开发者虽然经常使用它,但对静态方法和实例方法加锁的本质区别却存在理解偏差。我曾经在项目调试中就遇到过这样的场景:明明加了synchronized关键字,却依然出现线程安全问题,最后发现是把静态同步方法和实例同步方法混为一谈了。
静态同步方法锁定的是类的Class对象,而实例同步方法锁定的是当前实例对象。这个根本区别导致了它们在并发控制时的行为差异。举个例子,假设我们有一个银行账户类,如果要对账户余额这个静态变量进行操作,就必须使用静态同步方法;而如果是对单个账户实例的实例变量操作,则应该使用实例同步方法。
关键提示:选择错误的锁类型会导致看似正确的代码在实际运行时出现线程安全问题,这种bug往往在高压测试时才会暴露。
2. 静态同步方法解析
2.1 锁对象与作用范围
静态同步方法的锁对象是类的Class对象,这意味着无论创建多少个实例,所有静态同步方法都共享同一把锁。从JVM层面看,当类被加载时,JVM会为每个类创建一个唯一的Class对象,静态同步方法就是基于这个对象进行加锁的。
java复制public class Logger {
private static int logCount = 0;
public synchronized static void writeLog(String message) {
logCount++;
System.out.println("[" + logCount + "] " + message);
}
}
在这个日志工具类中,writeLog方法必须声明为静态同步方法,因为它要保护静态变量logCount的线程安全。即使创建多个Logger实例,所有调用writeLog的线程都会竞争同一把锁。
2.2 底层实现原理
从字节码层面看,静态同步方法会在方法访问标志位设置ACC_SYNCHRONIZED标记。当线程执行该方法时,JVM会先获取类对象的监视器锁(monitor),方法执行完毕后再释放锁。这等价于以下代码:
java复制public static void writeLog(String message) {
synchronized(Logger.class) {
logCount++;
System.out.println("[" + logCount + "] " + message);
}
}
在实际开发中,我曾经遇到一个性能问题:某个工具类中大量使用静态同步方法,导致不同业务线程之间出现不必要的阻塞。后来通过将不相关的静态方法拆分成不同的工具类,减少了锁竞争。
3. 实例同步方法详解
3.1 锁对象与作用范围
实例同步方法的锁对象是当前实例(this),每个实例拥有自己独立的锁。这意味着不同实例的同步方法可以并行执行,不会相互阻塞。这种特性非常适合保护实例级别的状态。
java复制public class BankAccount {
private double balance;
public synchronized void deposit(double amount) {
balance += amount;
}
public synchronized void withdraw(double amount) {
if (balance >= amount) {
balance -= amount;
}
}
}
在这个银行账户例子中,deposit和withdraw方法都使用实例同步,确保对单个账户余额的操作是线程安全的。但不同账户之间的操作互不影响,这符合银行业务的实际需求。
3.2 实现机制与注意事项
实例同步方法同样使用ACC_SYNCHRONIZED标志位,但锁对象是当前实例。特别需要注意的是,如果同步方法中调用了其他同步方法(无论是实例方法还是静态方法),锁的获取和释放都需要格外小心。
我曾经调试过一个死锁问题:线程A持有实例锁后尝试获取类锁,而线程B持有类锁后尝试获取实例锁,形成了典型的交叉死锁。解决方案是统一锁的获取顺序,或者使用更细粒度的锁策略。
4. 两种锁的对比实验
4.1 实验设计与执行
为了直观展示两种锁的区别,我设计了一个对比实验。创建两个实例,分别用不同线程调用它们的静态同步方法和实例同步方法:
java复制public class LockComparison {
public synchronized static void staticLock() {
System.out.println("静态锁开始 - " + Thread.currentThread().getName());
try { Thread.sleep(1000); } catch (InterruptedException e) {}
System.out.println("静态锁结束 - " + Thread.currentThread().getName());
}
public synchronized void instanceLock() {
System.out.println("实例锁开始 - " + Thread.currentThread().getName());
try { Thread.sleep(1000); } catch (InterruptedException e) {}
System.out.println("实例锁结束 - " + Thread.currentThread().getName());
}
public static void main(String[] args) {
LockComparison obj1 = new LockComparison();
LockComparison obj2 = new LockComparison();
// 测试静态锁
new Thread(() -> obj1.staticLock(), "静态线程1").start();
new Thread(() -> obj2.staticLock(), "静态线程2").start();
// 测试实例锁
new Thread(() -> obj1.instanceLock(), "实例线程1").start();
new Thread(() -> obj2.instanceLock(), "实例线程2").start();
}
}
4.2 结果分析与解释
运行结果清晰地展示了两种锁的区别:
code复制静态锁开始 - 静态线程1
实例锁开始 - 实例线程1
实例锁开始 - 实例线程2
静态锁结束 - 静态线程1
静态锁开始 - 静态线程2
实例锁结束 - 实例线程1
实例锁结束 - 实例线程2
静态锁结束 - 静态线程2
可以看到,静态同步方法即使在不同实例上调用也会互斥,而实例同步方法在不同实例上可以并行执行。这个实验验证了我们前面的理论分析。
5. 实际应用场景与选择建议
5.1 静态同步方法适用场景
静态同步方法最适合保护类级别的共享资源,常见场景包括:
- 静态变量的修改(如计数器、缓存)
- 类加载时的初始化操作
- 工具类中的共享资源访问
在开发配置中心时,我们使用静态同步方法来保证配置加载的线程安全:
java复制public class ConfigCenter {
private static Map<String, String> configs = new HashMap<>();
public synchronized static void loadConfig(String key, String value) {
configs.put(key, value);
}
public synchronized static String getConfig(String key) {
return configs.get(key);
}
}
5.2 实例同步方法适用场景
实例同步方法适合保护实例状态,典型场景有:
- 对象内部状态的修改(如银行账户余额)
- 实例缓存的访问
- 需要隔离不同实例的操作
在电商系统中,购物车的并发控制就使用了实例同步:
java复制public class ShoppingCart {
private List<Item> items = new ArrayList<>();
public synchronized void addItem(Item item) {
items.add(item);
}
public synchronized void removeItem(Item item) {
items.remove(item);
}
}
5.3 混合使用注意事项
当类中同时存在静态同步方法和实例同步方法时,要特别注意:
- 避免在实例同步方法中调用静态同步方法,容易导致死锁
- 不要过度使用静态同步方法,会降低系统吞吐量
- 考虑使用更细粒度的锁替代粗粒度的同步方法
我曾经优化过一个性能瓶颈,将全局的静态同步方法拆分为使用ConcurrentHashMap的分段锁,性能提升了5倍以上。
6. 常见问题与解决方案
6.1 锁竞争问题排查
在实际项目中,我们使用jstack和VisualVM等工具分析锁竞争情况。一个典型的排查流程:
- 通过线程dump查看阻塞的线程
- 分析锁持有者和等待者
- 确定是静态锁还是实例锁导致的竞争
- 根据业务场景优化锁策略
6.2 性能优化技巧
基于项目经验,我总结了几种优化锁性能的方法:
- 减小同步块的范围(同步关键代码而非整个方法)
- 使用读写锁替代互斥锁(适合读多写少场景)
- 考虑使用并发集合类(如ConcurrentHashMap)
- 对于静态数据,可以尝试使用不可变对象
6.3 典型错误案例
最常见的错误是混淆两种锁的使用场景:
- 错误1:用实例同步方法保护静态变量
- 错误2:在单例模式中错误使用实例同步
- 错误3:在静态初始化块中使用同步
我曾经review过一个代码,开发者在工具类中错误地使用实例同步方法保护静态变量,导致在高并发时出现数据不一致问题。修正为静态同步方法后问题解决。
7. 高级话题延伸
7.1 与其他同步机制对比
相比synchronized,ReentrantLock提供了更灵活的锁操作:
- 可中断的锁获取
- 公平锁选项
- 多个条件变量
- 尝试获取锁的超时机制
但在简单场景下,synchronized仍然是首选,因为它的使用更简单,且JVM会进行优化。
7.2 JVM对synchronized的优化
现代JVM对synchronized做了大量优化:
- 锁偏向:减少无竞争时的开销
- 轻量级锁:使用CAS操作避免重量级锁
- 锁消除:逃逸分析后去除不必要的锁
- 锁粗化:合并连续的同步块
理解这些优化有助于我们编写更高效的同步代码。例如,在热点代码中避免频繁的锁获取/释放,可以利用锁粗化优化。