AtomicInteger和volatile

34
我知道volatile允许可见性,AtomicInteger允许原子性。 所以如果我使用一个volatile AtomicInteger,是否意味着我不需要使用任何其他的同步机制?
例如。
class A {

    private volatile AtomicInteger count;

    void someMethod(){
        // do something
        if(count.get() < 10) {
            count.incrementAndGet();
        }
}

这是线程安全的吗?


2
这里的“线程安全”是什么意思?例如,多个线程可以调用intValue(),然后都调用incrementAndGet(),因此无法保证上述代码不会使count超过10。 - Jon Skeet
我是说线程安全吗?这意味着它是否能够安全地避免丢失更新? - Achow
4
可以 - 但是你可能会得到一个大于10的计数。 - Jon Skeet
是的,我清楚地理解了,所以仍然需要在AtomicInteger周围使用同步块。非常感谢@JonSkeet。 - Achow
没有,这些都不是显而易见的。有各种情况可能超过软限制是可以的,但不能失去已经超过限制的次数。鉴于您实际的要求,我建议您提出一个新问题,并留下当前问题。 - Jon Skeet
显示剩余4条评论
5个回答

72

我认为Atomic*实际上提供了原子性和易失性。因此,当您调用(比如)AtomicInteger.get()时,您保证获得的是最新的值。这在java.util.concurrent.atomic 包文档中有详细说明:

对于原子变量的访问和更新,其内存效果通常遵循易失性的规则,正如《Java™语言规范》第17.4节所述。

  • get具有读取易失变量的内存效果。
  • set具有写入(分配)易失变量的内存效果。
  • lazySet具有写入(分配)易失变量的内存效果,除了它允许与后续(但不是先前)内存操作重新排序,这些操作本身不会施加重排序约束与普通非易失性写入。在其他使用情况下,- lazySet可能适用于将再也不会访问的引用置为空,以进行垃圾收集。
  • weakCompareAndSet原子地读取并有条件写入一个变量,但不创建任何happens-before顺序,因此不提供与目标以外的任何变量的先前或后续读取和写入相关的保证。
  • compareAndSet和所有其他读取和更新操作,例如getAndIncrement具有读取和写入易失变量的内存效果。

现在如果您有

volatile AtomicInteger count;

volatile的部分意味着每个线程将使用最新的AtomicInteger引用,而它是一个AtomicInteger的事实意味着您也将看到该对象的最新值。

这并不常见(在我的经验中),因为通常情况下您不会重新分配count以引用其他对象。相反,您会有:

private final AtomicInteger count = new AtomicInteger();

在那一点上,final 变量意味着所有线程将处理相同的对象 - 而 Atomic* 对象意味着它们将看到该对象中的最新值。


好的,我稍微修改了问题,您能否解释一下是否仍然适用? - Achow
1
@anirbanchowdhury:为什么要使用intValue()而不是get - Jon Skeet
好的,我的错误,我已经更新了原始问题以使用get()。 - Achow
非常感谢。这正是我在寻找的内容。 - Thomas
1
一个AtomicInteger中保存值的真实字段是易变的,这并不奇怪! - n0rm1e
显示剩余2条评论

6
我认为如果你将线程安全定义为在单线程模式和多线程模式下产生相同结果,则会发现它不是线程安全的。在单线程模式下,计数永远不会超过10,但在多线程模式下可能会超过10。
问题在于get和incrementAndGet是原子操作,但if则不是。请记住,非原子操作可以随时暂停。例如:
1. 当前计数为9。 2. 线程A运行if(count.get() < 10)并得到true并停在那里。 3. 线程B运行if(count.get() < 10),也得到了true,所以它运行count.incrementAndGet()并完成。现在count = 10。 4. 线程A恢复并运行count.incrementAndGet(),现在count = 11,在单线程模式下永远不会发生这种情况。
如果你想使其线程安全而不使用速度较慢的synchronized,请尝试使用以下实现方式:
class A{

final AtomicInteger count;

void someMethod(){
// do something
  if(count.getAndIncrement() <10){
      // safe now
  } else count.getAndDecrement(); // rollback so this thread did nothing to count
}

2
为了保持原始语义并支持多线程,你可以这样做:
public class A {

    private AtomicInteger count = new AtomicInteger(0);

    public void someMethod() {

        int i = count.get();
        while (i < 10 && !count.compareAndSet(i, i + 1)) {
            i = count.get();
        }

    }

}

这将避免任何线程看到计数达到10。

1

0

你的问题可以分为两个部分回答,因为你的问题有两个:

1)参考Oracle的原子变量教程文档: https://docs.oracle.com/javase/tutorial/essential/concurrency/atomicvars.html

java.util.concurrent.atomic包定义了支持单变量原子操作的类。所有类都有get和set方法,这些方法的工作方式类似于volatile变量的读取和写入。也就是说,set与同一变量上的任何后续get具有happens-before关系。原子compareAndSet方法也具有这些内存一致性特征,整数原子变量适用的简单原子算术方法也是如此。

因此,原子整数确实在内部使用了volatile,正如其他答案中提到的那样。因此,将原子整数设置为volatile没有意义。你需要同步你的方法。

你应该观看John Purcell在Udemy上的免费视频,在视频中他展示了当多个线程尝试修改它时,volatile关键字的失败。这是一个简单而美丽的例子。 https://www.udemy.com/course/java-multithreading/learn/lecture/108950#overview

如果你将John的示例中的易失性计数器更改为原子变量,他的代码就可以保证成功,而不需要像他在教程中所做的那样使用同步关键字。

2)针对你的代码: 假设线程1开始执行并且“someMethod”执行了get并检查大小。在getAndIncrement执行之前(例如,由线程1执行),另一个线程(例如线程2)可能会启动并将计数增加到10,然后退出;之后,你的线程1将恢复并将计数增加到11。这是错误的输出。这是因为你的“someMethod”没有受到任何同步问题的保护。 我仍然建议你观看John Purcell的视频,以了解volatile关键字的失败情况,这样你就可以更好地理解volatile关键字。在他的示例中将其替换为AtomicInteger,看看魔术发生了什么。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接