1.概述

本文章讲解Java中如何使用 synchronized 关键字。

在多线程环境中,当两个或多个线程同时尝试更新可变的共享数据时会发生Race Conditions(竞争条件)。Java提供了一种通过同步线程控制对共享变量访问的机制来避免Reace Conditions。

一段标有 synchronized 的代码变为同步块,在任何时间上只允许被一个线程执行。

2.为什么需要同步?

让我们考虑一个典型的Race Conditions,用多个线程执行 calculate() 方法求和:

public class BaeldungSynchronizedMethods {

    private int sum = 0;

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

    // standard setters and getters
}

让我们写一个简单的测试:

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

    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)
...

这个结果是意料之中的。

避免Race Conditions的简单方法是通过使用 synchronized 关键字使得线程操作安全。

3. synchronized 关键词


synchronized关键字可以修饰不同层级的代码:

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

当我们使用 synchronized 时,java内部使用Monitor也称为监视锁或内在锁,来实现同步。每个对象都会绑定一个Monitor,因此同一对象的同步代码,同一时间只能被一个线程执行。

3.1 synchronized 实例方法

只需在方法声明中添加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 synchronized 静态方法

静态方法同步和普通的实例方法一样:

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

静态方法同步锁住的是类对象,并且由于每个jvm仅存在一个该类对象,所以同一时间只有一个线程可以执行该类的静态同步方法,无论有多少类的实例。

下面来测试:

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

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

    assertEquals(1000, BaeldungSynchronizedMethods.staticSum);
}

3.3 Synchronized 代码块

有时我们不想同步整个方法,而只是其中部分代码:

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

下面测试下:

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

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

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

请注意,我们传了个this参数给synchronized()。这个参数就是monitor对象,synchronized 块中的代码是在此monitor对象上同步。简单来说,每个monitor对象,同时只能有一个线程可以执行该代码块内的代码。

如果该方法是静态方法,我们则可以传递类对象,该类对象将成为用于同步块的monitor对象:

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

测试:

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

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

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

3.4 可重入性

synchronized锁是支持重入的(reentrant)。也就是说,当前线程获得锁对象后,还能再次进入同步代码块获取相同的同步锁:

Object lock = new Object();
synchronized (lock) {
    System.out.println("First time acquiring it");

    synchronized (lock) {
        System.out.println("Entering again");

         synchronized (lock) {
             System.out.println("And again");
         }
    }
}

如上所示,当我们在Synchronized代码块中时,我们还可以重复获取相同的Monitor锁。

4.结论

本文章中,我们学习了如何使用synchronized关键字实现线程同步。

更多使用Java锁实现线程安全性的内容,请参阅我们的java.util.concurrent.Locks文章

本教程完整代码存放在GitHub