29

来自 JavaAtomicReferenceFieldUpdater文档

请注意,compareAndSet此类中方法的保证比其他原子类中的要弱。因为这个类不能确保该字段的所有使用都适合原子访问的目的,所以它只能保证对 and 的其他调用的原子性和易失性compareAndSet语义set

这意味着我不能与 一起进行正常的易失性写入compareAndSet,但必须set改用。它没有提到任何关于get.

这是否意味着我仍然可以读取具有相同原子性保证的 volatile 字段 - 之前的所有写入setcompareAndSet对所有读取 volatile 字段的人都是可见的?

还是我必须在现场使用getAtomicReferenceFieldUpdater不是 volatile 读取?

如果您有参考资料,请张贴参考资料。

谢谢你。

编辑:

Java Concurrency in Practice中,他们唯一说的是:

更新程序类的原子性保证比常规原子类要弱,因为你不能保证底层字段不会被直接修改 - compareAndSet 和算术方法只保证使用原子字段更新程序方法的其他线程的原子性。

同样,没有提及其他线程应该如何读取这些易失性字段。

另外,我是否可以假设“直接修改”是常规的易失性写入?

4

3 回答 3

25

正如atomics的包文档中所解释的(通常,不是专门的更新程序):

原子访问和更新的记忆效应通常遵循 volatiles 的规则,[...]:

  • get具有读取volatile变量的记忆效应。
  • set具有写入(分配)volatile变量的记忆效应。
  • [...]
  • compareAndSet以及所有其他读取和更新操作,例如getAndIncrement具有读取和写入volatile变量的记忆效应。

原子compareAndSet试图解决什么问题?为什么使用(例如)atomicInteger.compareAndSet(1,2)而不是if(volatileInt == 1) { volatileInt = 2; }?它并没有试图解决并发读取的任何问题,因为这些已经由常规的volatile. (“易失性”读取或写入与“原子”读取或写入相同。并发读取只有在写入过程中发生,或者语句以某种有问题的方式重新排序或优化时才会成为问题;但volatile已经阻止了这些事情。)唯一compareAndSet解决的问题是,在该方法中,在我们读取()和写入()之间volatileInt,其他一些线程可能会同时进行写入volatileIntvolatileInt == 1volatileInt = 2compareAndSet通过在此期间锁定任何竞争性写入来解决此问题。

AtomicReferenceFieldUpdater在“更新程序”(等)的特定情况下也是如此:volatile 读取仍然只是桃色。更新者compareAndSet方法的唯一限制是,不像我上面写的那样“锁定任何竞争性写入”,它们只锁定来自同一实例的AtomicReferenceFieldUpdater竞争性写入;volatile当您同时直接更新字段时(或者,就此而言,当您同时使用多个AtomicReferenceFieldUpdaters 更新同一volatile字段时)它们无法保护您。(顺便说一句,这取决于您如何看待它 -AtomicReference及其亲属也是如此:如果您以绕过他们自己的 setter 的方式更新他们的字段,他们无法保护您。不同之处在于AtomicReference实际上拥有它的字段,并且它是private,因此无需警告您不要以某种方式通过外部方式对其进行修改。)

因此,回答您的问题:是的,您可以继续读取volatile具有相同原子性保证的字段,以防止部分/不一致的读取、对语句重新排序等。


