文章目录
- 引言
- synchronized的基本使用
- 同步方法
- 同步代码块
- synchronized的底层原理
- 字节码层面分析
- 对象头与Mark Word
- 锁升级优化过程
- 1. 偏向锁(Biased Locking)
- 2. 轻量级锁(Lightweight Locking)
- 3. 重量级锁(Heavyweight Locking)
- 实战中的最佳实践
- 1. 锁粒度控制
- 2. 避免死锁
- 3. 双检锁单例模式
- 性能优化建议
- 1. 减少锁持有时间
- 2. 使用读写锁替代
- 常见问题与解决方案
- 1. synchronized与Lock的区别
- 2. 如何选择锁策略
- 总结
引言
线程安全是我们必须面对的核心挑战之一。Java为我们提供了synchronized关键字。
synchronized的基本使用
同步方法
同步方法是最简单的使用方式,直接在方法声明中添加synchronized关键字即可:
publicclassCounter{privateintcount=0;// 同步实例方法publicsynchronizedvoidincrement(){count++;}// 同步静态方法publicstaticsynchronizedvoidstaticIncrement(){// 静态方法的锁是类的Class对象}}代码说明:
- 实例方法的锁是当前对象实例(this)
- 静态方法的锁是当前类的Class对象
- 同步方法保证了同一时间只有一个线程能执行该方法
同步代码块
同步代码块提供了更细粒度的控制,可以指定锁对象:
publicclassOrderService{privatefinalObjectlock=newObject();privateMap<String,Integer>inventory=newHashMap<>();publicvoidprocessOrder(StringproductId){// 非同步代码,可以并发执行System.out.println("开始处理订单...");synchronized(lock){// 同步代码块,保证库存操作的原子性Integerstock=inventory.get(productId);if(stock!=null&&stock>0){inventory.put(productId,stock-1);System.out.println("扣减库存成功");}}// 后续非同步操作System.out.println("订单处理完成");}}代码说明:
- 可以指定任意对象作为锁
- 锁的范围更小,性能更好
- 提供了更灵活的同步控制
synchronized的底层原理
字节码层面分析
让我们通过反编译来看看synchronized在字节码层面是如何实现的:
publicclassSynchronizedDemo{privatestaticintcounter=0;privatefinalObjectlock=newObject();publicvoidsyncMethod(){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)
publicclassBiasedLockExample{privatestaticfinalObjectlock=newObject();privatestaticintcount=0;publicstaticvoidmain(String[]args)throwsInterruptedException{// 默认情况下,JVM会延迟开启偏向锁Thread.sleep(5000);// 等待偏向锁开启synchronized(lock){count++;System.out.println("第一次获取锁,应该是偏向锁");}}}偏向锁特点:
- 适用于只有一个线程访问同步块的场景
- 在对象头中记录线程ID
- 同一个线程再次获取锁时不需要CAS操作
2. 轻量级锁(Lightweight Locking)
当有第二个线程尝试获取锁时,偏向锁会升级为轻量级锁:
publicclassLightweightLockExample{privatestaticfinalObjectlock=newObject();publicstaticvoidmain(String[]args){// 线程1newThread(()->{synchronized(lock){try{Thread.sleep(100);// 短暂持有锁}catch(InterruptedExceptione){e.printStackTrace();}}}).start();// 线程2 - 会触发锁升级newThread(()->{try{Thread.sleep(10);// 确保线程1先获取锁}catch(InterruptedExceptione){e.printStackTrace();}synchronized(lock){System.out.println("线程2获取锁,此时应该是轻量级锁");}}).start();}}轻量级锁特点:
- 使用CAS操作替代操作系统互斥量
- 适用于线程交替执行的场景
- 自旋等待避免线程切换开销
3. 重量级锁(Heavyweight Locking)
当竞争激烈时,轻量级锁会升级为重量级锁:
publicclassHeavyweightLockExample{privatestaticfinalObjectlock=newObject();privatestaticfinalintTHREAD_COUNT=10;publicstaticvoidmain(String[]args){CountDownLatchlatch=newCountDownLatch(THREAD_COUNT);for(inti=0;i<THREAD_COUNT;i++){newThread(()->{synchronized(lock){try{// 模拟业务处理Thread.sleep(50);}catch(InterruptedExceptione){e.printStackTrace();}}latch.countDown();}).start();}try{latch.await();System.out.println("所有线程执行完成,经历了锁升级过程");}catch(InterruptedExceptione){e.printStackTrace();}}}重量级锁特点:
- 使用操作系统的互斥量(Mutex)
- 线程会进入阻塞状态
- 适用于高竞争场景
实战中的最佳实践
1. 锁粒度控制
在商城项目中,库存管理需要特别注意锁的粒度:
publicclassInventoryManager{// 不好的做法:锁粒度太粗privatefinalObjectglobalLock=newObject();privateMap<String,Integer>inventory=newConcurrentHashMap<>();// 好的做法:细粒度锁privatefinalMap<String,Object>productLocks=newConcurrentHashMap<>();publicvoidupdateStock(StringproductId,intquantity){// 获取商品特定的锁ObjectproductLock=productLocks.computeIfAbsent(productId,k->newObject());synchronized(productLock){IntegercurrentStock=inventory.getOrDefault(productId,0);inventory.put(productId,currentStock+quantity);}}publicbooleanpurchase(StringproductId,intquantity){ObjectproductLock=productLocks.computeIfAbsent(productId,k->newObject());synchronized(productLock){IntegercurrentStock=inventory.get(productId);if(currentStock==null||currentStock<quantity){returnfalse;}inventory.put(productId,currentStock-quantity);returntrue;}}}2. 避免死锁
在营销系统的奖品发放中,要特别注意避免死锁:
publicclassPrizeDistribution{privatefinalObjectprizeLock=newObject();privatefinalObjectuserLock=newObject();// 错误的做法:可能产生死锁publicvoiddistributePrizeWrong(longuserId,StringprizeId){synchronized(prizeLock){synchronized(userLock){// 处理奖品发放}}}// 正确的做法:固定锁顺序publicvoiddistributePrizeRight(longuserId,StringprizeId){// 按照固定顺序获取锁ObjectfirstLock,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. 双检锁单例模式
在项目配置管理中,单例模式经常使用:
publicclassConfigManager{// volatile保证可见性和禁止指令重排序privatestaticvolatileConfigManagerinstance;privateConfigManager(){// 私有构造函数}publicstaticConfigManagergetInstance(){if(instance==null){// 第一次检查synchronized(ConfigManager.class){if(instance==null){// 第二次检查instance=newConfigManager();}}}returninstance;}}为什么需要volatile:
- 防止指令重排序
- 保证多线程环境下的可见性
- 避免其他线程看到未完全初始化的对象
性能优化建议
1. 减少锁持有时间
publicclassOptimizedOrderProcessor{privateMap<String,BigDecimal>prices=newHashMap<>();privateMap<String,Integer>stock=newHashMap<>();// 优化前:锁持有时间过长publicBigDecimalcalculateTotalBad(List<String>products){synchronized(this){BigDecimaltotal=BigDecimal.ZERO;for(Stringproduct:products){// 模拟耗时操作try{Thread.sleep(10);}catch(InterruptedExceptione){e.printStackTrace();}total=total.add(prices.getOrDefault(product,BigDecimal.ZERO));}returntotal;}}// 优化后:只锁必要的部分publicBigDecimalcalculateTotalGood(List<String>products){// 先收集需要的数据(不需要同步)List<BigDecimal>priceList=newArrayList<>();for(Stringproduct:products){// 模拟耗时操作try{Thread.sleep(10);}catch(InterruptedExceptione){e.printStackTrace();}}// 同步计算总和synchronized(this){BigDecimaltotal=BigDecimal.ZERO;for(Stringproduct:products){total=total.add(prices.getOrDefault(product,BigDecimal.ZERO));}returntotal;}}}2. 使用读写锁替代
对于读多写少的场景,考虑使用ReentrantReadWriteLock:
publicclassProductCache{privatefinalMap<String,Product>cache=newHashMap<>();privatefinalReentrantReadWriteLockrwLock=newReentrantReadWriteLock();publicProductgetProduct(Stringid){rwLock.readLock().lock();// 获取读锁try{returncache.get(id);}finally{rwLock.readLock().unlock();}}publicvoidupdateProduct(Productproduct){rwLock.writeLock().lock();// 获取写锁try{cache.put(product.getId(),product);}finally{rwLock.writeLock().unlock();}}}常见问题与解决方案
1. synchronized与Lock的区别
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 实现机制 | JVM层面实现 | JDK层面实现 |
| 锁获取 | 自动获取释放 | 手动获取释放 |
| 可中断 | 不支持 | 支持 |
| 公平锁 | 非公平 | 可选公平/非公平 |
| 条件变量 | 有限支持 | 灵活支持 |
2. 如何选择锁策略
根据实际场景选择合适的同步机制:
publicclassLockStrategySelector{/** * 根据场景选择锁策略 * @param scenario 场景描述 * @return 建议的锁策略 */publicStringselectLockStrategy(Stringscenario){switch(scenario){case"简单同步":return"使用synchronized,简单可靠";case"需要超时":return"使用ReentrantLock.tryLock()";case"读写分离":return"使用ReentrantReadWriteLock";case"高并发统计":return"考虑使用LongAdder";case"分布式环境":return"使用分布式锁如Redis锁";default:return"使用synchronized";}}}总结
synchronizedJava内置的同步机制,从最初的重量级锁发展到现在的智能锁升级,性能已经得到了极大的优化。在实际项目中,需要根据具体场景选择合适的同步策略:对于简单的同步需求,synchronized是选择;对于复杂的并发控制,可以考虑ReentrantLock等更灵活的机制。