Java synchronized 块

更新于 2025-12-28

Jakob Jenkov 2020-08-12

Java 中的 synchronized 块用于将方法或代码块标记为同步。Java 的 synchronized 块在任意时刻只能被一个线程执行(具体取决于使用方式)。因此,Java synchronized 块可用于避免竞态条件(race conditions)

本教程将更详细地解释 Java synchronized 关键字的工作原理。

Java 并发工具类

synchronized 机制是 Java 最早用于同步多线程共享对象访问的机制。然而,synchronized 机制功能并不强大。因此,从 Java 5 开始,引入了一整套 并发工具类(concurrency utility classes),帮助开发者实现比 synchronized 更细粒度的并发控制。


Java synchronized 关键字

Java 中的 synchronized 块通过 synchronized 关键字进行标记。Java 的 synchronized 块会基于某个对象进行同步。所有基于同一个对象进行同步的 synchronized 块,在同一时间只能有一个线程执行其中的代码。其他试图进入该 synchronized 块的线程会被阻塞,直到当前线程退出该块。

synchronized 关键字可以用于标记以下四种类型的代码块:

  • 实例方法
  • 静态方法
  • 实例方法中的代码块
  • 静态方法中的代码块

这些块基于不同的对象进行同步。你需要根据具体情况选择合适的同步方式。下面将逐一详细介绍。


同步实例方法(Synchronized Instance Methods)

以下是一个同步实例方法的示例:

public class MyCounter {
    private int count = 0;

    public synchronized void add(int value){
        this.count += value;
    }
}

注意 add() 方法声明中使用了 synchronized 关键字,这告诉 Java 该方法是同步的。

Java 中的同步实例方法是基于拥有该方法的对象实例进行同步的。因此,每个实例的同步方法都基于不同的对象(即各自的实例)进行同步。

对于同一个实例,同一时间只能有一个线程执行其同步实例方法。如果存在多个实例,则每个实例各自允许一个线程执行其同步方法(即“每个实例一个线程”)。

这一规则适用于同一个对象的所有同步实例方法。例如:

public class MyCounter {
    private int count = 0;

    public synchronized void add(int value){
        this.count += value;
    }

    public synchronized void subtract(int value){
        this.count -= value;
    }
}

在这个例子中,对于同一个 MyCounter 实例,同一时间只能有一个线程执行 add()subtract() 中的任意一个方法。


同步静态方法(Synchronized Static Methods)

静态方法也可以像实例方法一样使用 synchronized 关键字进行标记。例如:

public class MyStaticCounter {
    private static int count = 0;

    public static synchronized void add(int value){
        count += value;
    }
}

这里的 synchronized 关键字同样表示该静态方法是同步的。

同步静态方法是基于该类的 Class 对象进行同步的。由于在 Java 虚拟机中,每个类只有一个 Class 对象,因此同一时间只能有一个线程执行该类中的任意一个同步静态方法。

例如,如果一个类中有多个同步静态方法:

public class MyStaticCounter {
    private static int count = 0;

    public static synchronized void add(int value){
        count += value;
    }

    public static synchronized void subtract(int value){
        count -= value;
    }
}

那么,同一时间只能有一个线程执行 add()subtract() 中的任意一个。如果线程 A 正在执行 add(),那么线程 B 既不能执行 add(),也不能执行 subtract(),直到线程 A 退出 add()

如果这些同步静态方法位于不同的类中,则每个类各自允许一个线程执行其同步静态方法(即“每个类一个线程”)。


实例方法中的同步块(Synchronized Blocks in Instance Methods)

你不需要对整个方法进行同步。有时只同步方法中的一部分代码更为合适。Java 允许在方法内部使用 synchronized 块来实现这一点。

以下是在非同步方法内部使用 synchronized 块的示例:

public void add(int value){
    synchronized(this){
        this.count += value;
    }
}

这里使用了 Java 的 synchronized 块语法,并传入一个对象(这里是 this,即当前实例)作为监视器对象(monitor object)。这段代码的效果等同于将其整个方法声明为同步方法。

只有基于同一个监视器对象的 synchronized 块,在同一时间才能被一个线程执行。

以下两个方法都是基于 this 进行同步的,因此在同步行为上是等价的:

