什么是 synchronized 块?
synchronized 块是 Java 中用于实现线程同步的关键字,它的主要作用是确保在同一时间,只有一个线程可以执行被 synchronized 保护的代码块,这可以有效防止多个线程同时修改共享数据,从而避免数据不一致、数据损坏等并发问题。

synchronized 就是在代码周围加了一把“锁”,拿到锁的线程才能进入,执行完代码后释放锁,其他线程才能竞争这把锁。
synchronized 块的基本语法
synchronized 块有以下两种主要形式:
锁定任意对象(最常用)
这种形式用于锁定一个对象实例或类对象,确保只有一个线程能访问被该对象锁定的代码块。
synchronized (锁对象) {
// 需要被同步的代码块
// 只有获取到 "锁对象" 的锁的线程才能执行这里的代码
}
关键点:

- 锁对象:可以是任何 Java 对象,通常推荐使用共享资源本身、或者专门用于同步的
Object对象。 - 同一个锁对象:只有当多个线程试图锁定的是同一个锁对象时,同步才有效,如果每个线程都用自己的锁对象,那么它们之间就不会互相阻塞。
锁定 Class 对象(用于静态方法)
这种形式用于锁定类的 Class 对象,常用于同步静态方法或静态代码块,确保在 JVM 中,一个类的所有静态同步代码块在同一时间只能被一个线程执行。
synchronized (ClassName.class) {
// 需要被同步的静态代码块
// 只有获取到 "ClassName.class" 这把锁的线程才能执行
}
synchronized 块 vs. synchronized 方法
在学习 synchronized 块之前,很多人已经接触过 synchronized 方法,它们的核心原理是一样的,都是通过“锁”来实现的,但锁的粒度和应用场景有所不同。
| 特性 | synchronized 方法 |
synchronized 块 |
|---|---|---|
| 锁对象 | 非静态方法:this (当前实例对象)静态方法: ClassName.class (类对象) |
由程序员显式指定,可以是任意对象 |
| 锁粒度 | 粗粒度,锁定了整个方法,即使方法中只有一小部分代码需要同步。 | 细粒度,只锁定真正需要同步的关键代码,提高了并发性能。 |
| 灵活性 | 低,只能对整个方法进行同步。 | 高,可以精确控制哪些代码需要同步,哪些不需要。 |
| 语法 | public synchronized void method() { ... } |
synchronized (lock) { ... } |
在大多数情况下,推荐使用 synchronized 块,因为它提供了更好的性能和灵活性。
synchronized 块的工作原理(可重入锁)
synchronized 使用的是可重入锁(Reentrant Lock)。

- 可重入:指的是同一个线程可以多次获取同一个锁,而不会导致自己阻塞自己。
- 场景:如果一个线程已经持有一个锁,然后在同步代码块中调用另一个也需要同一个锁的同步方法,它是可以成功获取锁的。
示例:
class ReentrantDemo {
public synchronized void method1() {
System.out.println("进入 method1");
method2(); // 调用另一个同步方法
System.out.println("退出 method1");
}
public synchronized void method2() {
System.out.println("进入 method2");
// do something...
System.out.println("退出 method2");
}
}
// 在某个线程中调用
ReentrantDemo demo = new ReentrantDemo();
demo.method1();
输出:
进入 method1
进入 method2
退出 method2
退出 method1
synchronized 不是可重入的,当 method1 执行到 method2 时,会因为试图再次获取已经持有的锁而陷入死锁,但由于它是可重入的,所以可以正常执行。
代码示例
示例1:使用 synchronized 块保护共享资源(银行取钱)
假设有一个银行账户,两个线程同时尝试取钱。
class BankAccount {
private double balance = 1000.0;
// 锁对象,可以是任意对象,但必须是共享的
private final Object lock = new Object();
public void withdraw(double amount) {
// 使用 synchronized 块来保护关键代码
synchronized (lock) {
if (balance >= amount) {
System.out.println(Thread.currentThread().getName() + " 正在取款: " + amount);
try {
// 模拟处理时间
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance -= amount;
System.out.println(Thread.currentThread().getName() + " 取款成功,剩余余额: " + balance);
} else {
System.out.println(Thread.currentThread().getName() + " 余额不足,取款失败");
}
}
// synchronized 块外的代码可以被其他线程同时执行
}
}
public class Main {
public static void main(String[] args) {
BankAccount account = new BankAccount();
// 创建两个线程,模拟两个人同时取钱
Thread t1 = new Thread(() -> account.withdraw(600), "张三");
Thread t2 = new Thread(() -> account.withdraw(600), "李四");
t1.start();
t2.start();
}
}
可能输出:
张三 正在取款: 600.0
张三 取款成功,剩余余额: 400.0
李四 余额不足,取款失败
在这个例子中,synchronized (lock) 确保 if 判断、sleep 和 balance -= amount 这几个操作是原子性的,不会被另一个线程打断。t1 已经进入同步块并开始取款,t2 必须等待 t1 完成并释放 lock 对象后才能进入。
示例2:synchronized 块 vs. synchronized 方法
class Counter {
private int count = 0;
// 使用 synchronized 方法
public synchronized void incrementSyncMethod() {
count++;
}
// 使用 synchronized 块,锁对象是 this
public void incrementSyncBlock() {
synchronized (this) {
count++;
}
}
}
在这个简单的例子中,incrementSyncMethod() 和 incrementSyncBlock() 的效果是完全相同的,因为 synchronized 方法的锁对象就是 this。
最佳实践和注意事项
- 锁的范围要尽可能小:只同步真正需要同步的代码块,而不是整个方法,这样可以减少线程阻塞的时间,提高程序的并发性能。
- 避免锁定
String或Boxed基本类型:- 不要用
String:因为 Java 的字符串常量池可能会导致两个看似不同的String对象指向同一个内存地址,或者两个不同的字符串因为intern()而成为同一个锁,引发意想不到的死锁问题。 - 不要用
Integer,Long等包装类:因为它们的值在-128到127之间会被缓存,也可能导致不同线程使用同一个锁对象。 - 最佳实践:专门为同步创建一个
private final Object lock = new Object();对象,或者直接同步共享资源本身(如果它是对象类型)。
- 不要用
- 不要在
synchronized块内调用外部方法:如果在同步块内调用的方法可能会长时间运行、阻塞或获取其他锁,这会严重影响并发性能,甚至可能导致死锁。 - 注意死锁:如果多个线程互相等待对方持有的锁,就会形成死锁,线程 A 锁住了对象 1 并等待对象 2,而线程 B 锁住了对象 2 并等待对象 1。
| 特性 | 描述 |
|---|---|
| 目的 | 保证多线程环境下共享数据的一致性和完整性。 |
| 原理 | 通过内置锁(监视器 Monitor)实现,同一时间只有一个线程能持有锁。 |
| 语法 | synchronized (锁对象) { ... } |
| 优点 | - 简单易用,由 JVM 保证原子性、可见性和有序性。 - 是可重入锁,避免死锁。 |
| 缺点 | - 性能开销比 java.util.concurrent.locks.Lock 稍大。
|
