【问题标题】:Why are two AtomicIntegers never equal?为什么两个 AtomicInteger 永远不相等?
【发布时间】:2011-09-27 10:15:42
【问题描述】:

我偶然发现了AtomicInteger 的来源并意识到了这一点

new AtomicInteger(0).equals(new AtomicInteger(0))

计算为false

这是为什么?它是与并发问题相关的一些“防御性”设计选择吗?如果是这样,如果采用不同的实施方式会出现什么问题?

(我确实意识到我可以改用 get==。)

【问题讨论】:

  • 我想知道 Doug Lea 出现并给我们答案的可能性有多大。
  • 相当苗条 :-) 我最近在暑期学校遇到了他,他开始说 “我不会费心去看那些不会影响数百万人的事情” i> :-)
  • @aioobe:很好,但我也尝试过==,但它不相等,因为我们检查的是引用而不是实际值,但很明显,在这种情况下对象的引用会有所不同if(new AtomicInteger(0)==(new AtomicInteger(0))){System.out.println("equals");} 不打印等于。

标签: java concurrency equals


【解决方案1】:

这部分是因为AtomicInteger 不是Integer 的通用替代品。

java.util.concurrent.atomicpackage summary 声明:

原子类不是通用的替代品 java.lang.Integer 和相关类。他们没有定义方法 例如hashCodecompareTo。 (因为原子变量是 预计会发生突变,它们是哈希表键的糟糕选择。)

hashCode 未实现,equals 也是如此。这部分是由于discussed in the mailing list archives 的更大理由,即AtomicInteger 是否应该扩展Number

AtomicXXX 类不是原语的直接替代品,并且它没有实现Comparable 接口的原因之一是因为在大多数情况下比较 AtomicXXX 类的两个实例是没有意义的.如果两个线程可以访问和改变AtomicInteger 的值,那么the comparison result is invalid before you use the result, if a thread mutates the value of an AtomicInteger。同样的原理也适用于 equals 方法 - 相等性测试的结果(取决于 AtomicInteger 的值)仅在线程改变有问题的 AtomicIntegers 之一之前有效。