public class MyClass {
    public synchronized void log1(String msg1, String msg2){
        log.writeln(msg1);
        log.writeln(msg2);
    }

    public void log2(String msg1, String msg2){
        synchronized(this){
            log.writeln(msg1);
            log.writeln(msg2);
        }
    }
}

因此,这两个方法在同一时间只能被一个线程执行。

如果第二个 synchronized 块使用了不同于 this 的对象作为监视器,那么两个方法就可以被不同线程同时执行。


静态方法中的同步块(Synchronized Blocks in Static Methods)

synchronized 块也可以用在静态方法中。以下是上述示例的静态版本:

public class MyClass {
    public static synchronized void log1(String msg1, String msg2){
        log.writeln(msg1);
        log.writeln(msg2);
    }

    public static void log2(String msg1, String msg2){
        synchronized(MyClass.class){
            log.writeln(msg1);
            log.writeln(msg2);
        }
    }
}

这两个方法都是基于 MyClass.class(即该类的 Class 对象)进行同步的,因此同一时间只能有一个线程执行其中任意一个方法。

如果第二个 synchronized 块使用了不同的对象作为监视器,那么两个方法就可以被不同线程同时执行。


Lambda 表达式中的同步块(Synchronized Blocks in Lambda Expressions)

你甚至可以在 Java Lambda 表达式 或匿名类中使用 synchronized 块。

以下是一个在 Lambda 表达式中使用 synchronized 块的示例。注意,该块是基于包含该 Lambda 表达式的类的 Class 对象进行同步的(当然也可以基于其他对象,视具体场景而定):

import java.util.function.Consumer;

public class SynchronizedExample {
    public static void main(String[] args) {
        Consumer<String> func = (String param) -> {
            synchronized(SynchronizedExample.class) {
                System.out.println(
                    Thread.currentThread().getName() + " step 1: " + param);
                try {
                    Thread.sleep((long) (Math.random() * 1000));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(
                    Thread.currentThread().getName() + " step 2: " + param);
            }
        };

        Thread thread1 = new Thread(() -> {
            func.accept("Parameter");
        }, "Thread 1");

        Thread thread2 = new Thread(() -> {
            func.accept("Parameter");
        }, "Thread 2");

        thread1.start();
        thread2.start();
    }
}

Java synchronized 示例

以下示例启动两个线程,并让它们调用同一个 Counter 实例的 add 方法。由于 add 方法是同步的(基于实例),因此同一时间只能有一个线程执行该方法。

public class Example {
    public static void main(String[] args){
        Counter counter = new Counter();
        Thread threadA = new CounterThread(counter);
        Thread threadB = new CounterThread(counter);
        threadA.start();
        threadB.start();
    }
}

使用的两个类如下:

public class Counter{
    long count = 0;

    public synchronized void add(long value){
        this.count += value;
    }
}

public class CounterThread extends Thread{
    protected Counter counter = null;

    public CounterThread(Counter counter){
        this.counter = counter;
    }

    public void run() {
        for(int i=0; i<10; i++){
            counter.add(i);
        }
    }
}

两个线程共享同一个 Counter 实例。由于 add() 方法是基于实例同步的,因此两个线程不能同时执行该方法。

如果两个线程分别使用不同的 Counter 实例,则它们可以同时调用各自的 add() 方法,因为它们是基于不同对象进行同步的:

public class Example {
    public static void main(String[] args){
        Counter counterA = new Counter();
        Counter counterB = new Counter();
        Thread threadA = new CounterThread(counterA);
        Thread threadB = new CounterThread(counterB);
        threadA.start();
        threadB.start();
    }
}

synchronized 与数据可见性(Synchronized and Data Visibility)

如果不使用 synchronized(或 volatile 关键字),当一个线程修改了与其他线程共享的变量(例如通过一个所有线程都能访问的对象)时,无法保证其他线程能看到这个修改后的值。这是因为 CPU 寄存器中的变量何时写回主内存、其他线程何时从主内存刷新寄存器中的变量值,都没有保证。

synchronized 关键字解决了这个问题:

