synchronized的“双重人格“:静态与非静态方法锁的惊天差异

synchronized的“双重人格“:静态与非静态方法锁的惊天差异 面试官“synchronized修饰静态方法和非静态方法有什么区别” 候选人“呃…都是加锁吧…” 面试官“好的今天的面试到此结束。”这个看似简单的问题却让无数Java程序员在面试中折戟沉沙。今天我将为你彻底揭开synchronized的双重人格之谜一、一个致命的多线程Bug先来看一个价值百万的线上事故场景public class OrderService{private static int orderCount0;// 静态变量 private int instanceOrderCount0;// 实例变量 // 静态方法 public static synchronized voidcreateStaticOrder(){orderCount;System.out.println(Thread.currentThread().getName() 创建订单当前订单数: orderCount);}// 实例方法 public synchronized voidcreateInstanceOrder(){instanceOrderCount;System.out.println(Thread.currentThread().getName() 创建订单实例订单数: instanceOrderCount);}}这段代码看似完美实则隐藏着致命陷阱。让我们一起探索它的秘密。二、核心差异锁的对象完全不同1. 静态方法锁类级别的全局锁public class StaticLockExample{// synchronized修饰静态方法 public static synchronized voidstaticMethod(){System.out.println(进入静态同步方法锁住的是: StaticLockExample.class);try{Thread.sleep(2000);}catch(InterruptedException e){e.printStackTrace();}}// 等价写法更清晰 public static voidstaticMethod2(){// 注意锁的是类对象 synchronized(StaticLockExample.class){System.out.println(同步代码块锁住的是: StaticLockExample.class);}}}关键点锁对象类的Class对象StaticLockExample.class影响范围所有实例的该方法调用都会互斥类比公司的CEO办公室大门所有人实例共用一把锁2. 实例方法锁对象级别的个人锁public class InstanceLockExample{private int count0;// synchronized修饰实例方法 public synchronized voidinstanceMethod(){count;System.out.println(Thread.currentThread().getName() 进入实例同步方法当前对象: this , count: count);try{Thread.sleep(1000);}catch(InterruptedException e){e.printStackTrace();}}// 等价写法 public voidinstanceMethod2(){// 注意锁的是当前实例对象 synchronized(this){count;System.out.println(同步代码块锁住的是: this);}}}关键点锁对象当前实例对象this影响范围同一个实例的方法调用会互斥不同实例互不影响类比每个员工的个人储物柜每人有自己的锁三、实战演示差异对比场景1多实例访问实例方法public class MultiInstanceTest{public static void main(String[]args){// 创建两个不同实例 InstanceLockExample obj1new InstanceLockExample();InstanceLockExample obj2new InstanceLockExample();// 线程1访问obj1 Thread t1new Thread(()-{obj1.instanceMethod();},线程1-obj1);// 线程2访问obj1相同实例 Thread t2new Thread(()-{obj1.instanceMethod();},线程2-obj1);// 线程3访问obj2不同实例 Thread t3new Thread(()-{obj2.instanceMethod();},线程3-obj2);t1.start();t2.start();t3.start();// 输出结果 // 线程1-obj1 和 线程2-obj1 会互斥等待 // 线程3-obj2 会并行执行因为锁的是不同实例}}场景2多线程访问静态方法public class MultiThreadStaticTest{public static void main(String[]args){// 创建多个实例 StaticLockExample obj1new StaticLockExample();StaticLockExample obj2new StaticLockExample();// 线程1通过obj1调用静态方法 Thread t1new Thread(()-{StaticLockExample.staticMethod();// 通过类名调用},线程1-类名调用);// 线程2通过obj1调用静态方法 Thread t2new Thread(()-{obj1.staticMethod();// 通过实例调用不推荐},线程2-实例1调用);// 线程3通过obj2调用静态方法 Thread t3new Thread(()-{obj2.staticMethod();// 通过另一个实例调用},线程3-实例2调用);t1.start();t2.start();t3.start();// 输出结果 // 所有线程都会互斥等待因为锁的是同一个Class对象 // 无论通过类名还是实例调用静态同步方法锁都是类级别的}}四、最危险的组合静态与非静态方法同时使用这是最容易出问题的场景看这个经典的死锁陷阱public class BankAccount{private static double interestRate0.03;// 静态变量利率 private double balance;// 实例变量余额 // 静态同步方法修改利率 public static synchronized void updateInterestRate(double newRate){System.out.println(Thread.currentThread().getName() 开始修改利率: interestRate - newRate);try{Thread.sleep(1000);// 模拟耗时}catch(InterruptedException e){e.printStackTrace();}interestRatenewRate;}// 实例同步方法计算利息 public synchronized voidcalculateInterest(){System.out.println(Thread.currentThread().getName() 开始计算利息当前利率: interestRate);try{Thread.sleep(1000);}catch(InterruptedException e){e.printStackTrace();}// 读取静态变量 double interestbalance * interestRate;}// 混合方法先调用静态方法再调用实例方法 public synchronized voidcomplexOperation(){System.out.println(Thread.currentThread().getName() 开始复杂操作);// 危险在实例方法中调用静态同步方法 updateInterestRate(0.035);// 然后访问实例变量 balance1000;}}这里的陷阱是静态方法锁类锁和实例方法锁对象锁是不同的锁它们之间不会互斥这可能导致数据不一致。五、真实案例电商系统的并发灾难让我分享一个真实的线上事故// ❌ 错误实现库存管理类 public class InventoryManager{private static MapString, Integerinventorynew HashMap();// 静态库存 private int instanceCount0;// 实例计数器 // 静态同步方法减少库存 public static synchronized void decreaseStatic(String productId, int quantity){Integer stockinventory.get(productId);if(stock!nullstockquantity){// 模拟数据库操作耗时 try{Thread.sleep(100);}catch(InterruptedException e){}inventory.put(productId, stock - quantity);}}// 实例同步方法批量减少库存 public synchronized void decreaseBatch(ListStringproducts){instanceCount;for(String productId:products){// 这里调用了静态同步方法 decreaseStatic(productId,1);}}// 另一个实例同步方法检查库存 public synchronized boolean checkStock(String productId){// 读取静态变量returninventory.getOrDefault(productId,0)0;}}// 测试代码 public class DisasterTest{public static void main(String[]args)throws InterruptedException{InventoryManager manager1new InventoryManager();InventoryManager manager2new InventoryManager();// 线程1通过manager1减少库存 Thread t1new Thread(()-{ manager1.decreaseBatch(Arrays.asList(product1,product2));});// 线程2通过manager2检查库存 Thread t2new Thread(()-{// 这里可以和t1并行执行 // 因为t1持有了manager1的对象锁但这里用的是manager2的对象锁 // 而静态方法锁是类锁与对象锁不同 boolean availablemanager2.checkStock(product1);System.out.println(检查结果: available);});t1.start();t2.start();// 结果可能出现脏读 // t2可能在t1修改库存的过程中读取到中间状态}}事故原因线程t1持有manager1的对象锁线程t2持有manager2的对象锁它们不会互斥。但checkStock()方法读取的是静态变量可能在decreaseStatic()执行过程中被读取导致数据不一致。六、正确姿势如何选择正确的锁原则1保护什么数据就用什么锁public class CorrectLockUsage{// 场景1只操作实例变量 -用实例锁 private int instanceCounter0;public synchronized voidincrementInstance(){instanceCounter;}// 等价写法 public voidincrementInstance2(){synchronized(this){instanceCounter;}}// 场景2只操作静态变量 -用类锁 private static int staticCounter0;public static synchronized voidincrementStatic(){staticCounter;}// 等价写法 public static voidincrementStatic2(){synchronized(CorrectLockUsage.class){staticCounter;}}// 场景3操作多个资源 -使用细粒度锁 private final Object lock1new Object();private final Object lock2new Object();private int resource10;private int resource20;public voidupdateBoth(){// 分别加锁提高并发度 synchronized(lock1){resource1;}synchronized(lock2){resource2;}}}原则2避免锁嵌套防止死锁public class LockNestingSolution{// ❌ 错误可能导致死锁 public synchronized voidmethod1(){// 持有了this锁 StaticClass.staticSyncMethod();// 尝试获取类锁}public static synchronized voidstaticSyncMethod(){// 持有了类锁 // 如果另一个线程以相反顺序获取锁就会死锁}// ✅ 正确使用统一的锁顺序 private static final Object globalLocknew Object();public voidsafeMethod1(){synchronized(globalLock){// 业务逻辑}}public static voidsafeStaticMethod(){synchronized(globalLock){// 业务逻辑}}}原则3考虑使用更高级的并发工具importjava.util.concurrent.atomic.*;importjava.util.concurrent.locks.*;public class AdvancedConcurrency{//1. 使用Atomic类无锁 private AtomicInteger atomicCounternew AtomicInteger(0);public voidincrementAtomic(){atomicCounter.incrementAndGet();// 线程安全无锁}//2. 使用ReentrantLock更灵活 private final ReentrantLock locknew ReentrantLock();private int counter0;public voidincrementWithLock(){lock.lock();try{counter;}finally{lock.unlock();}}//3. 使用ReadWriteLock读写分离 private final ReadWriteLock rwLocknew ReentrantReadWriteLock();private MapString, Stringcachenew HashMap();public String get(String key){rwLock.readLock().lock();// 读锁可并发 try{returncache.get(key);}finally{rwLock.readLock().unlock();}}public void put(String key, String value){rwLock.writeLock().lock();// 写锁独占 try{cache.put(key, value);}finally{rwLock.writeLock().unlock();}}}七、性能对比测试让我们通过基准测试看看不同锁的性能差异BenchmarkMode(Mode.Throughput)// 吞吐量测试 OutputTimeUnit(TimeUnit.MILLISECONDS)public class LockPerformanceBenchmark{// 测试实例锁 Benchmark Threads(4)//4个线程 public void testInstanceLock(Blackhole bh){InstanceLock instancenew InstanceLock();instance.increment();bh.consume(instance);}// 测试静态锁 Benchmark Threads(4)public void testStaticLock(Blackhole bh){StaticLock.increment();}// 测试无锁Atomic Benchmark Threads(4)public void testAtomic(Blackhole bh){AtomicCounter.increment();}static class InstanceLock{private int count0;public synchronized voidincrement(){count;}}static class StaticLock{private static int count0;public static synchronized voidincrement(){count;}}static class AtomicCounter{private static AtomicInteger countnew AtomicInteger(0);public static voidincrement(){count.incrementAndGet();}}}测试结果分析低并发场景三者性能差异不大高并发场景八、最佳实践总结1. 选择锁的黄金法则public class LockSelectionRules{/** * 问自己三个问题 *1. 要保护什么数据 *2. 这个数据是实例级别还是类级别 *3. 并发访问的竞争程度如何 */ // 规则1实例数据用实例锁 private Object instanceData;public synchronized voidupdateInstanceData(){/*... */}// 规则2静态数据用静态锁 private static Object staticData;public static synchronized voidupdateStaticData(){/*... */}// 规则3混合数据要小心 private static Object sharedResource;private Object instanceResource;public voidupdateMixed(){// 危险需要更精细的控制 synchronized(LockSelectionRules.class){// 先获取类锁 // 修改静态数据}synchronized(this){// 再获取实例锁 // 修改实例数据}// 注意要避免死锁保持一致的锁获取顺序}}2. 代码审查清单在代码审查时检查以下synchronized使用情况// ✅ 良好模式 public class GoodPatterns{//1. 保护私有字段 private int count;public synchronized intgetCount(){returncount;}//2. 使用private final对象作为锁 private final Object locknew Object();public voidmethod(){synchronized(lock){/*... */}}//3. 锁范围尽量小 public voidminimizeLockScope(){// 非同步操作 int tempcompute();// 同步块尽量小 synchronized(this){update(temp);}}}// ❌ 危险模式 public class BadPatterns{//1. 锁住public对象 public Object publicLocknew Object();// 危险 //2. 在构造函数中同步 publicBadPatterns(){synchronized(this){// 危险对象尚未完全构造 //...}}//3. 锁住Class对象来保护实例数据 public voidupdateInstanceData(){synchronized(BadPatterns.class){// 错误过度同步 instanceData;// 实例数据}}}九、终极面试攻略面试官可能问的问题“synchronized修饰静态方法和实例方法有什么区别”答锁对象不同。静态方法锁的是Class对象是类级别的锁实例方法锁的是当前实例对象是对象级别的锁。“它们会发生死锁吗”答可能会。如果一个线程持有实例锁后尝试获取类锁另一个线程持有类锁后尝试获取实例锁就会发生死锁。“如何选择使用哪种锁”答根据保护的数据类型决定。保护实例数据用实例锁保护静态数据用静态锁。遵循最小化锁范围原则。“性能上有什么区别”答静态锁的竞争更激烈性能通常更差。实例锁的粒度更小并发度更高。在高并发场景下考虑使用无锁编程或其他并发工具。加分回答“实际上在Java 6之后synchronized经过了大量优化如偏向锁、轻量级锁、锁消除、锁粗化等。在大多数场景下synchronized的性能已经足够好。但理解其原理仍然是写出高性能并发代码的基础。”总结synchronized的双重人格不是缺陷而是精妙的设计。理解它们的差异就像掌握了一把开启高性能并发编程大门的钥匙静态方法锁类级别的全局卫士守护着类的静态数据实例方法锁对象级别的私人保镖保护着每个实例的内部状态记住正确的锁用在正确的数据上这是写出线程安全代码的第一原则。#Java并发 #多线程 #synchronized #面试技巧 #性能优化最后的小练习尝试分析JDK中Collections.synchronizedList()的实现看看它使用了哪种锁为什么这样设计把你的发现写在评论区吧