【问题标题】:Why can't AtomicBoolean be a replacement for Boolean?为什么 AtomicBoolean 不能替代 Boolean?
【发布时间】:2016-08-24 19:16:30
【问题描述】:

AtomicBoolean 的 Oracle JDK Javadoc 声明:

https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/atomic/AtomicBoolean.html

可以自动更新的布尔值。见 java.util.concurrent.atomic 包规范的描述 原子变量的属性。 AtomicBoolean 用于 应用程序,如原子更新的标志,不能用作 替换布尔值。

我和一位同事试图找出 AtomicBoolean 不能替代的用例,我们唯一能想到的是 Boolean 对象具有 AtomicBoolean 没有的方法。

这是唯一的原因,还是在写这篇文章时有其他想法?

【问题讨论】:

  • Boolean 是不可变的;根据定义,AtomicBoolean 不是。
  • 我在任何答案中都没有看到的一件事:性能。任何原子和线程安全的东西总是比它的“香草”替代品性能低。
  • 是的,我同意@DavidGrinberg - AtomicBoolean 比直接布尔值或原始布尔值慢。有时候,你真的不在乎额外的东西
  • 注意:此页面上的所有内容均等效于AtomicInteger vs IntegerAtomicLong vs Long
  • @David 好吧,这只是因为 Java 需要类包装器(另一方面,Scala 在某种程度上已经允许值类型)。 struct AtomicInteger 应该具有与在其他语言中可以看到的原语完全相同的性能。这并没有改变这本来就是一个坏主意的事实:AtomicBoolean documents 程序员的意图。它应该很少使用,因此当您看到它时会脱颖而出。出于相同和其他原因,C++ 使用 std::atomic,尽管它们可以轻松扩展 bool 而不会出现性能问题。

标签: java api-design


【解决方案1】:

Boolean 是一个不可变的值对象。它被设计为不变并最终确定以强制执行。 java.lang.Boolean 从 1.0 开始就存在了。

AtomicBoolean 是可变的,旨在进行更新,以便更新后的值在线程间可见。 AtomicBoolean 是在 Java 5 中引入的。

这些是完全不同的概念,这就是为什么 AtomicBoolean 不是为了扩展 Boolean 而设计的。如果不破坏使用它的代码的预期不变量,就不能用可变对象替换不可变对象。如果可以将原子版本传入其位置,则期望接收不可变值的代码可能会被破坏。

所以这是一个用例:如果 AtomicBoolean 被引入作为可替代 Boolean 的东西,您可能会遇到这样一种情况,即在此更改之前创建的类可以合理地期望在某些返回 Boolean 的方法中它不需要由于布尔值是不可变的,因此通过防御性副本。如果返回的引用恰好是从更改为使用 AtomicBoolean 而不是 Boolean 的源初始化的,那么现在可以通过调用返回 Boolean 的方法来修改该字段,方法是将其转换为 AtomicBoolean。

原子类设计用于处理并发更新(作为对volatile 的改进),但设计并发代码的最有效方法是使用不可变值。所以请注意不要将 AtomicBoolean 误认为是“编写多线程代码时使用的布尔值”。

【讨论】:

  • @JimmyB 不是真的,这不是它们不可替代的唯一原因。
  • 我会将此表述为AtomicBoolean 是对布尔值的原子引用。它存在的唯一原因是AtomicReference<boolean> 不是合法类型,而AtomicReference<Boolean> 有开销(语义略有不同)。
【解决方案2】:

Boolean 是原语boolean 的包装类。它可以由编译器从boolean 自动创建(装箱转换)或转换为布尔值(拆箱转换)。 AtomicBoolean 不是这种情况,它是一个为并发目的而设计的单独类。

因此这两个类在语言级别具有不同的语义:

Boolean b = new Boolean(true);
AtomicBoolean ab = new AtomicBoolean(true);
System.out.println(true == b);  // automatic unboxing of Boolean variable
System.out.println(true == ab);  // compiler error

【讨论】:

  • 这与 Dave 的回答一起为我们回答了我们的问题。感觉就像试图决定向谁捐款......亿万富翁或亿万富翁,所以最终归结为解释性,我首先看到了这个。
【解决方案3】:

它们不能自动装箱,因此它们不能用于条件句中,例如,

// Explodey
if (someAtomicBoolean) {
}

【讨论】:

  • 当然,但是为什么它不是自动装箱的?
  • @JimmyB 因为它不是布尔值?因为那是他们编写 Java 的方式。问题是为什么它不能用作布尔值?这(和子类化的东西)就是原因。只有语言设计者和实现者才能提供他们的推理。从技术上讲,您可以为任何东西定义自动装箱规则,例如,自定义应用程序类可以具有自动装箱为布尔表达式的机制。为什么Java不这样做?因为 Java 不这样做。
  • 这不是一个有效的论点。问题仍然存在,他们为什么不把AtomicBoolean 变成Boolean。 Nathan Hughes 给出了恰当的答案:因为他们表现不同,并且不能以相同的方式处理而不可能产生冲突。 - 请注意,自动拆箱仅适用于 Java 中的不可变类型。
  • @JimmyB 它们不可替代的两个原因是正交的。我们正在回答的来自 OP 的问题与“找出] AtomicBoolean 不能替代的用例有关”。我提供了一个。 是被问到的问题,而不是“为什么 Java 是这样写的”。
  • 当我专注于“我们唯一能想到的是 [...] Boolean [can do things] AtomicBoolean [cannot]”时,也许我过度解释了这个问题。这个想法确实正确与否,取决于您是否将 immutability 视为附加功能。所以正确的答案是:区别在于“不变性”,因为这既不比“可变性”多也不少于“可变性”,结论就是两者根本不兼容。
