在Java中,何时使用volatile boolean比AtomicBoolean更好?

58

我查看了其他关于volatile vs. Atomicxxxx问题的stackoverflow帖子(包括这个),并阅读了java.util.current.atomic的说明,但对细微差别不太满意。

如果我要在使用volatile booleanAtomicBoolean之间做出决策,除了AtomicBoolean提供的原子读写操作(如compareAndSet()getAndSet())之外,还有实际区别吗?

假设我有以下代码:

volatile boolean flag;

如果我有一个线程读取标志并执行某些操作,然后清除该标志,在此之前一个或多个线程设置了该标志(但未清除它),这时使用volatile关键字是否足够安全?

volatile boolean相比,AtomicBoolean的成本是否更高,包括内存空间和性能开销(volatile boolean需要内存屏障,而AtomicBoolean需要内存屏障+一些CAS操作上的轻微锁定,如java.util.current.atomic描述所示)。

我的直觉是只使用AtomicBoolean以确保安全,但我想知道是否存在使用volatile boolean的情况(例如,如果有数千个实例且性能成为问题)。


1
你想用这个标志做什么?你是用这个标志来控制并发,还是其他什么?我在想底层问题是否能帮助解释你要找到的答案。 - Jonathan B
1
是的,这正是volatile关键字所要解决的问题。 - Johan Sjöberg
@Jonathan:这只是一个特定的例子……我在考虑一个程序,其中我有被标记为“脏”的组件需要持久化。这些组件会自行标记为“脏”,然后持久化管理器会找到这些脏组件,保存它们的状态,并将它们标记为非脏。 - Jason S
@BuZZ-dEE,那个问题比我的早,但我在写作时已经阅读过它(在尝试使用modpowers之前,请您完整地阅读问题),并且我提出了更详细的问题。因此,是的,存在重叠,但不是重复。 - Jason S
关于Java中何时需要使用AtomicBoolean的问题 - BuZZ-dEE
5个回答

75

AtomicBooleanvolatile 最主要的区别在实际操作中是,在使用 compare-and-set 操作时,volatile 变量不是原子性的。

 volatile boolean b;

 void foo() {
   if( b ) {
     //Here another thread might have already changed the value of b to false
     b = false;
   }
 }

但是考虑到您所有的并发写入都是幂等的,并且您只从一个线程中读取,所以这不应该成为一个问题。


9
感谢您提出幂等性概念,给予加一赞成。 - Jason S
2
@Tom Hawtin - tackline 比较并设置(或者交换)是一种普遍的操作,尽管在Java中没有内置的实现方式,除了 Atomic* 类。http://en.wikipedia.org/wiki/Compare-and-set - biziclop
2
在你的例子中,无论发生什么,结果都是false。一个更好的例子是b = !b。当两个线程翻转b时,它应该保持不变,但是如果没有原子性,它可能会失败。 - maaartinus
1
@maaartinus,我实际上想要证明的是,在OP所描述的特定情况下,这并不重要,因为操作是幂等的。但正如你所说,b =!b不是幂等的,因此它会失败。 - biziclop
如果 ( !bool && (bool = true) ) .. 做某些事情 .. 这是原子操作吗?应该是的,对吧? - mjs
显示剩余5条评论

20

实际上,AtomicBoolean 就是一个包含 volatile boolean 的对象。

每个对象都会有一些小的开销。可能微不足道,但可能需要更多的内存进入缓存。

如果您需要使用 AtomicBooleanFieldUpdater,那么会有相当多的性能开销。如果你不经常这样做(比如在 NIO 中的 attach),那就没问题。


4
实际上,它是一个"volatile int"。http://www.docjar.com/html/api/java/util/concurrent/atomic/AtomicBoolean.java.html - helpermethod
1
@辅助方法 我认为这并不重要。它完全取决于实现的内容。 - Tom Hawtin - tackline
在内部,Java 最喜欢整数,比其他原始类型更喜欢。 - bvdb

19

我不确定我完全同意其他回答; biziclop的回答在一定程度上是正确的,但除非我们知道更多的细节,否则我不确定我们能得出您是安全的结论。

在简单情况下,交错可能看起来像这样:

Thread 1 (writer)   Thread 2 (Writer)  Thread 3 (Reader)
-----------------   -----------------  -----------------
flag = true;
                                       if (flag) {
                    flag = true;
                                         flag = false;
                                         doStuff();

这也许没问题(第二组flag设置为true并不重要,因为doStuff()很可能仍能看到线程2需要做的任务。

但是如果你反过来执行线程3:

Thread 1 (writer)   Thread 2 (Writer)  Thread 3 (Reader)
-----------------   -----------------  -----------------
flag = true;
                                       if (flag) {
                                         doStuff();
                    flag = true;
                                         flag = false;

那么线程2的更新可能会丢失。

当然,你需要同样小心地处理线程2所做的任何其他事情,以确保它对线程3可见。如果存在其他状态需要线程2设置,则顺序在那里也很重要。

在简单情况下,是的,你没问题,但如果不仅仅是简单的标志开关,那么这变得更难以理解。


7
好的。虽然值得一提的是,在第二种情况下,AtomicBoolean 也无法帮忙解决问题。这时需要全面的同步措施。 - Sergei Tachenov
4
@Cowen: 我从未看过这样可视化的交错过程,但它非常清晰。当我阅读线程代码时,我会在脑海中进行交错,但这是一种很好的技巧,在解释线程代码中的竞态条件/弱点时进行记录。 - kevinarpe
@SergeyTachenov,使用Thread 3中的AtomicBoolean.getAndSet()方法不就可以解决这个问题了吗? - Андрей Беньковский
1
@Андрей,我想这应该是可以的,但我们需要更多有关这三个线程在做什么方面的细节,以便能够精确回答。它可以帮助线程3检测到线程2再次设置标志,但不能帮助确定何时发生以及是否使用了线程2的更新完成相关工作。 - Sergei Tachenov

8

这里有很多有用的信息。然而,我想补充另一个可能有用的区别。你可以有一个AtomicBooleans数组,但是(据我所知)你不能有一个volatile布尔数组。


1
不行,但是你仍然可以拥有一个SynchronizedSet,或其他同步的布尔集合,可以安全地发布该数组。 - pkran

2

然后一个或多个线程设置标志(但不清除它)。如果我有一个读取标志的线程,如果设置了标志,则执行操作,然后清除标志,那么volatile是否足够?

是的,如果您不需要AtomicBoolean的特殊功能,使用volatile完全可以。实际上,这是volatile的少数合理用途之一。


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