侧边栏壁纸
博主头像
翻斗

开始一件事最好是昨天,其次是现在

  • 累计撰写 44 篇文章
  • 累计创建 42 个标签
  • 累计收到 3 条评论

Synchronized关键字

翻斗
2020-03-28 / 0 评论 / 0 点赞 / 620 阅读 / 4,099 字

Baeldung翻译系列之Java并发基础:

Java中的concurrent包
Java中的Synchronized关键字
Future介绍
ThreadLocal介绍
Java线程的生命周期
如何杀掉一个Java线程
Java中的线程池介绍
实现Runnable接口还是继承Thread类
Java中的wait和notify方法
Runnable vs Callable
wait和sleep的区别
Thread.join方法介绍
Java中使用锁对象
ThreadPoolTaskExecutor中的corePoolSize和maxPoolSize
Java中的异步编程

1.概览

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

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

使用 synchronized 标记的代码逻辑成为 synchronized 块,它只允许一个线程在任何给定时间执行。

2.为何要同步

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

public class SynchronizedMethods {

    private int sum = 0;

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

    // standard setters and getters
}

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

@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关键字:

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

当我们使用一个synchronized块时,Java内部使用一个监视器,也被称为监视器锁或内在锁,来提供同步。这些监视器绑定到一个对象上;因此,同一个对象的所有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,这就是监视器对象。块内的代码在监视器对象上进行同步。简单来说,每个监视器对象只能有一个线程在该代码块内部执行。

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

(译者:监视器就是这把锁的主体,多个线程同时执行,谁拿到了这把锁,谁才能执行,其他的线程必须等待)

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(SynchronizedBlocks::performStaticSyncTask));
    service.awaitTermination(100, TimeUnit.MILLISECONDS);

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

3.4. 可重入性

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

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

4. 结论

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

我们还了解了竞态条件如何影响我们的应用程序,以及同步如何帮助我们避免这种情况。

这篇文章的完整代码可以在GitHub上找到, 国内看Gitee

0

评论区