面试官:“synchronized修饰静态方法和非静态方法有什么区别?” 候选人:“呃…都是加锁吧…” 面试官:“好的,今天的面试到此结束。”
这个看似简单的问题,却让无数Java程序员在面试中折戟沉沙。今天,我将为你彻底揭开synchronized的"双重人格"之谜!
一、一个致命的多线程Bug
先来看一个价值百万的线上事故场景:
public class OrderService{private static int orderCount=0;// 静态变量 private int instanceOrderCount=0;// 实例变量 // 静态方法 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 count=0;// 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 obj1=new InstanceLockExample();InstanceLockExample obj2=new InstanceLockExample();// 线程1访问obj1 Thread t1=new Thread(()->{obj1.instanceMethod();},"线程1-obj1");// 线程2访问obj1(相同实例) Thread t2=new Thread(()->{obj1.instanceMethod();},"线程2-obj1");// 线程3访问obj2(不同实例) Thread t3=new 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 obj1=new StaticLockExample();StaticLockExample obj2=new StaticLockExample();// 线程1通过obj1调用静态方法 Thread t1=new Thread(()->{StaticLockExample.staticMethod();// 通过类名调用},"线程1-类名调用");// 线程2通过obj1调用静态方法 Thread t2=new Thread(()->{obj1.staticMethod();// 通过实例调用(不推荐)},"线程2-实例1调用");// 线程3通过obj2调用静态方法 Thread t3=new Thread(()->{obj2.staticMethod();// 通过另一个实例调用},"线程3-实例2调用");t1.start();t2.start();t3.start();// 输出结果: // 所有线程都会互斥等待!因为锁的是同一个Class对象 // 无论通过类名还是实例调用静态同步方法,锁都是类级别的}}四、最危险的组合:静态与非静态方法同时使用
这是最容易出问题的场景,看这个经典的"死锁陷阱":
public class BankAccount{private static double interestRate=0.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();}interestRate=newRate;}// 实例同步方法:计算利息 public synchronized voidcalculateInterest(){System.out.println(Thread.currentThread().getName()+" 开始计算利息,当前利率: "+ interestRate);try{Thread.sleep(1000);}catch(InterruptedException e){e.printStackTrace();}// 读取静态变量 double interest=balance * interestRate;}// 混合方法:先调用静态方法,再调用实例方法 public synchronized voidcomplexOperation(){System.out.println(Thread.currentThread().getName()+" 开始复杂操作");// 危险!在实例方法中调用静态同步方法 updateInterestRate(0.035);// 然后访问实例变量 balance+=1000;}}这里的陷阱是:静态方法锁(类锁)和实例方法锁(对象锁)是不同的锁,它们之间不会互斥!这可能导致数据不一致。
五、真实案例:电商系统的并发灾难
让我分享一个真实的线上事故:
// ❌ 错误实现:库存管理类 public class InventoryManager{private static Map<String, Integer>inventory=new HashMap<>();// 静态库存 private int instanceCount=0;// 实例计数器 // 静态同步方法:减少库存 public static synchronized void decreaseStatic(String productId, int quantity){Integer stock=inventory.get(productId);if(stock!=null&&stock>=quantity){// 模拟数据库操作耗时 try{Thread.sleep(100);}catch(InterruptedException e){}inventory.put(productId, stock - quantity);}}// 实例同步方法:批量减少库存 public synchronized void decreaseBatch(List<String>products){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 manager1=new InventoryManager();InventoryManager manager2=new InventoryManager();// 线程1:通过manager1减少库存 Thread t1=new Thread(()->{ manager1.decreaseBatch(Arrays.asList("product1","product2"));});// 线程2:通过manager2检查库存 Thread t2=new Thread(()->{// 这里可以和t1并行执行! // 因为t1持有了manager1的对象锁,但这里用的是manager2的对象锁 // 而静态方法锁是类锁,与对象锁不同 boolean available=manager2.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 instanceCounter=0;public synchronized voidincrementInstance(){instanceCounter++;}// 等价写法 public voidincrementInstance2(){synchronized(this){instanceCounter++;}}// 场景2:只操作静态变量 ->用类锁 private static int staticCounter=0;public static synchronized voidincrementStatic(){staticCounter++;}// 等价写法 public static voidincrementStatic2(){synchronized(CorrectLockUsage.class){staticCounter++;}}// 场景3:操作多个资源 ->使用细粒度锁 private final Object lock1=new Object();private final Object lock2=new Object();private int resource1=0;private int resource2=0;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 globalLock=new 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 atomicCounter=new AtomicInteger(0);public voidincrementAtomic(){atomicCounter.incrementAndGet();// 线程安全,无锁}//2. 使用ReentrantLock(更灵活) private final ReentrantLock lock=new ReentrantLock();private int counter=0;public voidincrementWithLock(){lock.lock();try{counter++;}finally{lock.unlock();}}//3. 使用ReadWriteLock(读写分离) private final ReadWriteLock rwLock=new ReentrantReadWriteLock();private Map<String, String>cache=new 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 instance=new 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 count=0;public synchronized voidincrement(){count++;}}static class StaticLock{private static int count=0;public static synchronized voidincrement(){count++;}}static class AtomicCounter{private static AtomicInteger count=new 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 lock=new Object();public voidmethod(){synchronized(lock){/*... */}}//3. 锁范围尽量小 public voidminimizeLockScope(){// 非同步操作 int temp=compute();// 同步块尽量小 synchronized(this){update(temp);}}}// ❌ 危险模式 public class BadPatterns{//1. 锁住public对象 public Object publicLock=new 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()的实现,看看它使用了哪种锁?为什么这样设计?把你的发现写在评论区吧!