Volatile vs. Atomic Variables in Java – Java中的易失性变量与原子变量

最后修改: 2022年 8月 2日

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

1. Overview

1.概述

In this tutorial, we’ll learn the difference between the volatile keyword and atomic classes and what problems they solve. First, it’s necessary to know how Java handles the communication between threads and what unexpected issues can arise.

在本教程中,我们将学习volatile关键字和原子类之间的区别,以及它们所解决的问题。首先,有必要了解Java如何处理线程之间的通信,以及可能出现的意外问题

Thread safety is a crucial topic that provides an insight into the internal work of multithreaded applications. We’ll also discuss race conditions, but we won’t go too deep into this subject.

线程安全是一个至关重要的话题,它可以让我们深入了解多线程应用程序的内部工作。我们还将讨论race conditions,但我们不会太深入地讨论这个话题。

2. Concurrency Problem

2.并发问题

Let’s take a simple example to see the difference between atomic classes and the volatile keyword. Imagine that we’re trying to create a counter that will be working in a multithreaded environment.

让我们举个简单的例子来看看原子类和volatile关键字的区别。想象一下,我们正试图创建一个将在多线程环境下工作的计数器。

In theory, any application thread can increment this counter’s value. Let’s start implementing it with a naive approach and will check what problems will arise:

理论上,任何应用线程都可以增加这个计数器的值。让我们以一种天真的方式开始实现它,并将检查会出现什么问题。

public class UnsafeCounter {
    
    private int counter;
    
    int getValue() {
        return counter;
    }
    
    void increment() {
        counter++;
    }
}

This is a perfectly working counter, but, unfortunately, only for a single-threaded application. This approach will suffer from visibility and synchronization problems in a multithreaded environment. In big applications, it might create difficulty to track bugs and even corrupt users’ data.

这是一个完全可行的计数器,但不幸的是,它只适用于单线程的应用程序。在多线程环境中,这种方法将受到可见性和同步问题的影响。在大型应用程序中,它可能会给追踪错误带来困难,甚至会破坏用户的数据。

3. Visibility Problem

3.可见性问题

A visibility problem is one of the issues when working in a multithreaded application. The visibility problem is tightly connected to the Java memory model.

可见性问题是在多线程应用程序中工作时的问题之一。可见性问题与Java内存模型紧密相连。

In multithreaded applications, each thread has its cached version of shared resources and updates the values in or from the main memory based on events or a schedule.

在多线程应用程序中,每个线程都有其共享资源的缓存版本,并根据事件或时间表更新主内存中或来自主内存的值。

The thread cache and main memory values might differ. Therefore, even if one thread updates the values in the main memory, these changes are not instantly visible to other threads. This is called a visibility problem.

线程缓存和主内存的值可能不同。因此,即使一个线程更新了主内存中的值,这些变化对其他线程也不是即时可见的。这被称为可见性问题。

The volatile keyword helps us to resolve this issue by bypassing caching in a local thread. Thus, volatile variables are visible to all the threads, and all these threads will see the same value. Hence, when one thread updates the value, all the threads will see the new value. We can think about it as a low-level observer pattern and can rewrite the previous implementation:

volatile关键字有助于我们通过绕过本地线程中的缓存来解决这个问题。因此,volatile变量对所有线程都是可见的,并且所有这些线程将看到相同的值。因此,当一个线程更新数值时,所有的线程都会看到新的数值。我们可以把它看成是一个低级的观察者模式,并可以重写之前的实现。

public class UnsafeVolatileCounter {
    
    private volatile int counter;
    
    public int getValue() {
        return counter;
    }
    
    public void increment() {
        counter++;
    }
}

The example above improves the counter and solves the problem with visibility. However, we still have a synchronization problem, and our counter won’t work correctly in a multithreaded environment.

上面的例子改进了计数器,解决了可见性的问题。但是,我们仍然有一个同步问题,我们的计数器在多线程环境下无法正常工作。

4. Synchronization Problem

4.同步问题

Although volatile keyword helps us with visibility, we still have another problem. In our increment example, we perform two operations with the variable count. First, we read this variable and then assign a new value to it. This means that the increment operation isn’t atomic.

尽管volatile关键字帮助我们解决了可见性问题,但我们仍有另一个问题。在我们的增量示例中,我们对变量count>进行了两次操作。首先,我们读取这个变量,然后为其分配一个新值。这意味着增量操作并不是原子性的。

What we’re facing here is a race condition. Each thread should read the value first, increment it, and then write it back. The problem will happen when several threads start working with the value, and read it before another one writes it.

我们在这里面对的是一个race condition。每个线程都应该先读取数值,将其递增,然后再将其写回。问题将发生在几个线程开始处理该值,并在另一个线程写入该值之前读取它。

This way, one thread may override the result written by another thread. The synchronized keyword can resolve this problem. However, this approach might create a bottleneck, and it’s not the most elegant solution to this problem.

这样一来,一个线程可能会覆盖另一个线程写的结果。synchronized关键字可以解决这个问题。然而,这种方法可能会造成瓶颈,而且这也不是解决这个问题的最优雅的方法。

5. Atomic Values

5.原子值

Atomic values provide a better and more intuitive way to handle this issue. Their interface allows us to interact with and update values without a synchronization problem.

原子值提供了一种更好、更直观的方式来处理这个问题。它们的接口允许我们在没有同步问题的情况下与数值进行交互和更新。

Internally, atomic classes ensure that, in this case, the increment will be an atomic operation. Thus, we can use it to create a thread-safe implementation:

在内部,原子类确保在这种情况下,增量将是一个原子操作。因此,我们可以用它来创建一个线程安全的实现。

public class SafeAtomicCounter {
    private final AtomicInteger counter = new AtomicInteger(0);
    
    public int getValue() {
        return counter.get();
    }
    
    public void increment() {
        counter.incrementAndGet();
    }
}

Our final implementation is thread-safe and can be used in a multithreaded application. It doesn’t differ significantly from our first example, and only by using atomic classes could we resolve visibility and synchronization problems in the multithreaded code.

我们的最终实现是线程安全的,可以用于多线程应用程序。它与我们的第一个例子没有明显区别,只有通过使用原子类才能解决多线程代码的可见性和同步问题。

6. Conclusion

6.结论

In this article, we learned that we should be very cautious when we’re working in a multithreading environment. The bugs and issues can be tough to track down and probably won’t appear while debugging. That’s why it’s essential to know how Java handles these situations.

在这篇文章中,我们了解到,当我们在多线程环境中工作时,应该非常谨慎。错误和问题可能很难追踪,而且在调试时可能不会出现。这就是为什么了解Java如何处理这些情况是至关重要的。

The volatile keyword can help with visibility problems and resolve the issue with intrinsically atomic operations. Setting a flag is one of the examples where the volatile keyword might be helpful.

volatile关键字可以帮助解决可见性问题,并解决内在原子操作的问题。设置一个标志是volatile关键字可能有帮助的例子之一。

Atomic variables help with handling non-atomic operations like increment-decrement, or any operations, which need to read the value before assigning a new one. Atomic values are a simple and convenient way to resolve synchronization issues in our code.

原子变量有助于处理非原子操作,如增量-减量,或任何需要在分配新值之前读取数值的操作。原子值是解决我们代码中同步问题的一种简单而方便的方法。

As always, the source code for the examples is available over on GitHub.

像往常一样,这些例子的源代码可以在GitHub上找到