Java中synchronized关键字

1.概述

这篇快速文章将介绍如何在Java中使用synchronized块。

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

标记为synchronized的逻辑变为同步块,在任何给定时间只允许一个线程执行。

2.为什么同步?

让我们考虑一个典型的竞争条件,我们计算总和,多个线程执行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来执行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块时,内部Java使用监视器(也称为监视器锁或内部锁)来提供同步。这些监视器绑定到一个对象,因此同一对象的所有同步块只能有一个线程同时执行它们。

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 static方法

静态方法与实例方法一样是同步的:

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(BaeldungSynchronizedMethods::syncStaticCalculate));
    service.awaitTermination(100, TimeUnit.MILLISECONDS);
 
    assertEquals(1000, BaeldungSynchronizedMethods.staticSum);
}
3.3 方法中的同步块

有时我们不想同步整个方法,只需要同步其中的一些指令。这可以通过将 synchronized应用于块来实现:

public void performSynchrinisedTask() {
    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块。这是监视器对象,块内的代码在监视器对象上获得同步。简而言之,每个监视器对象只能在该代码块内执行一个线程。

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

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

5.结论

在这篇快速文章中,我们已经看到了使用synchronized关键字实现线程同步的不同方法。

我们还探讨了竞争条件如何影响我们的应用程序,以及同步如何帮助我们避免这种情况。有关使用Java中的锁的线程安全性的更多信息,请参阅我们的java.util.concurrent.Locks 文章。

关注微信公众号:Java知己
回复关键词 synchronized 获取本教程的完整代码。

在这里插入图片描述
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/feilang00/article/details/86148804