Using a Mutex Object in Java – 在Java中使用互斥对象

最后修改: 2019年 8月 28日

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

1. Overview

1.概述

In this tutorial, we’ll see different ways to implement a mutex in Java.

在本教程中,我们将看到在Java中实现mutex的不同方法

2. Mutex

2.突变器

In a multithreaded application, two or more threads may need to access a shared resource at the same time, resulting in unexpected behavior. Examples of such shared resources are data-structures, input-output devices, files, and network connections.

在一个多线程的应用程序中,两个或更多的线程可能需要同时访问一个共享资源,从而导致意外行为。这种共享资源的例子是数据结构、输入输出设备、文件和网络连接。

We call this scenario a race condition. And, the part of the program which accesses the shared resource is known as the critical section. So, to avoid a race condition, we need to synchronize access to the critical section.

我们把这种情况称为竞争条件。而且,程序中访问共享资源的部分被称为关键部分因此,为了避免竞赛条件,我们需要同步对关键部分的访问。

A mutex (or mutual exclusion) is the simplest type of synchronizer – it ensures that only one thread can execute the critical section of a computer program at a time.

互斥(或互斥)是最简单的同步器类型–确保每次只有一个线程可以执行计算机程序的关键部分

To access a critical section, a thread acquires the mutex, then accesses the critical section, and finally releases the mutex. In the meantime, all other threads block till the mutex releases. As soon as a thread exits the critical section, another thread can enter the critical section.

为了访问临界区,一个线程获得了mutex,然后访问了临界区,最后释放了mutex。在此期间,所有其他线程都会阻塞,直到mutex释放。只要一个线程退出临界区,另一个线程就可以进入临界区。

3. Why Mutex?

3.为什么是Mutex?

First, let’s take an example of a SequenceGeneraror class, which generates the next sequence by incrementing the currentValue by one each time:

首先,让我们以SequenceGeneraror类为例,它通过每次将currentValue递增一个来生成下一个序列。

public class SequenceGenerator {
    
    private int currentValue = 0;

    public int getNextSequence() {
        currentValue = currentValue + 1;
        return currentValue;
    }

}

Now, let’s create a test case to see how this method behaves when multiple threads try to access it concurrently:

现在,让我们创建一个测试用例,看看这个方法在多个线程试图并发访问它时是如何表现的。

@Test
public void givenUnsafeSequenceGenerator_whenRaceCondition_thenUnexpectedBehavior() throws Exception {
    int count = 1000;
    Set<Integer> uniqueSequences = getUniqueSequences(new SequenceGenerator(), count);
    Assert.assertEquals(count, uniqueSequences.size());
}

private Set<Integer> getUniqueSequences(SequenceGenerator generator, int count) throws Exception {
    ExecutorService executor = Executors.newFixedThreadPool(3);
    Set<Integer> uniqueSequences = new LinkedHashSet<>();
    List<Future<Integer>> futures = new ArrayList<>();

    for (int i = 0; i < count; i++) {
        futures.add(executor.submit(generator::getNextSequence));
    }

    for (Future<Integer> future : futures) {
        uniqueSequences.add(future.get());
    }

    executor.awaitTermination(1, TimeUnit.SECONDS);
    executor.shutdown();

    return uniqueSequences;
}

Once we execute this test case, we can see that it fails most of the time with the reason similar to:

一旦我们执行这个测试用例,我们可以看到,它在大多数时候都失败了,原因类似于。

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

The uniqueSequences is supposed to have the size equal to the number of times we’ve executed the getNextSequence method in our test case. However, this is not the case because of the race condition. Obviously, we don’t want this behavior.

uniqueSequences的大小应该等于我们在测试案例中执行getNextSequence方法的次数。然而,由于竞赛条件的存在,情况并非如此。很明显,我们不希望出现这种行为。

So, to avoid such race conditions, we need to make sure that only one thread can execute the getNextSequence method at a time. In such scenarios, we can use a mutex to synchronize the threads.

因此,为了避免这种竞赛条件,我们需要确保每次只有一个线程可以执行getNextSequence方法。在这种情况下,我们可以使用一个mutex来同步各线程。

There are various ways, we can implement a mutex in Java. So, next, we’ll see the different ways to implement a mutex for our SequenceGenerator class.

我们可以用各种方法在Java中实现互斥。所以,接下来,我们将看到为我们的SequenceGenerator类实现互斥的不同方法。

4. Using synchronized Keyword

4.使用synchronized关键词

First, we’ll discuss the synchronized keyword, which is the simplest way to implement a mutex in Java.

首先,我们将讨论synchronized关键字,这是在Java中实现互斥的最简单方法。

Every object in Java has an intrinsic lock associated with it. The synchronized method and the synchronized block use this intrinsic lock to restrict the access of the critical section to only one thread at a time.