【解决方案4】:

例子:

void doSomething( final Boolean flag ) {


  final boolean before = flag.booleanValue();

  do0( flag );

  final boolean after = flag.booleanValue();

  assert before == after;



  if ( flag.booleanValue() ) {
    do1();
  }

  if ( flag.booleanValue() ) {
    do2();
  }

}

可以给出不同的结果

void doSomething( final AtomicBoolean flag ) {


  final boolean before = flag.get();

  do0( flag );

  final boolean after = flag.get();

  assert (before == after) || (before != after);



  if ( flag.get() ) {
    do1();
  }

  if ( flag.get() ) {
    do2();
  }

}

因为AtomicBoolean 可以更改其值,而Boolean 不能。

在第一种情况下,do1()do2() 要么都被调用,要么都不被调用。

在第二种情况下,如果AtomicBoolean 的值同时被修改,则可能会同时调用这两个、任何一个或一个都不调用。

因为Boolean 一直存在,并且总是被定义为不可变的,所以很晚才引入的AtomicBoolean 不能替代Boolean,因为它的行为不同并且代码正确地依赖于如果不可变性被破坏,Boolean 可能会中断。

请注意,Boolean 不能替代 AtomicBoolean反之亦然。它们只是在语义上不兼容。

【讨论】:

  • AtomicBoolean 的并发“功能”并不是我们真正想要的,而是尝试在非并发应用程序中使用 AtomicBoolean 而不是 Boolean 的含义(尽管非并发诚然,申请不是原始问题的一部分)。
  • Ok :) 但是可怜的 Java 编译器应该如何知道您实际上不会在您的特定应用程序中同时使用 AtomicBoolean 呢?它不能,因此它不能让您使用AtomicBoolean 而不是Boolean,反之亦然。
  • @JimmyB 自动装箱本身不存在并发问题;它只会调用get(),就像您手动执行的方式一样,例如自动装箱代码如何调用java/lang/Boolean.booleanValue
  • @DaveNewton 对不起,我不应该在示例中使用自动拆箱,因为它显然会分散实际的注意力;我编辑了帖子。
  • 不过,自动拆箱的合同只为 immutable 对象定义。所以一个 mutable 对象不能在不破坏装箱语义的情况下拆箱。
【解决方案5】:

因为Boolean 是不可变的。请参阅:Why Wrapper class like Boolean in java is immutable? 和我的回答:


因为2 是2。明天就不是3了。

不可变总是首选作为默认值,尤其是在多线程情况下,它使代码更易于阅读和更易于维护。例如:Java Date API,它充满了设计缺陷。如果Date 是不可变的,那么 API 将会非常精简。我知道Date 操作会创建新日期,并且永远不必寻找修改它们的 API。

阅读实践中的并发,了解不可变类型的真正重要性。

但还要注意,如果出于某种原因您想要可变类型,请使用AtomicInteger AtomicBoolean 等。为什么要使用Atomic?因为通过引入可变性,您引入了对线程安全的需求。如果您的类型保持不可变,您将不需要它,因此在使用可变类型时,您还必须为考虑线程安全和使用 concurrent 包中的类型付出代价。欢迎来到并发编程的美妙世界。

另外,对于Boolean,我挑战你说出一个你可能想要执行的操作,它关心布尔值是否可变。设置为真?使用myBool = true。那是重新分配,而不是突变。否定? myBool = !myBool。同样的规则。请注意,不变性是一个特性,而不是一个约束,所以如果你可以提供它,你应该 - 在这些情况下,你当然可以。

请注意,这也适用于其他类型。整数最微妙的是count++,但这只是count = count + 1,除非您关心以原子方式获取值......在这种情况下使用可变的AtomicInteger

【讨论】:

    【解决方案6】:

    任何语言中原子操作(有时被封装)的第一个主要用例是比较和交换语义 - 并发应用程序的基本构建块。

    第二个主要用途是隐藏正确放置内存栅栏的复杂性,这是内存模型语义所必需的。

    在 Java Atomic* 中封装了上述两者,前者 - 使用特定于平台的本机代码,后者借助 volatile 关键字。

    【讨论】:

    • 我没有投反对票,但虚拟机可以模拟它想要的任何东西——这就是虚拟机的意义所在。
    • @DaveNewton - 不是虚拟机,而是编译器,它无法读懂程序员的心思,因此对自动检测if (!x) { dosomething; x = true;} 竞争条件没有任何作用。
    • 我的意思是我不同意“比较和交换是不可能在软件中模拟的”这样一个笼统的说法——显然这是可能的,例如,用于具有一条 CaS 指令。
    • @DaveNewton 我重新表述了第一段
    • @JimmyB - 你看到synchronizedin AtomicBoolean.java了吗?
    猜你喜欢
    • 2022-07-05
    • 2011-06-20
    • 2016-06-21
    • 2012-11-26
    • 2020-01-27
    • 2015-06-27
    • 2018-08-16
    • 2015-11-29
    相关资源
    最近更新 更多