  • 当线程进入 synchronized 块时,它会刷新所有可见变量的值(从主内存加载最新值)。
  • 当线程退出 synchronized 块时,它对所有可见变量的修改都会写回主内存

这与 volatile 关键字的行为类似。


synchronized 与指令重排序(Synchronized and Instruction Reordering)

Java 编译器和 JVM 允许对代码中的指令进行重排序,以提高执行效率(例如让 CPU 并行执行某些指令)。

但在多线程环境下,指令重排序可能导致问题。例如,如果 synchronized 块内的写操作被重排到块外执行,就会破坏同步语义。

为了解决这个问题,Java 的 synchronized 关键字对 synchronized 块前后及内部的指令重排序施加了限制。这与 volatile 关键字施加的限制类似。

最终结果是:你可以确信你的代码行为符合预期——不会因指令重排序而导致逻辑错误。


应该基于什么对象进行同步?(What Objects to Synchronize On)

如前所述,synchronized 块必须基于某个对象进行同步。你可以选择任何对象,但不建议使用字符串字面量或基本类型包装类对象(如 IntegerBoolean 等)作为监视器对象,因为编译器或 JVM 可能会对它们进行优化(缓存/复用),导致你以为不同的 synchronized 块实际上同步在同一个对象上。

例如:

synchronized("Hey") {
    // do something
}

多个地方使用 "Hey" 字符串字面量,JVM 可能会复用同一个 String 对象,导致这些 synchronized 块意外地同步在同一个对象上。

同样地:

synchronized(Integer.valueOf(1)) {
    // do something
}

Integer.valueOf(1) 在一定范围内会返回缓存的相同对象,也会导致多个 synchronized 块同步在同一个对象上。

安全做法:使用 thisnew Object() 作为监视器对象。这些对象不会被 JVM 或标准库缓存或复用。


synchronized 块的局限性与替代方案(Synchronized Block Limitations and Alternatives)

Java 的 synchronized 块存在一些局限性:

  • 只允许一个线程进入:即使多个线程只是读取共享数据(而非修改),也无法并发执行。此时可考虑使用 读写锁(Read/Write Lock)
  • 无法控制并发数量:如果你希望允许多个(但有限数量)线程进入,可以使用 信号量(Semaphore)。Java 提供了 Semaphore 类。
  • 不保证公平性:等待的线程获得锁的顺序不确定。如果你需要 FIFO 公平调度,需自行实现 公平性机制
  • 仅适用于单 JVM:synchronized 块只在同一个 JVM 内有效。在集群环境中(多个 JVM 实例),需要使用分布式锁等其他机制。
  • 简单读写场景:如果只有一个线程写、多个线程只读,可考虑使用 volatile 变量 而无需同步。

synchronized 块的性能开销(Synchronized Block Performance Overhead)

进入和退出 synchronized 块会带来轻微的性能开销。虽然随着 Java 版本演进,这一开销已大幅降低,但仍需注意:

  • 如果在紧密循环中频繁进入/退出 synchronized 块,性能影响可能显著。
  • 尽量缩小 synchronized 块的范围,只同步真正需要同步的代码,以提高程序的并行度。

synchronized 块的可重入性(Synchronized Block Reentrance)

一旦线程进入 synchronized 块,就认为它“持有”了该监视器对象的锁。如果该线程在持有锁的情况下再次尝试进入同一个监视器对象的 synchronized 块(例如递归调用),它是允许的(即可重入)。

例如:

public class MyClass {
    List<String> elements = new ArrayList<String>();

    public int count() {
        if(elements.size() == 0) {
            return 0;
        }
        synchronized(this) {
            elements.remove(0);
            return 1 + count(); // 递归调用,再次进入 synchronized 块
        }
    }
}

注:此例仅为演示可重入性,实际中不会这样计算列表长度。

但需注意:如果设计不当,多重 synchronized 块嵌套可能导致 嵌套监视器锁死(Nested Monitor Lockout)


集群环境中的 synchronized 块(Synchronized Blocks in Cluster Setups)

synchronized 块仅在同一个 Java 虚拟机内有效。如果你的应用部署在多个 JVM(集群)中,那么每个 JVM 中的线程都可以同时进入各自的 synchronized 块。

如果需要跨 JVM 的同步,必须使用其他机制(如分布式锁、数据库锁、Redis 锁等)。