Java中的每个对象都有一个与之相关的内在锁。同步方法和同步块使用这个内在锁来限制一次只允许一个线程访问关键部分。

Therefore, when a thread invokes a synchronized method or enters a synchronized block, it automatically acquires the lock. The lock releases when the method or block completes or an exception is thrown from them.

因此,当一个线程调用一个同步方法或进入一个同步块时,它自动获得了锁。当方法或块完成时,锁就会释放,或者从它们中抛出一个异常。

Let’s change getNextSequence to have a mutex, simply by adding the synchronized keyword:

让我们把getNextSequence改成有一个mutex,只需添加synchronized关键字。

public class SequenceGeneratorUsingSynchronizedMethod extends SequenceGenerator {
    
    @Override
    public synchronized int getNextSequence() {
        return super.getNextSequence();
    }

}

The synchronized block is similar to the synchronized method, with more control over the critical section and the object we can use for locking.

synchronized块与synchronized方法类似,对关键部分和我们可以用于锁定的对象有更多控制。

So, let’s now see how we can use the synchronized block to synchronize on a custom mutex object:

所以,现在让我们看看我们如何使用synchronized块来在自定义的mutex对象上进行同步

public class SequenceGeneratorUsingSynchronizedBlock extends SequenceGenerator {
    
    private Object mutex = new Object();

    @Override
    public int getNextSequence() {
        synchronized (mutex) {
            return super.getNextSequence();
        }
    }

}

5. Using ReentrantLock

5.使用ReentrantLock

The ReentrantLock class was introduced in Java 1.5. It provides more flexibility and control than the synchronized keyword approach.

ReentrantLock类在Java 1.5中被引入。它比synchronized关键字方法提供了更多的灵活性和控制。

Let’s see how we can use the ReentrantLock to achieve mutual exclusion:

让我们看看如何使用ReentrantLock来实现互斥。

public class SequenceGeneratorUsingReentrantLock extends SequenceGenerator {
    
    private ReentrantLock mutex = new ReentrantLock();

    @Override
    public int getNextSequence() {
        try {
            mutex.lock();
            return super.getNextSequence();
        } finally {
            mutex.unlock();
        }
    }
}

6. Using Semaphore

6.使用Semaphore

Like ReentrantLock, the Semaphore class was also introduced in Java 1.5.

ReentrantLock一样,Semaphore类也在Java 1.5中被引入。

While in case of a mutex only one thread can access a critical section, Semaphore allows a fixed number of threads to access a critical section. Therefore, we can also implement a mutex by setting the number of allowed threads in a Semaphore to one.

在mutex的情况下,只有一个线程可以访问一个关键部分,而Semaphore允许固定数量的线程访问一个关键部分。因此,我们也可以通过将Semaphore中允许的线程数设置为1来实现mutex

Let’s now create another thread-safe version of SequenceGenerator using Semaphore:

现在让我们使用Semaphore创建另一个线程安全版本的SequenceGenerator

public class SequenceGeneratorUsingSemaphore extends SequenceGenerator {
    
    private Semaphore mutex = new Semaphore(1);

    @Override
    public int getNextSequence() {
        try {
            mutex.acquire();
            return super.getNextSequence();
        } catch (InterruptedException e) {
            // exception handling code
        } finally {
            mutex.release();
        }
    }
}

7. Using Guava’s Monitor Class

7.使用Guava的Monitor

So far, we’ve seen the options to implement mutex using features provided by Java.

到目前为止,我们已经看到了使用Java提供的功能来实现mutex的选项。

However, the Monitor class of Google’s Guava library is a better alternative to the ReentrantLock class. As per its documentation, code using Monitor is more readable and less error-prone than the code using ReentrantLock.

然而,Google的Guava库的Monitor类是ReentrantLock类的一个更好的替代品。根据其文档,使用Monitor的代码比使用ReentrantLock的代码更具可读性,错误率更低。

First, we’ll add the Maven dependency for Guava:

首先,我们要添加Guava的Maven依赖性。

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>31.0.1-jre</version>
</dependency>

Now, we’ll write another subclass of SequenceGenerator using the Monitor class:

现在,我们将使用Monitor类编写SequenceGenerator的另一个子类。

public class SequenceGeneratorUsingMonitor extends SequenceGenerator {
    
    private Monitor mutex = new Monitor();

    @Override
    public int getNextSequence() {
        mutex.enter();
        try {
            return super.getNextSequence();
        } finally {
            mutex.leave();
        }
    }

}

8. Conclusion

8.结语

In this tutorial, we’ve looked into the concept of a mutex. Also, we’ve seen the different ways to implement it in Java.

在本教程中,我们已经了解了互斥的概念。此外,我们还看到了在Java中实现它的不同方法。

As always, the complete source code of the code examples used in this tutorial is available over on GitHub.

一如既往,本教程中所使用的代码示例的完整源代码可在GitHub上获取