目录
synchronized是什么?synchronized的使用方式synchronized的底层原理synchronized的特性与优缺点synchronized与java.util.concurrent.locks.Lock的比较- 最佳实践与总结
synchronized 是什么?
synchronized 是 Java 中用于实现线程同步的关键字,它的核心作用是确保在同一时间,只有一个线程可以执行被 synchronized 修饰的代码块或方法,从而避免多线程环境下因共享资源竞争而导致的数据不一致问题。

可以把它想象成一个“房间的钥匙”:
- 当一个线程进入
synchronized代码块时,它相当于拿到了这把钥匙,并进入房间执行代码。 - 在这个线程执行期间,其他试图进入该房间的线程都必须在门外等待,因为钥匙被占用了。
- 当线程执行完毕,它会交出钥匙,这样等待的线程中才能有一个拿到钥匙并进入房间。
这种机制保证了共享资源的原子性(Atomicity)、可见性(Visibility)和有序性(Ordering),是解决并发问题的基本手段。
synchronized 的使用方式
synchronized 有三种主要的使用方式:
实例方法锁(锁住当前对象实例)
public class SynchronizedExample {
public synchronized void instanceMethod() {
// 临界区代码
System.out.println(Thread.currentThread().getName() + " is running instanceMethod.");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 锁对象:
this(当前类的对象实例)。 - 作用范围:当一个线程调用一个对象的
synchronized实例方法时,该对象的其他synchronized实例方法将被阻塞,直到前一个方法执行完毕。 - 示例:
SynchronizedExample example = new SynchronizedExample(); new Thread(example::instanceMethod, "Thread-A").start(); new Thread(example::instanceMethod, "Thread-B").start(); // 输出: // Thread-A is running instanceMethod. // (等待2秒后) // Thread-B is running instanceMethod.
静态方法锁(锁住类对象)
public class SynchronizedExample {
public static synchronized void staticMethod() {
// 临界区代码
System.out.println(Thread.currentThread().getName() + " is running staticMethod.");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
- 锁对象:
Class对象(SynchronizedExample.class),每个类在 JVM 中只有一个Class对象。 - 作用范围:当一个线程调用一个类的
synchronized静态方法时,该类的所有其他synchronized静态方法都将被阻塞,与具体实例无关。 - 示例:
new Thread(SynchronizedExample::staticMethod, "Thread-C").start(); new Thread(SynchronizedExample::staticMethod, "Thread-D").start(); // 输出: // Thread-C is running staticMethod. // (等待2秒后) // Thread-D is running staticMethod.
代码块锁(锁住指定对象)
这是最灵活的方式,可以只锁定代码中需要同步的部分,而不是整个方法。

public class SynchronizedExample {
private final Object lock = new Object(); // 一个专门的锁对象
public void blockMethod() {
// 锁住 this 对象
synchronized (this) {
// 临界区代码
System.out.println(Thread.currentThread().getName() + " is running in 'this' synchronized block.");
}
// 锁住一个自定义对象(推荐)
synchronized (lock) {
// 临界区代码
System.out.println(Thread.currentThread().getName() + " is running in 'lock' synchronized block.");
}
// 锁住类对象
synchronized (SynchronizedExample.class) {
// 临界区代码
System.out.println(Thread.currentThread().getName() + " is running in 'class' synchronized block.");
}
}
}
- 锁对象:可以是任意对象,包括
this、类的Class对象,或者一个专门创建的、不用于业务逻辑的Object实例。 - 最佳实践:永远不要使用字符串字面量作为锁对象,因为字符串在 Java 中是会被 JVM intern(暂存) 的,可能导致不同代码块意外地锁住了同一个字符串对象。
- 错误示例:
synchronized ("my_lock") { ... }危险! - 正确示例:
private final Object lock = new Object();
- 错误示例:
synchronized 的底层原理
synchronized 的底层实现与 JVM 版本有关,主要依赖于 Monitor(监视器) 机制。
在 Java 6 之前
- Monitor 实现:
synchronized是基于 操作系统互斥量(Mutex Lock) 实现的。 - 工作流程:
- 当线程尝试获取锁时,需要从用户态切换到内核态,通过操作系统 API 来获取互斥量。
- 如果获取失败,线程会进入阻塞状态,并会被放入一个阻塞队列中。
- 当锁被释放时,操作系统会从阻塞队列中唤醒一个线程,让它去竞争锁。
- 缺点:这个“用户态到内核态的切换”成本非常高,导致
synchronized的性能在早期版本中很差。
在 Java 6 及之后(优化升级)
为了提升性能,Java 对 synchronized 进行了大量优化,引入了锁升级(Lock Escalation)机制,一个对象在内存中的布局分为三部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding),锁信息就存储在对象头的 Mark Word 中。
synchronized 的锁状态分为四种,随着竞争升级而变化:
-
无锁状态
(图片来源网络,侵删)对象的 Mark Word 中没有记录锁信息。
-
偏向锁
- 目标:消除没有竞争时的同步操作,提高单线程性能。
- 机制:当一个线程访问同步块并获取锁时,会在 Mark Word 中记录线程ID,之后该线程再次进入同步块时,JVM 发现锁对象偏向于它,无需再进行任何同步操作。
- 适用场景:只有一个线程会反复访问同步代码的场景。
-
轻量级锁
- 目标:当有另一个线程尝试竞争锁时,避免进入阻塞状态(避免用户态/内核态切换)。
- 机制:
- 竞争线程会尝试通过自旋(Spin)的方式获取锁。
- 自旋就是让线程执行一个空循环,在很短的时间内不断尝试获取锁,而不是立即挂起。
- 如果自旋成功,线程就获得了锁。
- 如果自旋一定次数后仍未成功,说明竞争比较激烈,锁就会升级为重量级锁。
- 适用场景:线程交替执行同步块的场景(锁竞争时间很短)。
-
重量级锁
- 目标:当锁竞争非常激烈时,保证最终只有一个线程能持有锁。
- 机制:与 Java 6 之前一样,依赖操作系统的互斥量,未获取到锁的线程会被挂起,放入阻塞队列。
- 适用场景:锁竞争持续很长时间的场景。
锁升级路径: 无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
这个升级过程是单向不可逆的,锁只能升级,不能降级。
synchronized 的特性与优缺点
优点
- 使用简单:是 Java 语言的关键字,JVM 原生支持,语法简洁,不易出错。
- 自动释放锁:基于 JVM 机制,锁的获取和释放是隐式的,当一个线程执行完
synchronized代码块或因异常退出时,JVM 会自动释放锁,避免了忘记释放锁导致死锁的问题。 - 保证可见性:
synchronized不仅保证原子性,还保证可见性,一个线程在解锁前对共享变量的修改,对后续获取该锁的另一个线程是立即可见的。 - 保证有序性:
synchronized内部的代码和外部的代码不会发生重排序,保证了执行的有序性。
缺点
- 性能开销:虽然经过大量优化,但在高并发竞争激烈的情况下,重量级锁带来的用户态/内核态切换和线程阻塞/唤醒的开销仍然很大。
- 功能有限:功能相对简单,无法实现一些高级的锁功能,
- 尝试获取锁并超时:
synchronized无法设置获取锁的超时时间。 - 公平锁/非公平锁:
synchronized只能实现非公平锁。 - 多个条件变量:一个
synchronized只有一个等待队列,而Lock可以有多个Condition,实现更精确的线程唤醒。
- 尝试获取锁并超时:
- 可能导致死锁:如果多个线程以不同的顺序获取多个锁,很容易发生死锁,虽然
synchronized本身不会导致死锁,但使用不当会。
synchronized 与 java.util.concurrent.locks.Lock 的比较
| 特性 | synchronized |
java.util.concurrent.locks.Lock (如 ReentrantLock) |
|---|---|---|
| 实现机制 | JVM 层面的 Monitor 机制 | Java 代码层面实现的 API |
| 锁获取/释放 | 自动获取和释放 | 必须手动 lock() 和 unlock(),通常在 finally 块中释放 |
| 锁类型 | 非公平锁 | 可选公平锁或非公平锁(构造函数参数) |
| 等待/中断 | 线程会一直等待,无法响应中断 | 可以响应中断(lockInterruptibly()) |
| 超时获取 | 不支持 | 支持(tryLock(long time, TimeUnit unit)) |
| 条件变量 | 一个(隐式的) | 多个 Condition 对象,可实现精确唤醒 |
| 灵活性 | 较低 | 高,可实现更复杂的同步逻辑 |
| 使用复杂度 | 低,简单易用 | 高,需要手动管理锁的生命周期,容易出错 |
| 性能 | Java 6 后优化很好,在低竞争下性能优秀 | 在高竞争下,性能通常优于 synchronized,因为它提供了更多优化手段 |
如何选择?
- 优先使用
synchronized:对于绝大多数同步场景,synchronized已经足够好,并且更安全、更简单,除非有明确的、synchronized无法满足的需求。 - 考虑使用
Lock:当需要以下功能时,应考虑使用ReentrantLock:- 需要实现公平锁。
- 需要尝试获取锁并设置超时。
- 需要可中断的锁获取。
- 需要多个条件变量来管理线程等待/唤醒。
最佳实践与总结
-
作用范围最小化:尽量使用
synchronized代码块而不是同步整个方法,只锁定真正需要同步的代码段,以减少锁的持有时间,提高并发性。// 不推荐 public synchronized void badMethod() { // ... 一些非同步代码 ... // ... 一些同步代码 ... } // 推荐 public void goodMethod() { // ... 一些非同步代码 ... synchronized (this) { // ... 只同步必要的代码 ... } } -
避免在
synchronized块中调用外部方法:特别是可能被重写或执行时间不确定的方法,这会延长锁的持有时间,增加死锁风险。 -
使用专门的锁对象:对于实例方法,如果同步范围仅限于某个特定资源,不要使用
synchronized (this),而是创建一个private final的锁对象,这可以避免与其他无关代码(如第三方库)因锁this而产生不必要的阻塞。private final Object lock = new Object(); public void doSomething() { synchronized (lock) { // ... } } -
注意死锁:如果需要获取多个锁,始终以相同的顺序获取它们,先锁 A,再锁 B,这样即使多个线程也遵循这个顺序,也不会出现线程 A 拿到 A 等待 B,而线程 B 拿到 B 等待 A 的情况。
synchronized 是 Java 并发编程的基石,它通过内置锁机制简单有效地解决了线程安全问题,尽管 java.util.concurrent.locks.Lock 提供了更强大的功能和更好的灵活性,但对于绝大多数日常开发场景,synchronized 仍然是首选,因为它简单、安全且在 JVM 层面得到了深度优化,理解其原理、使用方式和优缺点,对于编写高质量、高性能的并发程序至关重要。
