synchronized 关键字
Java 自带的锁,用法简单,但门道很深。
先看一个问题
java
public class Counter {
private int count = 0;
public void increment() {
count++; // 两个线程同时执行,可能丢更新
}
}怎么让 count++ 线程安全?用 synchronized!
synchronized 三种用法
用法一:修饰实例方法
锁住当前对象:
java
public class Counter {
private int count = 0;
public synchronized void increment() {
count++; // 同时只有一个线程能进来
}
public synchronized int get() {
return count;
}
}原理:锁的是 this 对象。
用法二:修饰静态方法
锁住类的 Class 对象:
java
public class StaticCounter {
private static int count = 0;
public static synchronized void increment() {
count++; // 锁住 StaticCounter.class
}
}原理:锁的是 Class 对象,全局唯一。
用法三:修饰代码块
锁住指定对象,最灵活:
java
public class Counter {
private int count = 0;
private final Object lock = new Object();
public void increment() {
synchronized (lock) { // 只锁这一块代码
count++;
}
}
public void decrement() {
synchronized (this) { // 锁当前对象
count--;
}
}
public void reset() {
synchronized (StaticCounter.class) { // 锁 Class 对象
count = 0;
}
}
}synchronized 的特性
可重入:同一线程可以反复进入
java
public class ReentrantDemo {
public synchronized void method1() {
System.out.println("method1");
method2(); // ✅ 可以再次获取锁
}
public synchronized void method2() {
System.out.println("method2");
method3();
}
public synchronized void method3() {
System.out.println("method3");
}
}如果不可重入,method1 调用 method2 时就会死锁。
保证原子性
java
// ❌ 不安全
public void increment() {
count++; // 分三步:读→改→写,可能被打断
}
// ✅ 安全
public synchronized void increment() {
count++; // 完整执行完,不会被打断
}保证可见性
java
// ✅ synchronized 保证可见性
public synchronized void write(int v) {
value = v; // 释放锁时,会刷新到主内存
}
public synchronized int read() {
return value; // 获取锁时,从主内存读取
}synchronized 锁的是什么?
锁的是对象的「入口」,不是代码。
java
// 两个线程操作两个不同的 Counter 实例
Counter c1 = new Counter();
Counter c2 = new Counter();
c1.increment(); // 锁 c1
c2.increment(); // 锁 c2
// ✅ 互不影响,因为锁的是不同的对象java
// 两个线程操作同一个 Counter 实例
Counter c = new Counter();
c.increment(); // 锁 c
c.decrement(); // 等 c 的锁释放
// ❌ 会互斥,因为锁的是同一个对象synchronized 锁升级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁
JVM 对 synchronized 做了优化,不会一上来就用重量级锁:
无锁 → 偏向锁 → 轻量级锁 → 重量级锁
│ │ │ │
│ 一个线程 多个线程 竞争激烈
│ 反复访问 交替获取 自旋失败偏向锁
只有一个线程访问同步块时使用。记录线程 ID,下次直接进入。
┌────────────────────────────────────────┐
│ Mark Word (64 bits) │
│ ┌────────────────────────────────────┐ │
│ │ 无锁状态 │ │
│ │ 分代年龄 | 偏向锁 | 锁标志位(01) │ │
│ └────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────┐ │
│ │ 偏向锁状态 │ │
│ │ 线程ID | Epoch | 分代年龄 | 锁标志位 │ │
│ └────────────────────────────────────┘ │
└────────────────────────────────────────┘轻量级锁
多个线程交替访问同步块(无真正竞争),使用自旋锁。
线程 A:创建锁记录 → 把 Mark Word 复制到锁记录 → CAS 修改 Mark Word 指向锁记录
线程 B:也想获取锁,发现 Mark Word 已指向锁记录,自旋等待重量级锁
竞争激烈时,自旋失败,升级为重量级锁。线程进入阻塞状态,不消耗 CPU。
JVM 默认延迟开启偏向锁,可通过 -XX:BiasedLockingStartupDelay=0 取消延迟。
最佳实践
1. 缩小锁的范围
java
// ❌ 不推荐:整个方法加锁
public synchronized void process() {
prepare(); // 不需要同步
sharedResource.modify(); // 需要同步
cleanup(); // 不需要同步
}
// ✅ 推荐:只锁必要部分
public void process() {
prepare();
synchronized (this) {
sharedResource.modify();
}
cleanup();
}2. 锁对象要选好
java
// ❌ 错误:锁可变对象
private String lock = "lock";
public void method() {
synchronized (lock) { // 如果 lock 被改了,问题严重
// ...
}
}
// ✅ 推荐:锁 final 对象
private final Object lock = new Object();3. 避免嵌套锁
java
// ❌ 嵌套过深,难维护
synchronized (lockA) {
synchronized (lockB) {
synchronized (lockC) {
// ...
}
}
}
// ✅ 推荐:提取方法,减少嵌套
public void method() {
synchronized (lock) {
doRealWork();
}
}
private synchronized void doRealWork() {
// ...
}4. 多线程同时访问不同对象,不需同步
java
// 两个线程分别访问不同的实例,不需要同步
Counter c1 = new Counter();
Counter c2 = new Counter();
t1: c1.increment(); // 锁 c1
t2: c2.increment(); // 锁 c2,互不影响synchronized vs Lock
| 对比项 | synchronized | Lock (ReentrantLock) |
|---|---|---|
| 获取锁 | 自动进入同步块 | 手动 lock() |
| 释放锁 | 自动离开同步块 | 手动 unlock() |
| 公平锁 | 否 | 可配置 |
| 超时等待 | 不支持 | tryLock(timeout) |
| 中断等待 | 不支持 | lockInterruptibly() |
| 条件变量 | Object.wait() | Condition |
| 性能 | JDK 6+ 已优化 | 相当或更好 |
总结
synchronized可以修饰方法或代码块- 锁的是对象,不是代码
- 具有可重入性
- 保证原子性、可见性、有序性
- 锁会升级:无锁 → 偏向锁 → 轻量级锁 → 重量级锁
- 优先使用同步代码块,缩小锁的范围
