1. 线程同步的核心挑战
在多线程编程中,最令人头疼的问题莫过于多个线程同时访问共享资源时引发的数据竞争(Data Race)。想象一下超市收银台的场景:当多个顾客(线程)同时抢购最后一件商品(共享资源)时,如果没有排队机制(同步控制),就会导致库存统计错误、支付混乱等问题。在Java中,synchronized关键字就是解决这类问题的关键工具。
我曾在电商平台的库存管理系统里遇到过典型的线程安全问题。某个促销活动期间,系统日志显示某商品实际售出数量超过了库存总量,这就是典型的"超卖"现象。通过添加synchronized关键字修饰库存扣减方法,问题立即得到解决。这个经历让我深刻认识到,理解synchronized的实现原理和使用技巧,是Java开发者从初级向中级跃迁的必经之路。
2. synchronized的底层实现机制
2.1 对象监视器(Monitor)原理
每个Java对象都内置了一个监视器锁(Monitor),这是synchronized实现同步的基础。可以把Monitor想象成特殊房间的门禁系统:
- 进入区(Entry Set):线程尝试获取锁时在此等待
- 拥有者(Owner):成功获取锁的线程独占资源
- 等待区(Wait Set):调用wait()的线程在此休眠
当执行synchronized代码块时,JVM会通过以下步骤管理锁:
// 编译后的字节码示例 monitorenter // 尝试获取对象锁 try { // 同步代码块内容 } finally { monitorexit // 释放锁 }关键点:monitorenter和monitorexit指令必须成对出现,这解释了为什么synchronized能保证即使抛出异常也会释放锁。
2.2 锁的升级优化过程
现代JVM(如HotSpot)采用锁升级策略优化性能:
- 无锁状态:新建对象初始状态
- 偏向锁:通过CAS记录线程ID,适合单线程重复访问场景
- 轻量级锁:通过自旋尝试获取锁,适合短时间竞争
- 重量级锁:真正的互斥锁,线程进入阻塞状态
通过以下命令可以查看对象头中的锁状态信息:
java -XX:+PrintFlagsFinal | grep BiasedLocking3. synchronized的四种使用方式
3.1 实例方法同步
public class Counter { private int count = 0; public synchronized void increment() { count++; // 原子操作 } }锁对象:当前实例(this)适用场景:需要保护实例变量的场合
3.2 静态方法同步
public class Logger { private static int logCount = 0; public static synchronized void log(String message) { logCount++; System.out.println(message); } }锁对象:类的Class对象(Logger.class)适用场景:需要保护类静态变量的场合
3.3 代码块同步(实例锁)
public void transfer(Account target, int amount) { synchronized(this) { synchronized(target) { this.balance -= amount; target.balance += amount; } } }警告:这种嵌套锁容易导致死锁,应该按照固定顺序获取锁
3.4 代码块同步(类锁)
public class ConfigManager { private static Map<String, String> configs = new HashMap<>(); public static void updateConfig(String key, String value) { synchronized(ConfigManager.class) { configs.put(key, value); } } }4. 高级特性与性能优化
4.1 锁的可重入性
synchronized具有可重入特性,即线程可以重复获取已经持有的锁。这个特性通过锁计数器实现:
public class ReentrantDemo { public synchronized void methodA() { methodB(); // 可重入调用 } public synchronized void methodB() { // ... } }每个锁关联一个计数器和一个所有者线程。当计数器为0时表示未被占用,线程请求时将计数器加1;同一线程再次获取时计数器继续递增。
4.2 锁消除与锁粗化
锁消除:JIT编译器通过逃逸分析,发现某些锁不可能被共享时,会直接消除同步操作。例如:
public String concatStrings(String s1, String s2) { StringBuffer sb = new StringBuffer(); // 局部变量,不会逃逸 sb.append(s1); sb.append(s2); return sb.toString(); }锁粗化:将相邻的同步块合并,减少锁的获取/释放次数:
// 优化前 for(int i=0; i<100; i++) { synchronized(this) { doSomething(); } } // 优化后 synchronized(this) { for(int i=0; i<100; i++) { doSomething(); } }5. 常见问题排查指南
5.1 死锁检测与解决
典型的死锁场景:
// 线程1 synchronized(lockA) { synchronized(lockB) { ... } } // 线程2 synchronized(lockB) { synchronized(lockA) { ... } }排查工具:
- jstack命令生成线程转储
- JConsole或VisualVM的可视化分析
- 第三方工具如Arthas
解决方案:
- 统一锁的获取顺序
- 使用tryLock()设置超时
- 减少同步代码块粒度
5.2 性能瓶颈定位
当系统出现以下症状时,可能是锁竞争导致:
- CPU使用率高但吞吐量低
- 线程状态大量处于BLOCKED
- 平均响应时间波动大
优化建议:
- 使用JMC(Java Mission Control)分析锁竞争情况
- 考虑改用并发容器(如ConcurrentHashMap)
- 尝试读写锁(ReentrantReadWriteLock)替代
6. 最佳实践与替代方案
6.1 synchronized使用守则
最小化同步范围:只同步必要的代码段
// 不推荐 public synchronized void process() { // 大量非同步操作... count++; // 只有这一行需要同步 } // 推荐 public void process() { // 非同步操作... synchronized(this) { count++; } }避免锁嵌套:容易导致死锁和性能问题
谨慎选择锁对象:使用private final对象作为锁
private final Object lock = new Object(); public void safeMethod() { synchronized(lock) { // ... } }
6.2 替代方案比较
| 特性 | synchronized | ReentrantLock | StampedLock |
|---|---|---|---|
| 实现方式 | JVM内置 | Java代码实现 | Java代码实现 |
| 尝试非阻塞获取 | 不支持 | tryLock() | tryOptimisticRead() |
| 公平锁 | 不支持 | 支持 | 不支持 |
| 读写分离 | 不支持 | 不支持 | 支持 |
| 锁降级 | 不支持 | 不支持 | 支持 |
| 条件变量 | 有限支持 | 多条件支持 | 不支持 |
对于高并发场景,我个人更倾向于以下选择策略:
- 读多写少:优先考虑StampedLock
- 需要条件队列:选择ReentrantLock
- 简单同步需求:synchronized仍是首选
7. 实战:设计线程安全的LRU缓存
让我们用synchronized实现一个简易的线程安全LRU缓存:
public class SynchronizedLRUCache<K, V> { private final int capacity; private final LinkedHashMap<K, V> cache; private final Object lock = new Object(); public SynchronizedLRUCache(int capacity) { this.capacity = capacity; this.cache = new LinkedHashMap<K, V>(capacity, 0.75f, true) { @Override protected boolean removeEldestEntry(Map.Entry<K, V> eldest) { return size() > capacity; } }; } public V get(K key) { synchronized(lock) { return cache.get(key); } } public void put(K key, V value) { synchronized(lock) { cache.put(key, value); } } public int size() { synchronized(lock) { return cache.size(); } } }设计要点:
- 使用单独的final对象作为锁,避免意外暴露锁对象
- 重写LinkedHashMap的removeEldestEntry实现LRU淘汰
- 所有访问缓存的方法都使用相同的锁对象
- 访问顺序设置为true,使LinkedHashMap维护访问顺序
在实际性能测试中,当并发量超过5000QPS时,这种实现方式会显现出明显的竞争瓶颈。此时可以考虑改用ConcurrentHashMap配合分段锁,或者直接使用Guava Cache等成熟解决方案。但作为synchronized的教学示例,这个实现已经足够展示其核心用法。