引言
在多线程编程的世界里,线程安全是我们必须面对的核心挑战之一。想象一下,在电商商城的秒杀场景中,库存的扣减如果处理不当,很可能导致超卖问题;在营销抽奖系统中,奖品的发放如果没有正确的同步机制,可能会让幸运儿变成"倒霉蛋"——重复中奖。Java为我们提供了synchronized关键字这个强大的工具来解决这些问题。今天,我就结合自己的项目经验,带大家深入理解synchronized的工作原理、使用技巧和性能优化,让你在开发中能更自信地处理并发问题。
synchronized的基本使用
同步方法
同步方法是最简单的使用方式,直接在方法声明中添加synchronized关键字即可:
public class Counter { private int count = 0; // 同步实例方法 public synchronized void increment() { count++; } // 同步静态方法 public static synchronized void staticIncrement() { // 静态方法的锁是类的Class对象 } }代码说明:
- 实例方法的锁是当前对象实例(this)
- 静态方法的锁是当前类的Class对象
- 同步方法保证了同一时间只有一个线程能执行该方法
同步代码块
同步代码块提供了更细粒度的控制,可以指定锁对象:
public class OrderService { private final Object lock = new Object(); private Map<String, Integer> inventory = new HashMap<>(); public void processOrder(String productId) { // 非同步代码,可以并发执行 System.out.println("开始处理订单..."); synchronized (lock) { // 同步代码块,保证库存操作的原子性 Integer stock = inventory.get(productId); if (stock != null && stock > 0) { inventory.put(productId, stock - 1); System.out.println("扣减库存成功"); } } // 后续非同步操作 System.out.println("订单处理完成"); } }代码说明:
- 可以指定任意对象作为锁
- 锁的范围更小,性能更好
- 提供了更灵活的同步控制
synchronized的底层原理
字节码层面分析
让我们通过反编译来看看synchronized在字节码层面是如何实现的:
public class SynchronizedDemo { private static int counter = 0; private final Object lock = new Object(); public void syncMethod() { synchronized (this) { counter++; } } }使用javap -c SynchronizedDemo.class反编译后,可以看到关键字节码:
public void syncMethod(); Code: 0: aload_0 1: dup 2: astore_1 3: monitorenter // 进入同步块 4: getstatic #2 // 获取counter 7: iconst_1 8: iadd 9: putstatic #2 // 设置counter 12: aload_1 13: monitorexit // 正常退出同步块 14: goto 22 17: astore_2 18: aload_1 19: monitorexit // 异常退出同步块 20: aload_2 21: athrow 22: return关键点解析:
monitorenter:获取对象的监视器锁monitorexit:释放对象的监视器锁- 编译器会自动生成异常处理,确保锁一定会被释放
对象头与Mark Word
在HotSpot虚拟机中,每个对象都有一个对象头,其中包含Mark Word,它记录了对象的锁状态信息:
| 锁状态 | 存储内容 | 标志位 | |------------|---------------------------------------------|------| | 无锁 | 对象哈希码、分代年龄 | 01 | | 偏向锁 | 线程ID、Epoch、分代年龄 | 01 | | 轻量级锁 | 指向栈中锁记录的指针 | 00 | | 重量级锁 | 指向互斥量(monitor)的指针 | 10 | | GC标记 | 空 | 11 |
锁升级优化过程
JDK 1.6之后,synchronized引入了锁升级机制来优化性能:
1. 偏向锁(Biased Locking)
public class BiasedLockExample { private static final Object lock = new Object(); private static int count = 0; public static void main(String[] args) throws InterruptedException { // 默认情况下,JVM会延迟开启偏向锁 Thread.sleep(5000); // 等待偏向锁开启 synchronized (lock) { count++; System.out.println("第一次获取锁,应该是偏向锁"); } } }偏向锁特点:
- 适用于只有一个线程访问同步块的场景
- 在对象头中记录线程ID
- 同一个线程再次获取锁时不需要CAS操作
2. 轻量级锁(Lightweight Locking)
当有第二个线程尝试获取锁时,偏向锁会升级为轻量级锁:
public class LightweightLockExample { private static final Object lock = new Object(); public static void main(String[] args) { // 线程1 new Thread(() -> { synchronized (lock) { try { Thread.sleep(100); // 短暂持有锁 } catch (InterruptedException e) { e.printStackTrace(); } } }).start(); // 线程2 - 会触发锁升级 new Thread(() -> { try { Thread.sleep(10); // 确保线程1先获取锁 } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock) { System.out.println("线程2获取锁,此时应该是轻量级锁"); } }).start(); } }轻量级锁特点:
- 使用CAS操作替代操作系统互斥量
- 适用于线程交替执行的场景
- 自旋等待避免线程切换开销
3. 重量级锁(Heavyweight Locking)
当竞争激烈时,轻量级锁会升级为重量级锁:
public class HeavyweightLockExample { private static final Object lock = new Object(); private static final int THREAD_COUNT = 10; public static void main(String[] args) { CountDownLatch latch = new CountDownLatch(THREAD_COUNT); for (int i = 0; i < THREAD_COUNT; i++) { new Thread(() -> { synchronized (lock) { try { // 模拟业务处理 Thread.sleep(50); } catch (InterruptedException e) { e.printStackTrace(); } } latch.countDown(); }).start(); } try { latch.await(); System.out.println("所有线程执行完成,经历了锁升级过程"); } catch (InterruptedException e) { e.printStackTrace(); } } }重量级锁特点:
- 使用操作系统的互斥量(Mutex)
- 线程会进入阻塞状态
- 适用于高竞争场景
实战中的最佳实践
1. 锁粒度控制
在商城项目中,库存管理需要特别注意锁的粒度:
public class InventoryManager { // 不好的做法:锁粒度太粗 private final Object globalLock = new Object(); private Map<String, Integer> inventory = new ConcurrentHashMap<>(); // 好的做法:细粒度锁 private final Map<String, Object> productLocks = new ConcurrentHashMap<>(); public void updateStock(String productId, int quantity) { // 获取商品特定的锁 Object productLock = productLocks.computeIfAbsent(productId, k -> new Object()); synchronized (productLock) { Integer currentStock = inventory.getOrDefault(productId, 0); inventory.put(productId, currentStock + quantity); } } public boolean purchase(String productId, int quantity) { Object productLock = productLocks.computeIfAbsent(productId, k -> new Object()); synchronized (productLock) { Integer currentStock = inventory.get(productId); if (currentStock == null || currentStock < quantity) { return false; } inventory.put(productId, currentStock - quantity); return true; } } }2. 避免死锁
在营销系统的奖品发放中,要特别注意避免死锁:
public class PrizeDistribution { private final Object prizeLock = new Object(); private final Object userLock = new Object(); // 错误的做法:可能产生死锁 public void distributePrizeWrong(long userId, String prizeId) { synchronized (prizeLock) { synchronized (userLock) { // 处理奖品发放 } } } // 正确的做法:固定锁顺序 public void distributePrizeRight(long userId, String prizeId) { // 按照固定顺序获取锁 Object firstLock, secondLock; if (System.identityHashCode(prizeLock) < System.identityHashCode(userLock)) { firstLock = prizeLock; secondLock = userLock; } else { firstLock = userLock; secondLock = prizeLock; } synchronized (firstLock) { synchronized (secondLock) { // 安全的奖品发放逻辑 System.out.println("为用户" + userId + "发放奖品" + prizeId); } } } }3. 双检锁单例模式
在项目配置管理中,单例模式经常使用:
public class ConfigManager { // volatile保证可见性和禁止指令重排序 private static volatile ConfigManager instance; private ConfigManager() { // 私有构造函数 } public static ConfigManager getInstance() { if (instance == null) { // 第一次检查 synchronized (ConfigManager.class) { if (instance == null) { // 第二次检查 instance = new ConfigManager(); } } } return instance; } }为什么需要volatile:
- 防止指令重排序
- 保证多线程环境下的可见性
- 避免其他线程看到未完全初始化的对象
性能优化建议
1. 减少锁持有时间
public class OptimizedOrderProcessor { private Map<String, BigDecimal> prices = new HashMap<>(); private Map<String, Integer> stock = new HashMap<>(); // 优化前:锁持有时间过长 public BigDecimal calculateTotalBad(List<String> products) { synchronized (this) { BigDecimal total = BigDecimal.ZERO; for (String product : products) { // 模拟耗时操作 try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } total = total.add(prices.getOrDefault(product, BigDecimal.ZERO)); } return total; } } // 优化后:只锁必要的部分 public BigDecimal calculateTotalGood(List<String> products) { // 先收集需要的数据(不需要同步) List<BigDecimal> priceList = new ArrayList<>(); for (String product : products) { // 模拟耗时操作 try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } } // 同步计算总和 synchronized (this) { BigDecimal total = BigDecimal.ZERO; for (String product : products) { total = total.add(prices.getOrDefault(product, BigDecimal.ZERO)); } return total; } } }2. 使用读写锁替代
对于读多写少的场景,考虑使用ReentrantReadWriteLock:
public class ProductCache { private final Map<String, Product> cache = new HashMap<>(); private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); public Product getProduct(String id) { rwLock.readLock().lock(); // 获取读锁 try { return cache.get(id); } finally { rwLock.readLock().unlock(); } } public void updateProduct(Product product) { rwLock.writeLock().lock(); // 获取写锁 try { cache.put(product.getId(), product); } finally { rwLock.writeLock().unlock(); } } }常见问题与解决方案
1. synchronized与Lock的区别
| 特性 | synchronized | ReentrantLock | |------|-------------|--------------| | 实现机制 | JVM层面实现 | JDK层面实现 | | 锁获取 | 自动获取释放 | 手动获取释放 | | 可中断 | 不支持 | 支持 | | 公平锁 | 非公平 | 可选公平/非公平 | | 条件变量 | 有限支持 | 灵活支持 |
2. 如何选择锁策略
根据实际场景选择合适的同步机制:
public class LockStrategySelector { /** * 根据场景选择锁策略 * @param scenario 场景描述 * @return 建议的锁策略 */ public String selectLockStrategy(String scenario) { switch (scenario) { case "简单同步": return "使用synchronized,简单可靠"; case "需要超时": return "使用ReentrantLock.tryLock()"; case "读写分离": return "使用ReentrantReadWriteLock"; case "高并发统计": return "考虑使用LongAdder"; case "分布式环境": return "使用分布式锁如Redis锁"; default: return "使用synchronized"; } } }总结
synchronized作为Java内置的同步机制,从最初的重量级锁发展到现在的智能锁升级,性能已经得到了极大的优化。在实际项目中,我们需要根据具体场景选择合适的同步策略:对于简单的同步需求,synchronized是最佳选择;对于复杂的并发控制,可以考虑ReentrantLock等更灵活的机制。记住,良好的并发设计不仅要保证线程安全,还要兼顾性能和可维护性。