An Introduction to Atomic Variables in Java – Java中的原子变量简介

最后修改: 2017年 7月 22日

1. Introduction

1.介绍

Simply put, a shared mutable state very easily leads to problems when concurrency is involved. If access to shared mutable objects is not managed properly, applications can quickly become prone to some hard-to-detect concurrency errors.

简单地说,当涉及到并发时,共享的可变状态非常容易导致问题。如果对共享的易变对象的访问没有得到适当的管理,应用程序会很快变得容易出现一些难以检测的并发错误。

In this article, we’ll revisit the use of locks to handle concurrent access, explore some of the disadvantages associated with locks, and finally, introduce atomic variables as an alternative.

在这篇文章中,我们将重新审视使用锁来处理并发访问,探索与锁相关的一些缺点,最后,介绍原子变量作为一种替代方法。

2. Locks

2.锁

Let’s have a look at the class:

让我们看一下这个班级。

public class Counter {
    int counter; 
 
    public void increment() {
        counter++;
    }
}

In the case of a single-threaded environment, this works perfectly; however, as soon as we allow more than one thread to write, we start getting inconsistent results.

在单线程的环境中,这个方法工作得很好;但是,一旦我们允许一个以上的线程写入,我们就开始得到不一致的结果。

This is because of the simple increment operation (counter++), which may look like an atomic operation, but in fact is a combination of three operations: obtaining the value, incrementing, and writing the updated value back.

这是因为简单的增量操作(counter++),它看起来像一个原子操作,但实际上是三个操作的组合:获取值、增量和写回更新的值。

If two threads try to get and update the value at the same time, it may result in lost updates.

如果两个线程试图同时获取和更新数值,可能会导致丢失更新。

One of the ways to manage access to an object is to use locks. This can be achieved by using the synchronized keyword in the increment method signature. The synchronized keyword ensures that only one thread can enter the method at one time (to learn more about Locking and Synchronization refer to – Guide to Synchronized Keyword in Java):

管理对一个对象的访问的方法之一是使用锁。这可以通过在increment方法签名中使用synchronized关键字来实现。synchronized关键字确保在同一时间只有一个线程可以进入该方法(要了解有关锁和同步的更多信息,请参考 – Java中同步关键字指南)。

public class SafeCounterWithLock {
    private volatile int counter;
 
    public synchronized void increment() {
        counter++;
    }
}

Additionally, we need to add the volatile keyword to ensure proper reference visibility among threads.

此外,我们需要添加volatile关键字,以确保线程之间适当的引用可见性。

Using locks solves the problem. However, the performance takes a hit.

使用锁可以解决这个问题。然而,性能却受到了影响。

When multiple threads attempt to acquire a lock, one of them wins, while the rest of the threads are either blocked or suspended.

当多个线程试图获取一个锁时,其中一个线程获胜,而其余的线程要么被封锁,要么被暂停。

The process of suspending and then resuming a thread is very expensive and affects the overall efficiency of the system.

暂停和恢复一个线程的过程非常昂贵,并影响到系统的整体效率。

In a small program, such as the counter, the time spent in context switching may become much more than actual code execution, thus greatly reducing overall efficiency.

在一个小程序中,例如计数器,用于上下文切换的时间可能会变得比实际的代码执行多得多,从而大大降低了整体效率。

3. Atomic Operations

3.原子操作

There is a branch of research focused on creating non-blocking algorithms for concurrent environments. These algorithms exploit low-level atomic machine instructions such as compare-and-swap (CAS), to ensure data integrity.

有一个研究分支专注于为并发环境创建非阻塞算法。这些算法利用低级的原子机器指令,如比较和交换(CAS),以确保数据的完整性。

A typical CAS operation works on three operands:

一个典型的CAS操作在三个操作数上工作。

  1. The memory location on which to operate (M)
  2. The existing expected value (A) of the variable
  3. The new value (B) which needs to be set

The CAS operation updates atomically the value in M to B, but only if the existing value in M matches A, otherwise no action is taken.

CAS操作将M中的值原子化地更新为B,但只有当M中的现有值与A匹配时,才会采取任何行动。