【讨论】:

    【解决方案2】:

    从表面上看,这似乎是一个简单的省略,但实际上只使用 Object.equals 提供的 identity equals 可能确实有意义

    例如:

    AtomicInteger a = new AtomicInteger(0)
    AtomicInteger b = new AtomicInteger(0)
    
    assert a.equals(b)
    

    看起来很合理,但b 并不是真正的a,它被设计为一个值的可变持有者,因此不能真正替换程序中的a

    还有:

    assert a.equals(b)
    assert a.hashCode() == b.hashCode()
    

    应该可以,但是如果 b 的值在两者之间发生变化怎么办。

    如果这是因为它没有记录在AtomicInteger 的源代码中,那就太可惜了。

    顺便说一句:一个不错的功能也可能是允许AtomicInteger 等于一个整数。

    AtomicInteger a = new AtomicInteger(25);
    
    if( a.equals(25) ){
        // woot
    }
    

    麻烦的是,为了在这种情况下具有自反性,Integer 也必须接受 AtomicInteger

    【讨论】:

    • 我明白你的意思(这是一个很好的观点:-)其他可变类,如 ArrayList 确实实现了 equals
    • 我认为这是关键 - AtomicInteger 是显式可变的,因此强制开发人员显式比较所持有的值而不是整个对象更有意义。无论如何,对我来说很有意义。
    • @aioobe - 当两个引用指向同一个对象时,ArrayList.equals() 将返回 true。在这种情况下,OP 的期望行为是让 .equals() 在引用对象的内容相同时返回 true,无论对象实际上是否相同 - 我相信设计人员可能不认为这种行为是安全的.
    • 我不太明白最后的评论。查看 AbstractList 的 equals() 实现。它使用 equals 方法遍历两个列表,逐个元素地进行比较。然而 ArrayList 与 AtomicInteger 一样可变。我认为这是一个权衡。拥有 AtomicInteger.equals() 的便利性并没有超过由此产生的问题。但对于列表,情况正好相反。
    • 可变性(即时间)应该成为两个对象是否相等的一个因素是没有意义的。当您比较两个对象时,您显然想知道它们在那一刻是否相等,而不是永远:)
    【解决方案3】:

    我会争辩说,因为AtomicInteger 的要点是可以原子地完成操作,所以很难确保原子地比较这两个值,而且因为 AtomicIntegers 通常是计数器,所以你会得到一些奇怪的行为。

    因此,如果不确保 equals 方法是同步的,您将无法确定在 equals 返回时原子整数的值没有改变。但是,由于原子整数的全部意义在于不使用同步,因此您最终不会有什么好处。

    【讨论】:

    • 这肯定是答案。根本没有办法实现线程安全的值比较AtomicInteger.equals,而在 java.util.concurrent 中的类中放入非线程安全实现将是一件非常奇怪的事情!
    【解决方案4】:

    我怀疑比较值是不行的,因为没有办法以可移植的方式原子地进行比较(也就是说,没有锁)。

    如果没有原子性,那么变量可以比较相等,即使它们从未同时包含相同的值(例如,如果 a0 更改为 1b 更改的时间完全相同从10)。

    【讨论】:

      【解决方案5】:

      AtomicInteger 继承自 Object 而不是 Integer,它使用标准的引用相等检查。

      如果你谷歌你会发现this discussion of this exact case

      【讨论】:

        【解决方案6】:

        想象一下,如果 equals 被覆盖,你把它放在 HashMap 中,然后你改变了值。坏事会发生:)

        【讨论】:

        • 该参数适用于所有可变类。并且在实现 equals 的标准 API 中有很多。
        • @aioobe - 是的,但对于很多人来说AtomicInteger 就像Integer。我可以在地图上放一个Integer,那我为什么不也放一个AtomicInteger...这很危险。
        • That argument holds for all mutable classes. 仅当您将引用类型和值类型视为同一事物时。
        【解决方案7】:

        equals 不仅用于相等性,还用于满足其与hashCode 的约定,即在哈希集合中。哈希集合唯一安全的方法是让可变对象不依赖于它们的内容。即对于可变键,HashMap 与使用 IdentityMap 相同。这样当keys内容改变时,hashCode和两个对象是否相等不会改变。

        所以new StringBuilder().equals(new StringBuilder()) 也是假的。

        比较两个AtomicInteger的内容,需要ai.get() == ai2.get()或者ai.intValue() == ai2.intValue()

        假设您有一个可变键,其中 hashCode 和 equals 会根据内容发生变化。

        static class BadKey {
            int num;
            @Override
            public int hashCode() {
                return num;
            }
        
            @Override
            public boolean equals(Object obj) {
                return obj instanceof BadKey && num == ((BadKey) obj).num;
            }
        
            @Override
            public String toString() {
                return "Bad Key "+num;
            }
        }
        
        public static void main(String... args) {
            Map<BadKey, Integer> map = new LinkedHashMap<BadKey, Integer>();
            for(int i=0;i<10;i++) {
                BadKey bk1 = new BadKey();
                bk1.num = i;
                map.put(bk1, i);
                bk1.num = 0;
            }
            System.out.println(map);
        }
        

        打印

        {Bad Key 0=0, Bad Key 0=1, Bad Key 0=2, Bad Key 0=3, Bad Key 0=4, Bad Key 0=5, Bad Key 0=6, Bad Key 0=7, Bad Key 0=8, Bad Key 0=9}
        

        如您所见,我们现在有 10 个键,全部相等且具有相同的 hashCode!

        【讨论】:

        • 基本上你是在说“AtomicInteger 没有实现 equals,因为它不适用于基于哈希的集合”。美好的。 Java 库 do 中的其他可变类实现了 equals。这些与 AtomicInteger 和 StringBuilder 有什么区别?
        • 你能给我一些实现equals的可变类的例子吗?
        • 除了像Date 这样的类通常被认为设计不佳。 ;)
        • 有些可变类的 hashCode 和 equals 不依赖于可变字段。例如领域和方法。
        • ByteBuffer 和类似的类确实实现了 hashCode 和 equals。这样做的原因可能是因为类层次结构足够复杂而没有添加真正不可变的变体类型。
        【解决方案8】:

        equals 已正确实现:AtomicInteger 实例只能与自身相等,因为随着时间的推移,只有同一个实例可证明存储相同的值序列。

        请记住Atomic* 类充当引用类型(就像java.lang.ref.*),旨在包装一个实际的“有用”值。与函数式语言中的情况不同(参见例如 Clojure 的 Atom 或 Haskell 的 IORef),引用和值之间的区别在 Java 中相当模糊(归咎于可变性),但它仍然存在。

        将 Atomic 类的当前包装值视为相等标准显然是一种误解,因为它暗示 new AtomicInteger(1).equals(1)

        【讨论】:

          【解决方案9】:

          Java 的一个限制是,没有办法区分可以并且将要发生变异的可变类实例与永远不会暴露给任何可能改变它的东西的可变类实例(*)。对前一种事物的引用只有在它们引用同一个对象时才应该被认为是相等的,而对后一种事物的引用通常应该在引用具有等效状态的对象时被认为是相等的。因为 Java 只允许对虚拟 equals(object) 方法进行覆盖,所以可变类的设计者必须猜测是否有足够的实例满足后一种模式(即以永远不会发生变异的方式保存)来证明拥有 @987654322 是合理的@ 和 hashCode() 的行为方式适合这种用法。

          Date 之类的情况下,有很多类封装了对Date 的引用永远不会被修改,并且想要拥有自己的等价关系包含封装的Date 的值等价关系。因此,Date 覆盖 equalshashCode 以测试值等价是有意义的。另一方面,持有对永远不会被修改的AtomicInteger 的引用是愚蠢的,因为该类型的整个目的 都围绕着可变性。一个永远不会发生变异的AtomicInteger 实例,出于所有实际目的,可能只是一个Integer

          (*) 任何要求特定实例永不变异的要求只有在以下情况下才有约​​束力:(1) 关于其身份哈希值的信息存在于某处,或 (2) 对该对象的多个引用存在于宇宙中的某处。如果这两个条件都不适用于Foo 引用的实例,则将Foo 替换为Foo 的克隆引用将不会产生明显的影响。因此,可以通过假装用克隆替换Foo 并对“克隆”进行变异,从而在不违反实例“永不变异”的要求的情况下变异该实例。

          【讨论】:

          • 我喜欢这个答案,尽管它有点“极端”。我不会说“可以而且将要发生变异的可变类实例[...]只有在它们引用同一个对象时才应该被认为是相等的,”。如果对象在多个线程之间共享或用作哈希映射中的键,我可能会同意您的说法。然而,我可以想象许多单线程场景,其中使用 equals 比较可变对象是有意义的。
          • @aioobe:我认为尽可能将对象分类为代表实体或值是有帮助的。表示实体的对象只与自己相等,而表示值的对象与表示相同值的其他对象相等。从概念上讲,值应该是不可变的——将 1 添加到值 4 会生成一个新值(即 5),但不会改变值 4 的含义(它将始终继续表示 (1+1+1+1)。不幸的是,在 Java 中,每次创建新值时都创建一个新对象通常是不切实际的,...
          • ...所以像Date 这样的一些类型可以通过三种方式使用: (1) 创建Date 的私有实例并使用它来保存日期值;通过更改该实例来更改持有日期; (2) 使用对Date 实例的引用,所有持有者都同意不变异以持有日期值;通过存储对包含所需值的不同Date 的引用来更改日期值; (3) 使用Date 作为实体。在使用模式 #1 中,让equals() 比较值可能是合理的,但前提是人们对“私人持有”的态度足以使参考可用于比较。
          • @aioobe:我添加了一个关于“从不”的脚注。让我知道你的想法。在任何情况下,AtomicIntegers 仅作为实体才有意义,因此参考比较是合适的。
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2011-09-05
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2015-12-06
          相关资源
          最近更新 更多