编辑添加(12 月 6 日):对这个主题特别感兴趣的任何人都可能会对下面的讨论感兴趣。我被要求更新答案以澄清该讨论的要点:

  • 我认为最重要的一点是,以上是我自己对文档的解释。我相当有信心我已经正确理解了它,并且没有其他解释是有意义的;如果需要,我可以详细讨论这一点;-);但是我和其他任何人都没有对任何权威文档提供任何参考,这些文档比问题本身中提到的两个文档(类的 Javadoc 和Java Concurrency in Practice)和我的原始答案中提到的一个文档更明确地解决了这一点它在上面(包的 Javadoc)。

  • 我认为,下一个最重要的一点是,尽管 for 的文档AtomicReferenceUpdatercompareAndSet与 volatile 写入混合是不安全的,但我相信在典型平台上它实际上安全的。仅在一般情况下才不安全。我这样说是因为包文档中的以下评论:

    这些方法的规范使实现能够使用现代处理器上可用的有效机器级原子指令。但是在某些平台上,支持可能需要某种形式的内部锁定。因此,这些方法不能严格保证是非阻塞的——线程可能会在执行操作之前暂时阻塞。

    所以:

    • 在现代处理器的典型 JDK 实现中,AtomicReference.set仅使用 volatile 写入,因为AtomicReference.compareAndSet使用相对于 volatile 写入的原子性比较和交换操作。AtomicReferenceUpdater.set必然比 更复杂AtomicReference.set,因为它必须使用类似反射的逻辑来更新另一个对象中的字段,但我认为这是它更复杂的唯一原因。一个典型的实现调用Unsafe.putObjectVolatile,这是一个较长名称的 volatile 写入。
    • 但并非所有平台都支持这种方法,如果不支持,则允许阻塞。冒着过度简化的风险,我粗略地认为原子类compareAndSet可以通过(或多或少)应用于直接使用的synchronized方法来实现。但是为了使它起作用,也必须是,原因在我上面的原始答案中解释过;也就是说,它不能只是一个 volatile 写入,因为它可以在has call 之后但在calls之前修改字段。getsetsetsynchronizedcompareAndSetgetcompareAndSetset
    • 不用说,我最初的答案对“锁定”一词的使用不应该从字面上理解,因为在典型的平台上,不会发生非常像锁定的需求。
  • 在 Sun 的 JDK 1.6.0_05 实现中java.util.concurrent.ConcurrentLinkedQueue<E>,我们发现:

    private static class Node<E> {
        private volatile E item;
        private volatile Node<E> next;
        private static final AtomicReferenceFieldUpdater<Node, Node> nextUpdater =
            AtomicReferenceFieldUpdater.newUpdater(Node.class, Node.class, "next");
        private static final AtomicReferenceFieldUpdater<Node, Object> itemUpdater =
            AtomicReferenceFieldUpdater.newUpdater(Node.class, Object.class, "item");
        Node(E x) { item = x; }
        Node(E x, Node<E> n) { item = x; next = n; }
        E getItem() { return item; }
        boolean casItem(E cmp, E val)
            { return itemUpdater.compareAndSet(this, cmp, val); }
        void setItem(E val) { itemUpdater.set(this, val); }
        Node<E> getNext() { return next; }
        boolean casNext(Node<E> cmp, Node<E> val)
            { return nextUpdater.compareAndSet(this, cmp, val); }
        void setNext(Node<E> val) { nextUpdater.set(this, val); }
    }
    

    (注意:为紧凑性调整了空格),其中,一旦构造了一个实例,就没有易失性写入——也就是说,所有写入都是通过AtomicReferenceFieldUpdater.compareAndSetAtomicReferenceFieldUpdater.set——但易失性读取似乎可以自由使用,而无需一次调用AtomicReferenceFieldUpdater.get. JDK 1.6 的后续版本被更改为Unsafe直接使用(Oracle 的 JDK 1.6.0_27 发生了这种情况),但 JSR 166 邮件列表上的讨论将此更改归因于性能考虑,而不是对先前实现的正确性的任何疑虑。

    • 但我必须指出,这不是万无一失的权威。为方便起见,我将“Sun 的实现”写成好像它是一个单一的东西,但我之前的要点清楚地表明,不同平台的 JDK 实现可能必须以不同的方式做事。在我看来,上面的代码似乎是以平台中立的方式编写的,因为它避免了普通的 volatile 写入,而是支持调用AtomicReferenceFieldUpdater.set; 但是不接受我对这一点的解释的人可能不会接受我对另一点的解释,并且可能会争辩说上述代码并不意味着对所有平台都是安全的。
    • 该权限的另一个弱点是,虽然Node似乎允许 volatile 读取与调用 同步发生AtomicReferenceFieldUpdater.compareAndSet,但它是一个私有类;而且我没有提供任何证据证明其所有者 ( ConcurrentLinkedQueue) 确实在没有采取预防措施的情况下拨打了此类电话。(但虽然我没有证明这一说法,但我怀疑有人会对此提出异议。)

有关本附录的背景信息和进一步讨论,请参阅以下评论。

于 2011-11-29T23:12:32.583 回答
2

这不是问题的确切答案:

从文档中看,解释和意图都不清楚。如果这个想法是绕过全局排序,也就是在允许它[如 IBM Power 或 ARM] 的架构上的 volatile 写入,并且只在没有防护的情况下公开 CAS(LoadLinked/StoreCondition)行为,那将是一项非常惊人的努力和混乱的根源。

sun.misc.Unsafe 的 CAS 没有规范或顺序保证(就像以前发生的那样),但 java.util.atomic... 有。所以在较弱的模型 java.util.atomic impl 上。在这种情况下,将需要必要的栅栏来遵循 Java 规范。

假设 Updater 类实际上没有围栏。如果他们这样做,字段的易失性读取(不使用 get)将返回更新值,即显然get()不需要。由于不会有订购保证,以前的商店可能不会传播(在弱模型上)。在 x86/Sparc TSO 硬件上确保 java 规范。

但是,这也意味着 CAS 可以通过以下非易失性读取重新排序。队列中有一个有趣的注释:java.util.concurrent.SynchronousQueue

        // Note: item and mode fields don't need to be volatile
        // since they are always written before, and read after,
        // other volatile/atomic operations.

所有提到的原子操作都是 AtomicReferenceFieldUpdater 的 CAS。这意味着在正常读写和 AtomicReferenceFieldUpdater.CAS 之间缺少或重新排序,即表现得像 volatile write。

        s.item = null;   // forget item
        s.waiter = null; // forget thread

        //....

        while ((p = head) != null && p != past && p.isCancelled())
            casHead(p, p.next);

只是 CAS,没有易失性写入。

鉴于上述条件,我得出的结论是 AtomicXXXFieldUpdater 公开与其 AtomicXXX 对应项相同的语义。

于 2011-11-30T02:06:22.800 回答
2

这意味着对对象的引用将得到保证,但是因为您可以使用任何对象,所以当另一个线程访问该对象时,该对象的字段可能无法正确写入。

唯一可以保证的方法是这些字段是最终的还是易变的。

于 2011-11-25T00:25:00.180 回答