synchronized 的 3 种用法详解(附代码实战)
以下是 Java 中 synchronized
的三种核心用法及详细示例,结合底层原理和实际场景解释其作用机制。
1. 修饰实例方法:锁住当前对象
代码示例:
java
public class Counter {
private int count = 0;
// 实例方法加锁
public synchronized void add() {
count++;
}
}
解释:
-
锁对象 :当前实例(
this
)。 -
作用范围 :同一实例的多个线程会竞争锁,不同实例的线程互不影响。例如,
Counter c1 = new Counter();
和Counter c2 = new Counter();
的add()
方法可被不同线程同时执行。 -
适用场景:实例变量需要线程安全操作的场景,如计数器、订单处理等。
底层原理 :
JVM 通过对象头中的 Mark Word
标记锁状态。当一个线程进入 add()
方法时,会获取当前对象的 Monitor 锁,其他线程必须等待锁释放。
2. 修饰静态方法:锁住类对象
代码示例:
java
public class GlobalCounter {
private static int count = 0;
// 静态方法加锁
public static synchronized void add() {
count++;
}
}
解释:
-
锁对象 :类的
Class
对象(如GlobalCounter.class
)。 -
作用范围 :所有实例共享同一把锁。例如,
GlobalCounter.add()
被线程 A 执行时,线程 B 无论通过哪个实例调用add()
都会被阻塞。 -
适用场景:静态变量或全局资源的同步操作,如全局配置管理、单例模式等。
底层原理 :
静态方法的锁对应类的元数据(Class
对象),Monitor 机制会阻止所有实例的并发访问。
3. 修饰代码块:灵活控制锁粒度
代码示例:
java
public class TransactionService {
private final Object lock = new Object(); // 自定义锁对象
private List<String> logs = new ArrayList<>();
public void logTransaction(String message) {
// 非同步操作(其他线程可并行执行)
System.out.println("Preparing log...");
// 同步代码块(锁定自定义对象)
synchronized (lock) {
logs.add(message);
}
}
}
解释:
-
锁对象 :可以是任意对象(
this
、类对象、自定义对象等)。 -
作用范围:
-
**
synchronized(this)
**:锁当前实例,效果等同于修饰实例方法。 -
**
synchronized(ClassName.class)
**:锁类对象,效果等同于修饰静态方法。 -
自定义对象锁:更细粒度控制,如只锁日志列表而非整个方法。
-
-
适用场景:需要减少锁粒度以提升性能的场景,如仅保护共享资源的部分操作。
底层原理 :
代码块锁通过 monitorenter
和 monitorexit
指令实现,编译器会在代码块前后插入这两个指令,确保锁的获取和释放。
4. 锁对象的选择与优化
关键原则:
-
避免锁字符串或基本类型 :因常量池缓存可能导致意外竞争(如
synchronized("LOCK")
可能全局锁死)。 -
使用私有锁对象 :如
private final Object lock = new Object();
,防止外部代码意外获取锁。 -
减少锁范围:尽量缩小同步代码块,避免在锁内执行 I/O 或复杂计算。
示例对比:
java
// 错误示例:锁字符串(风险高)
synchronized ("GLOBAL_LOCK") { ... }
// 正确示例:锁私有对象(安全)
private final Object lock = new Object();
synchronized (lock) { ... }
5. 进阶:锁升级与性能优化
JDK 1.6 后引入锁升级机制,减少锁竞争开销:
-
偏向锁:记录第一个获取锁的线程 ID,无竞争时直接进入。
-
轻量级锁:通过 CAS 自旋尝试获取锁,适用于低竞争场景。
-
重量级锁:竞争激烈时升级为操作系统级互斥锁,线程进入阻塞队列。
代码实战:
java
// 高竞争场景(可能触发重量级锁)
public class HighContentionDemo {
private static int counter = 0;
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
synchronized (HighContentionDemo.class) {
counter++;
}
}).start();
}
}
}
此时 JVM 可能直接将锁升级为重量级锁,避免自旋带来的 CPU 浪费。
总结
synchronized
的三种用法对应不同粒度的线程安全控制:
-
实例方法锁:保护实例变量,适用于对象级别的并发控制。
-
静态方法锁:保护静态变量,全局唯一锁。
-
代码块锁:灵活控制锁对象,优化性能的关键手段。
合理选择锁对象和范围,结合 JVM 的锁升级机制,能在保证线程安全的同时最大限度提升性能。