Java 中 synchronized 关键字使用指南

更新于 2025-12-29

1. 概述

在本文中,我们将学习如何在 Java 中使用 synchronized 块。

简单来说,在多线程环境中,当两个或多个线程同时尝试修改共享的可变数据时,就会发生竞态条件(race condition)。Java 提供了一种机制,通过同步线程对共享数据的访问来避免竞态条件。

使用 synchronized 标记的一段逻辑会成为一个同步块(synchronized block),确保在任意时刻只有一个线程可以执行该代码。

2. 为什么要使用同步?

让我们考虑一个典型的竞态条件场景:我们计算一个总和,多个线程同时执行 calculate() 方法:

public class SynchronizedMethods {

    private int sum = 0;

    public void calculate() {
        setSum(getSum() + 1);
    }

    // 标准的 setter 和 getter 方法
}

然后编写一个简单的测试用例:

@Test
public void givenMultiThread_whenNonSyncMethod() {
    ExecutorService service = Executors.newFixedThreadPool(3);
    SynchronizedMethods summation = new SynchronizedMethods();

    IntStream.range(0, 1000)
      .forEach(count -> service.submit(summation::calculate));
    service.awaitTermination(1000, TimeUnit.MILLISECONDS);

    assertEquals(1000, summation.getSum());
}

这里我们使用了一个包含 3 个线程的 ExecutorService,执行 calculate() 方法 1000 次。

如果串行执行,预期结果应为 1000,但在多线程环境下,几乎每次都会失败,并返回不一致的实际结果:

java.lang.AssertionError: expected:<1000> but was:<965>
at org.junit.Assert.fail(Assert.java:88)
at org.junit.Assert.failNotEquals(Assert.java:834)
...

当然,这个结果并不意外。

避免竞态条件的一个简单方法就是使用 synchronized 关键字,使操作变成线程安全的。

3. synchronized 关键字

我们可以在不同层级使用 synchronized 关键字:

  • 实例方法(Instance methods)
  • 静态方法(Static methods)
  • 代码块(Code blocks)

当我们使用 synchronized 块时,Java 内部会使用一种称为监视器(monitor)(也叫监视器锁或内置锁)的机制来实现同步。这些监视器与对象绑定,因此同一个对象的所有 synchronized 块在同一时间只能被一个线程执行。

3.1 同步实例方法

我们可以在方法声明中添加 synchronized 关键字,使其成为同步方法:

public synchronized void synchronisedCalculate() {
    setSum(getSum() + 1);
}

注意,一旦我们将方法同步化,测试用例就能通过,实际输出为 1000:

@Test
public void givenMultiThread_whenMethodSync() {
    ExecutorService service = Executors.newFixedThreadPool(3);
    SynchronizedMethods method = new SynchronizedMethods();

    IntStream.range(0, 1000)
      .forEach(count -> service.submit(method::synchronisedCalculate));
    service.awaitTermination(1000, TimeUnit.MILLISECONDS);

    assertEquals(1000, method.getSum());
}

实例方法的同步是基于拥有该方法的类的实例对象。这意味着每个类的实例在同一时间只能有一个线程执行该同步方法。

3.2 同步静态方法

静态方法也可以像实例方法一样进行同步:

public static synchronized void syncStaticCalculate() {
    staticSum = staticSum + 1;
}

这类方法是基于与该类关联的 Class 对象进行同步的。由于每个 JVM 中每个类只有一个 Class 对象,因此无论该类有多少个实例,同一时间只能有一个线程执行该类的静态同步方法。

测试如下:

@Test
public void givenMultiThread_whenStaticSyncMethod() {
    ExecutorService service = Executors.newCachedThreadPool();

    IntStream.range(0, 1000)
      .forEach(count -> 
        service.submit(SynchronizedMethods::syncStaticCalculate));
    service.awaitTermination(100, TimeUnit.MILLISECONDS);

    assertEquals(1000, SynchronizedMethods.staticSum);
}

3.3 方法内的同步代码块

有时我们并不希望同步整个方法,而只是同步其中的一部分代码。这时可以将 synchronized 应用于代码块:

public void performSynchronisedTask() {
    synchronized (this) {
        setCount(getCount() + 1);
    }
}

然后我们可以测试这一改动:

@Test
public void givenMultiThread_whenBlockSync() {
    ExecutorService service = Executors.newFixedThreadPool(3);
    SynchronizedBlocks synchronizedBlocks = new SynchronizedBlocks();

    IntStream.range(0, 1000)
      .forEach(count -> 
        service.submit(synchronizedBlocks::performSynchronisedTask));
    service.awaitTermination(100, TimeUnit.MILLISECONDS);

    assertEquals(1000, synchronizedBlocks.getCount());
}

注意,我们在 synchronized 块中传入了 this 作为参数。这就是监视器对象(monitor object)。块内的代码将基于该监视器对象进行同步。简而言之,每个监视器对象在同一时间只允许一个线程执行该代码块。

如果方法是静态的,则应传入类名作为监视器对象,此时该类的 Class 对象将作为同步监视器:

public static void performStaticSyncTask(){
    synchronized (SynchronizedBlocks.class) {
        setStaticCount(getStaticCount() + 1);
    }
}

测试静态方法中的同步块:

@Test
public void givenMultiThread_whenStaticSyncBlock() {
    ExecutorService service = Executors.newCachedThreadPool();

    IntStream.range(0, 1000)
      .forEach(count -> 
        service.submit(SynchronizedBlocks::performStaticSyncTask));
    service.awaitTermination(100, TimeUnit.MILLISECONDS);

    assertEquals(1000, SynchronizedBlocks.getStaticCount());
}

3.4 可重入性(Reentrancy)

synchronized 方法和代码块背后的锁是可重入的(reentrant)。这意味着当前线程在已经持有某个同步锁的情况下,可以多次再次获取同一个锁:

Object lock = new Object();
synchronized (lock) {
    System.out.println("第一次获取锁");

    synchronized (lock) {
        System.out.println("再次进入");

        synchronized (lock) {
            System.out.println("又一次进入");
        }
    }
}

如上所示,在一个 synchronized 块内部,我们可以反复获取同一个监视器锁。

4. 结论

在本文中,我们探讨了使用 synchronized 关键字实现线程同步的多种方式。

我们还了解了竞态条件可能对应用程序造成的影响,以及同步机制如何帮助我们避免此类问题。