易失性布尔型 vs 原子布尔型

303

AtomicBoolean相对于volatile boolean的优点是什么?


22
我正在寻找更细致的回答:“每种方法的限制是什么?”例如,如果一个线程设置标记并由一个或多个其他线程读取,那么就不需要使用AtomicBoolean。然而,如同我从这些答案中看到的,如果线程在多个线程中共享变量,且多个线程可以写入并依据读取结果执行操作,那么AtomicBoolean会带来CAS类型的非锁定操作。实际上,我在这里学到了很多。希望其他人也能受益。 - JeffV
1
可能是 volatile boolean vs. AtomicBoolean 的重复问题。 - Flow
易变的布尔类型需要显式同步来处理竞态条件,换句话说,就是多个线程更新共享资源(状态更改)的情况,例如增量/减量计数器或翻转布尔值。 - sactiw
12个回答

290
我在仅当字段仅由其所有者线程更新且值仅由其他线程读取时使用volatile字段,您可以将其视为发布/订阅场景,其中有许多观察者但只有一个发布者。但是,如果这些观察者必须根据字段的值执行某些逻辑,然后推回新值,则我会使用Atomic *变量、锁定或同步块,以适应最好的情况。在许多并发场景中,它归结为获取该值,将其与另一个值进行比较,并在必要时进行更新,因此在Atomic *类中存在compareAndSet和getAndSet方法。
请查看java.util.concurrent.atomic包的JavaDocs,了解Atomic类列表和它们工作方式的出色说明(刚学到它们是无锁的,因此它们比锁或同步块具有优势)。

2
@ksl,我认为@teto想表达的是,如果只有一个线程修改boolean变量,我们应该选择volatile boolean - znlyj
2
优秀的总结。 - Ravindra babu

135

它们完全不同。考虑这个volatile整数的例子:

volatile int i = 0;
void incIBy5() {
    i += 5;
}

如果两个线程同时调用该函数,i 可能在执行后变为5。这是因为编译后的代码与以下代码有些相似(除了你不能在 int 上同步):

void incIBy5() {
    int temp;
    synchronized(i) { temp = i }
    synchronized(i) { i = temp + 5 }
}

如果一个变量被声明为volatile,那么对它的每个原子访问都是同步的,但并不总是明显哪些访问符合原子访问的条件。使用Atomic*对象可以保证每个方法都是“原子的”。

因此,如果你使用AtomicIntegergetAndAdd(int delta),你可以确信结果是10。同样地,如果两个线程同时对一个boolean变量进行取反操作,在使用AtomicBoolean时可以确保其具有原始值,而在使用volatile boolean时则不能。

所以,无论何时当你有超过一个线程修改一个字段时,你需要使其成为原子的或者使用显式同步。

volatile的目的不同。考虑以下例子:

volatile boolean stop = false;
void loop() {
    while (!stop) { ... }
}
void stop() { stop = true; }

如果你有一个正在运行 loop() 的线程,还有另一个线程调用 stop(),如果不加 volatile,则可能会遇到无限循环的问题,因为第一个线程可能会缓存 stop 的值。在这里,volatile 作为一种提示,告诉编译器要更加小心地进行优化。


101
你举了例子,但并没有真正解释volatile和Atomicxxxx之间的区别。 - Jason S
91
问题不在于volatile。问题在于volatile booleanAtomicBoolean之间的区别。 - dolmen
37
这个问题明确要求关于布尔类型的解释,这与其他数据类型有所不同,应该直接进行解释。 - John Haager
8
这与Java 5的同步有关。 - Man of One Way
7
如果一个布尔值被多个线程读取,但只由一个线程写入,则volatile boolean就足够了。如果有多个写入者,您可能需要使用AtomicBoolean - StvnBrkdll
显示剩余11条评论

61

除非你使用同步,否则无法将compareAndSetgetAndSet 作为具有原子性操作的volatile boolean


7
可以,但布尔值这种情况似乎相当罕见。 - Robin
2
@Robin 考虑使用它来控制初始化方法的惰性调用。 - Ustaman Sangat
1
实际上,我认为这是主要的使用情况之一。 - fool4jesus

47

AtomicBoolean提供了一些可以原子性地执行复合操作的方法,而无需使用 synchronized 块。另一方面,只有在 synchronized 块中才能执行 volatile boolean 的复合操作。

读写 volatile boolean 的内存效果与分别调用 AtomicBooleangetset 方法相同。

例如,compareAndSet 方法将原子性地执行以下操作(无需 synchronized 块):

if (value == expectedValue) {
    value = newValue;
    return true;
} else {
    return false;
}
因此,compareAndSet方法将允许您编写一次性执行的代码,即使在多个线程中调用也可以保证。例如:
final AtomicBoolean isJobDone = new AtomicBoolean(false);

...

if (isJobDone.compareAndSet(false, true)) {
    listener.notifyJobDone();
}

保证只向监听器通知一次(假设没有其他线程将AtomicBoolean设置回false,在其被设置为true后)。


@Android开发者,我的回答没有提到任何性能问题!您可以澄清一下哪一部分回答让您产生了这种想法吗?通常,如果CPU支持,易失性变量会使用内存栅栏指令来实现,而不是同步/锁定。这与AtomicXXX类使用compare-and-set或load-linked store-conditional指令的方式类似,如果CPU支持的话。 - Nam San
抱歉,我认为我把它写到了错误的地方。无论如何,在性能方面,如果您只是使用它们进行get&set(不使用CAS),哪个更好(volatile vs Atomic)? - android developer
1
如果您查看源代码,您会发现AtomicBoolean的get()方法是通过读取一个volatile int字段并将其与0进行比较来实现的,而set()方法是通过向volatile int字段写入0或1来实现的。因此,在AtomicBooleans和volatile布尔值之间,实际读取或写入的性能将非常相似,如果不是完全相同的话。如果JIT无法优化它们,则AtomicBoolean将具有额外的函数调用和比较开销。如果您有很多,则在内存和GC方面,volatile布尔值将更有效率。 - Nam San
所以如果这是情况的话,使用volatile会稍微好一点。好的,谢谢。 - android developer