In both cases, the existing value in M is returned. This combines three steps – getting the value, comparing the value, and updating the value – into a single machine level operation.

在这两种情况下,M中的现有值被返回。这将三个步骤–获取数值、比较数值和更新数值–结合到一个机器层面的操作中。

When multiple threads attempt to update the same value through CAS, one of them wins and updates the value. However, unlike in the case of locks, no other thread gets suspended; instead, they’re simply informed that they did not manage to update the value. The threads can then proceed to do further work and context switches are completely avoided.

当多个线程试图通过CAS更新同一个值时,其中一个线程会获胜并更新该值。然而,与锁的情况不同,没有其他线程被暂停;相反,他们只是被告知,他们没有设法更新该值。然后,这些线程可以继续做进一步的工作,并且完全避免了上下文切换。

One other consequence is that the core program logic becomes more complex. This is because we have to handle the scenario when the CAS operation didn’t succeed. We can retry it again and again till it succeeds, or we can do nothing and move on depending on the use case.

另一个后果是,核心程序逻辑变得更加复杂。这是因为我们必须处理CAS操作不成功的情况。我们可以一次又一次地重试,直到成功,或者我们可以什么都不做,继续前进,这取决于用例。

4. Atomic Variables in Java

4.Java中的原子变量

The most commonly used atomic variable classes in Java are AtomicInteger, AtomicLong, AtomicBoolean, and AtomicReference. These classes represent an int, long, boolean, and object reference respectively which can be atomically updated. The main methods exposed by these classes are:

Java中最常用的原子变量类是AtomicIntegerAtomicLongAtomicBoolean,以及AtomicReference。这些类分别代表一个intlongboolean、和对象引用,它们可以被原子化地更新。这些类所暴露的主要方法是。

  • get() – gets the value from the memory, so that changes made by other threads are visible; equivalent to reading a volatile variable
  • set() – writes the value to memory, so that the change is visible to other threads; equivalent to writing a volatile variable
  • lazySet() – eventually writes the value to memory, maybe reordered with subsequent relevant memory operations. One use case is nullifying references, for the sake of garbage collection, which is never going to be accessed again. In this case, better performance is achieved by delaying the null volatile write
  • compareAndSet() – same as described in section 3, returns true when it succeeds, else false
  • weakCompareAndSet() – same as described in section 3, but weaker in the sense, that it does not create happens-before orderings. This means that it may not necessarily see updates made to other variables. As of Java 9, this method has been deprecated in all atomic implementations in favor of weakCompareAndSetPlain(). The memory effects of weakCompareAndSet() were plain but its names implied volatile memory effects. To avoid this confusion, they deprecated this method and added four methods with different memory effects such as weakCompareAndSetPlain() or weakCompareAndSetVolatile()

A thread-safe counter implemented with AtomicInteger is shown in the example below:

一个用AtomicInteger实现的线程安全的计数器在下面的例子中显示。

public class SafeCounterWithoutLock {
    private final AtomicInteger counter = new AtomicInteger(0);
    
    public int getValue() {
        return counter.get();
    }
    public void increment() {
        while(true) {
            int existingValue = getValue();
            int newValue = existingValue + 1;
            if(counter.compareAndSet(existingValue, newValue)) {
                return;
            }
        }
    }
}

As you can see, we retry the compareAndSet operation and again on failure, since we want to guarantee that the call to the increment method always increases the value by 1.

正如你所看到的,我们重试compareAndSet操作,并在失败时再次重试,因为我们要保证对increment方法的调用总是将值增加1。

5. Conclusion

5.结论

In this quick tutorial, we described an alternate way of handling concurrency where disadvantages associated with locking can be avoided. We also looked at the main methods exposed by the atomic variable classes in Java.

在这个快速教程中,我们描述了另一种处理并发的方式,其中可以避免与锁相关的缺点。我们还研究了Java中的原子变量类所暴露的主要方法。

As always, the examples are all available over on GitHub.

一如既往,所有的例子都可以在GitHub上找到

To explore more classes that internally use non-blocking algorithms refer to a guide to ConcurrentMap.

要探索更多内部使用非阻塞算法的类,请参考 ConcurrentMap指南