面试官:"synchronized修饰静态方法和非静态方法有什么区别?" 候选人:"呃...都是加锁吧..." 面试官:"好的,今天的面试到此结束。"
这个看似简单的问题,却让无数Java程序员在面试中折戟沉沙。今天,我将为你彻底揭开synchronized的"双重人格"之谜!
一、一个致命的多线程Bug
先来看一个价值百万的线上事故场景:
public class OrderService {
private static int orderCount = 0; // 静态变量
private int instanceOrderCount = 0; // 实例变量
// 静态方法
public static synchronized void createStaticOrder() {
orderCount++;
System.out.println(Thread.currentThread().getName()
+ " 创建订单,当前订单数: " + orderCount);
}
// 实例方法
public synchronized void createInstanceOrder() {
instanceOrderCount++;
System.out.println(Thread.currentThread().getName()
+ " 创建订单,实例订单数: " + instanceOrderCount);
}
}
这段代码看似完美,实则隐藏着致命陷阱。让我们一起探索它的秘密。
二、核心差异:锁的对象完全不同
1. 静态方法锁:类级别的"全局锁"
public class StaticLockExample {
// synchronized修饰静态方法
public static synchronized void staticMethod() {
System.out.println("进入静态同步方法,锁住的是: " +
StaticLockExample.class);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 等价写法(更清晰)
public static void staticMethod2() {
// 注意:锁的是类对象!
synchronized (StaticLockExample.class) {
System.out.println("同步代码块,锁住的是: " +
StaticLockExample.class);
}
}
}
关键点:
-
锁对象:类的Class对象(
StaticLockExample.class) -
影响范围:所有实例的该方法调用都会互斥
-
类比:公司的CEO办公室大门,所有人(实例)共用一把锁
2. 实例方法锁:对象级别的"个人锁"
public class InstanceLockExample {
private int count = 0;
// synchronized修饰实例方法
public synchronized void instanceMethod() {
count++;
System.out.println(Thread.currentThread().getName()
+ " 进入实例同步方法,当前对象: " + this
+ ", count: " + count);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 等价写法
public void instanceMethod2() {
// 注意:锁的是当前实例对象!
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 void calculateInterest() {
System.out.println(Thread.currentThread().getName()
+ " 开始计算利息,当前利率: " + interestRate);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 读取静态变量
double interest = balance * interestRate;
}
// 混合方法:先调用静态方法,再调用实例方法
public synchronized void complexOperation() {
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) {
// 读取静态变量
return inventory.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 void incrementInstance() {
instanceCounter++;
}
// 等价写法
public void incrementInstance2() {
synchronized (this) {
instanceCounter++;
}
}
// 场景2:只操作静态变量 -> 用类锁
private static int staticCounter = 0;
public static synchronized void incrementStatic() {
staticCounter++;
}
// 等价写法
public static void incrementStatic2() {
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 void updateBoth() {
// 分别加锁,提高并发度
synchronized (lock1) {
resource1++;
}
synchronized (lock2) {
resource2++;
}
}
}
原则2:避免锁嵌套,防止死锁
public class LockNestingSolution {
// ❌ 错误:可能导致死锁
public synchronized void method1() {
// 持有了this锁
StaticClass.staticSyncMethod(); // 尝试获取类锁
}
public static synchronized void staticSyncMethod() {
// 持有了类锁
// 如果另一个线程以相反顺序获取锁,就会死锁
}
// ✅ 正确:使用统一的锁顺序
private static final Object globalLock = new Object();
public void safeMethod1() {
synchronized (globalLock) {
// 业务逻辑
}
}
public static void safeStaticMethod() {
synchronized (globalLock) {
// 业务逻辑
}
}
}
原则3:考虑使用更高级的并发工具
import java.util.concurrent.atomic.*;
import java.util.concurrent.locks.*;
public class AdvancedConcurrency {
// 1. 使用Atomic类(无锁)
private AtomicInteger atomicCounter = new AtomicInteger(0);
public void incrementAtomic() {
atomicCounter.incrementAndGet(); // 线程安全,无锁
}
// 2. 使用ReentrantLock(更灵活)
private final ReentrantLock lock = new ReentrantLock();
private int counter = 0;
public void incrementWithLock() {
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 {
return cache.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 void increment() {
count++;
}
}
static class StaticLock {
private static int count = 0;
public static synchronized void increment() {
count++;
}
}
static class AtomicCounter {
private static AtomicInteger count = new AtomicInteger(0);
public static void increment() {
count.incrementAndGet();
}
}
}
测试结果分析:
-
低并发场景:三者性能差异不大
-
高并发场景:
八、最佳实践总结
1. 选择锁的黄金法则
public class LockSelectionRules {
/**
* 问自己三个问题:
* 1. 要保护什么数据?
* 2. 这个数据是实例级别还是类级别?
* 3. 并发访问的竞争程度如何?
*/
// 规则1:实例数据用实例锁
private Object instanceData;
public synchronized void updateInstanceData() { /* ... */ }
// 规则2:静态数据用静态锁
private static Object staticData;
public static synchronized void updateStaticData() { /* ... */ }
// 规则3:混合数据要小心
private static Object sharedResource;
private Object instanceResource;
public void updateMixed() {
// 危险!需要更精细的控制
synchronized (LockSelectionRules.class) { // 先获取类锁
// 修改静态数据
}
synchronized (this) { // 再获取实例锁
// 修改实例数据
}
// 注意:要避免死锁,保持一致的锁获取顺序
}
}
2. 代码审查清单
在代码审查时,检查以下synchronized使用情况:
// ✅ 良好模式
public class GoodPatterns {
// 1. 保护私有字段
private int count;
public synchronized int getCount() { return count; }
// 2. 使用private final对象作为锁
private final Object lock = new Object();
public void method() {
synchronized (lock) { /* ... */ }
}
// 3. 锁范围尽量小
public void minimizeLockScope() {
// 非同步操作
int temp = compute();
// 同步块尽量小
synchronized (this) {
update(temp);
}
}
}
// ❌ 危险模式
public class BadPatterns {
// 1. 锁住public对象
public Object publicLock = new Object(); // 危险!
// 2. 在构造函数中同步
public BadPatterns() {
synchronized (this) { // 危险!对象尚未完全构造
// ...
}
}
// 3. 锁住Class对象来保护实例数据
public void updateInstanceData() {
synchronized (BadPatterns.class) { // 错误!过度同步
instanceData++; // 实例数据
}
}
}
九、终极面试攻略
面试官可能问的问题:
-
"synchronized修饰静态方法和实例方法有什么区别?"
-
答:锁对象不同。静态方法锁的是Class对象,是类级别的锁;实例方法锁的是当前实例对象,是对象级别的锁。
-
"它们会发生死锁吗?"
- 答:可能会。如果一个线程持有实例锁后尝试获取类锁,另一个线程持有类锁后尝试获取实例锁,就会发生死锁。
- "如何选择使用哪种锁?"
- 答:根据保护的数据类型决定。保护实例数据用实例锁,保护静态数据用静态锁。遵循"最小化锁范围"原则。
- "性能上有什么区别?"
- 答:静态锁的竞争更激烈,性能通常更差。实例锁的粒度更小,并发度更高。在高并发场景下,考虑使用无锁编程或其他并发工具。
加分回答:
"实际上,在Java 6之后,synchronized经过了大量优化,如偏向锁、轻量级锁、锁消除、锁粗化等。在大多数场景下,synchronized的性能已经足够好。但理解其原理仍然是写出高性能并发代码的基础。"
总结
synchronized的"双重人格"不是缺陷,而是精妙的设计。理解它们的差异,就像掌握了一把开启高性能并发编程大门的钥匙:
-
静态方法锁:类级别的全局卫士,守护着类的静态数据
-
实例方法锁:对象级别的私人保镖,保护着每个实例的内部状态
记住:正确的锁用在正确的数据上,这是写出线程安全代码的第一原则。
#Java并发 #多线程 #synchronized #面试技巧 #性能优化
最后的小练习:尝试分析JDK中Collections.synchronizedList()的实现,看看它使用了哪种锁?为什么这样设计?把你的发现写在评论区吧!