17

volatile boolean与AtomicBoolean

Atomic*类封装了相同类型的volatile原语。从源代码来看:

public class AtomicLong extends Number implements java.io.Serializable {
   ...
   private volatile long value;
   ...
   public final long get() {
       return value;
   }
   ...
   public final void set(long newValue) {
       value = newValue;
   }

如果你只是在获取和设置Atomic*,那么最好还是用一个volatile字段。

AtomicBoolean有什么功能是volatile boolean无法实现的吗?

Atomic*类提供了更高级的方法,比如数字的incrementAndGet()、布尔值的compareAndSet(),以及其他实现多个操作(获取/递增/设置、测试/设置)而无需锁定的方法。这就是Atomic*类如此强大的原因。

例如,如果多个线程使用下面的代码进行递增操作,由于++实际上是获取、递增和设置操作,所以会产生竞态条件。

private volatile value;
...
// race conditions here
value++;

然而,在多线程环境下,以下代码可以安全地工作而不需要锁:

private final AtomicLong value = new AtomicLong();
...
value.incrementAndGet();

同时需要注意的是,使用Atomic*类来封装易变字段是从对象角度将关键共享资源封装起来的好方法。这意味着开发人员不能仅仅处理该字段,假设它不被共享,可能会通过field ++;或其他引入竞争条件的代码来注入问题。


16

volatile 关键字确保共享该变量的线程之间存在 happens-before 关系。它不能保证在访问该布尔变量时 2 个或更多线程不会相互中断。


17
在Java中,布尔类型的访问(指原始类型)是原子性的,包括读取和赋值操作。因此,没有其他线程会“中断”布尔运算。 - Maciej Biłas
2
抱歉,但这个回答如何解决问题?一个Atomic*类封装了一个volatile字段。 - Gray
CPU缓存不是设置易失性的主要因素吗?以确保读取的值实际上是最近设置的值。 - jocull

8
这里很多答案过于复杂、令人困惑或者是错误的。例如:
如果你有多个线程修改布尔值,你应该使用一个AtomicBoolean。
这是不正确的一般性陈述。
如果一个变量是volatile,每次访问都是同步的……
这也是不正确的;同步是另外一回事。
简单来说,AtomicBoolean允许你在某些需要读取某个值并根据所读取的值写入值的操作中预防竞态条件;它使这些操作成为原子操作(即移除了变量在读取与写入之间可能发生改变的竞态条件)—由此得名。
如果你只是读取和写入变量而且写入并不依赖于你刚刚读取的某个值,那么即使使用多个线程,volatile也可以很好地工作。

7

记住这个成语 - 阅读 - 修改 - 写入,使用 volatile 无法实现。


3
简洁明了,volatile 只在所有者线程有能力更新字段值并且其他线程只能读取的情况下才起作用。 - Arefe
2
@ChakladerAsfakArefe 不行!正如MoveFast所说,你唯一不能做的就是读取+修改+写入。因此,例如,在工作线程中有一些volatile boolean keepRunning = true;,两个不相关的线程可以调用worker上的取消方法来设置keepRunning = false;,并且工作线程将正确地获取到最新写入的值。唯一无法实现的是类似于keepRunning = !keepRunning;这样的读取-修改-写入操作。 - Falkreon
澄清一下:你甚至可以有一个“取消取消”方法,将 keepRunning = true;。 “没有什么是真实的,一切皆有可能” :) - Falkreon

7

如果有多个线程访问类级变量,每个线程可以在其本地缓存中保存该变量的副本。

将变量设置为volatile将防止线程在本地缓存中保存变量的副本。

原子变量不同,它们允许对其值进行原子修改。


5

如果只有一个线程修改您的布尔变量,则可以使用volatile布尔变量(通常这样做是为了定义在线程的主循环中检查的“stop”变量)。

但是,如果有多个线程修改该布尔变量,则应使用AtomicBoolean。否则,以下代码是不安全的:

boolean r = !myVolatileBoolean;

这个操作包含两个步骤:

  1. 读取布尔值。
  2. 写入布尔值。

如果在 #1#2 之间,另一个线程修改了该值,则您可能会得到错误的结果。使用 AtomicBoolean 方法可以通过原子地执行步骤 #1#2 来避免此问题。


2
如果您只有一个线程修改布尔值,您可以使用一个volatile boolean。但是,如果您只使用一个线程,为什么需要volatile呢?您应该删除第一段以使答案更好。 - minsk
@minsk 一个线程写入,一个或多个线程读取。如果按照这种模式进行操作,您将不会遇到线程问题,但这并不完全正确;请参阅我对MoveFast答案的评论。 - Falkreon
@Falkreon,即使使用该模式,您仍然可能遇到问题。即可见性问题,其中一个或多个读取线程将无法“看到”写入线程更新的结果。这实际上就是volatile解决的问题。 - cmhteixeira
@CarlosTeixeira 这正是我的意思。volatile boolean可以安全地写入,但不能否定;否定是一种读-修改-写的循环。但只有 myVolatileBool = false;是线程安全的——因为这就是volatile的作用,强制任何写操作都要写入同一堆内存,并强制任何读操作来自堆内存。 - Falkreon

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