Guide to the Synchronized Keyword in Java – Java中的 “同步 “关键字指南

最后修改: 2017年 5月 18日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

This quick tutorial will be an intro to using the synchronized block in Java.

这个快速教程将介绍如何在Java中使用synchronized块。

Simply put, in a multi-threaded environment, a race condition occurs when two or more threads attempt to update mutable shared data at the same time. Java offers a mechanism to avoid race conditions by synchronizing thread access to shared data.

简单地说,在多线程环境中,当两个或多个线程试图同时更新易变的共享数据时,就会出现race condition。Java提供了一种机制,通过同步线程对共享数据的访问来避免竞赛条件。

A piece of logic marked with synchronized becomes a synchronized block, allowing only one thread to execute at any given time.

一段标有synchronized的逻辑成为一个同步块,在任何时候只允许一个线程执行

2. Why Synchronization?

2.为什么要进行同步化?

Let’s consider a typical race condition where we calculate the sum, and multiple threads execute the calculate() method:

让我们考虑一个典型的竞赛条件,我们计算总和,多个线程执行calculate() 方法。

public class BaeldungSynchronizedMethods {

    private int sum = 0;

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

    // standard setters and getters
}

Then let’s write a simple test:

然后让我们写一个简单的测试。

@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());
}

We’re using an ExecutorService with a 3-threads pool to execute the calculate() 1000 times.

我们正在使用一个有3个线程池的ExecutorService 来执行calculate() 1000次。

If we executed this serially, the expected output would be 1000, but our multi-threaded execution fails almost every time with an inconsistent actual output:

如果我们串行执行,预期输出为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)
...

Of course, we don’t find this result unexpected.

当然,我们并不觉得这个结果出乎意料。

A simple way to avoid the race condition is to make the operation thread-safe by using the synchronized keyword.

避免竞赛条件的一个简单方法是通过使用synchronized关键字使操作成为线程安全的。

3. The Synchronized Keyword

3.同步的关键词

We can use the synchronized keyword on different levels:

我们可以在不同层面上使用synchronized关键字。

  • Instance methods
  • Static methods
  • Code blocks

When we use a synchronized block, Java internally uses a monitor, also known as monitor lock or intrinsic lock, to provide synchronization. These monitors are bound to an object; therefore, all synchronized blocks of the same object can have only one thread executing them at the same time.

当我们使用同步块时,Java内部使用监视器,也称为监视器锁或内在锁,以提供同步。这些监视器被绑定到一个对象;因此,同一对象的所有同步块在同一时间只能有一个线程执行。

3.1. Synchronized Instance Methods

3.1.同步的实例方法

We can add the synchronized keyword in the method declaration to make the method synchronized:

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

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

Notice that once we synchronize the method, the test case passes with the actual output as 1000:

请注意,一旦我们同步了这个方法,测试用例就会通过,实际输出为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());
}

Instance methods are synchronized over the instance of the class owning the method, which means only one thread per instance of the class can execute this method.

实例方法在拥有该方法的类的实例上是同步的,这意味着该类的每个实例只有一个线程可以执行该方法。

3.2. Synchronized Static Methods

3.2.同步的静态方法

Static methods are synchronized just like instance methods:

静态方法是同步的就像实例方法。

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

These methods are synchronized on the Class object associated with the class. Since only one Class object exists per JVM per class, only one thread can execute inside a static synchronized method per class, irrespective of the number of instances it has.

这些方法是在与该类相关的Class对象上同步的。由于每个JVM的每个类只存在一个Class对象,所以每个类只有一个线程可以在static synchronized 方法内执行,而不管它有多少实例。

Let’s test it:

让我们来测试一下。

@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 Blocks Within Methods

3.3.方法中的同步

Sometimes we don’t want to synchronize the entire method, only some instructions within it. We can achieve this by applying synchronized to a block:

有时我们并不想同步整个方法,只想同步其中的一些指令。我们可以通过应用同步到一个块来实现这一点。

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

Then we can test the change:

然后我们可以测试这个变化。

@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());
}

Notice that we passed a parameter this to the synchronized block. This is the monitor object. The code inside the block gets synchronized on the monitor object. Simply put, only one thread per monitor object can execute inside that block of code.

注意,我们向synchronized块传递了一个参数this。这就是监视器对象。块内的代码在监视器对象上得到同步。简单地说,每个监视器对象只有一个线程可以在该代码块内执行。

If the method was static, we would pass the class name in place of the object reference, and the class would be a monitor for synchronization of the block:

如果该方法是static,我们将传递类的名称来代替对象引用,而该类将成为同步块的监视器。

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

Let’s test the block inside the static method:

让我们测试一下static 方法里面的块。

@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. Reentrancy

3.4.再入性

The lock behind the synchronized methods and blocks is reentrant. This means the current thread can acquire the same synchronized lock over and over again while holding it:

synchronized方法和块背后的锁是可重入的。这意味着当前线程可以在持有相同的synchronized锁的情况下反复获取。

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");
         }
    }
}

As shown above, while we’re in a synchronized block, we can acquire the same monitor lock repeatedly.

如上所示,当我们在一个同步块中时,我们可以重复获取同一个监控锁。

4. Conclusion

4.结论

In this brief article, we explored different ways of using the synchronized keyword to achieve thread synchronization.

在这篇简短的文章中,我们探讨了使用synchronized关键字来实现线程同步的不同方法。

We also learned how a race condition can impact our application, and how synchronization helps us avoid that. For more about thread safety using locks in Java, refer to our java.util.concurrent.Locks article.

我们还学习了竞赛条件如何影响我们的应用程序,以及同步化如何帮助我们避免这种情况。关于在Java中使用锁的线程安全的更多信息,请参考我们的java.util.concurrent.Locks 文章

The complete code for this article is available over on GitHub.

本文的完整代码可在GitHub上获得