【问题标题】:Why is there a loop in getAndIncrement() in Java AtomicInteger?为什么在 Java AtomicInteger 的 getAndIncrement() 中有一个循环?
【发布时间】:2017-03-13 15:48:04
【问题描述】:

getAndIncrement的源码为:

public final int getAndIncrement() {
   for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return current;
    }
}

我不明白为什么会有循环。如果其他一些线程改变了值,那它怎么可能是原子的?

假设值为5,那么我调用getAndIncrement(),我们期望它是6,但同时一些其他线程已经将值更改为6,那么getAndIncrement()会将值变为7,这不是预期的。

我哪里错了?

【问题讨论】:

  • @yshavit 你提到的链接没有回答我的问题,你能检查一下吗?谢谢!
  • @yshavit 那么为什么要重试呢?我不明白这一点。假设它的原始值是5,现在我想把它变成6,但是如果其他线程已经把它变成了6,它为什么要重试让它变成7?
  • 该方法是原子的,因为它不受竞争条件的影响并且是线程安全的。如果一个线程将其设置为 6 并且您调用 getAndIncrement 您会期望它返回 7。
  • @Lily 嗯,一个线程需要返回 7,对吧?如果你从 5 开始,两个线程以原子方式递增它,那么一个应该返回 6,而另一个返回 7。
  • 考虑这会做什么; public synchronized int getAndIncrement() { return value++; } 这有什么不同?

标签: java multithreading


【解决方案1】:

循环将继续进行,直到它设法执行get()+1compareAndSet,而没有任何其他线程首先进入 compareAndSet。如果另一个线程确实获得了 compareAndSet,则该线程的 compareAndSet 将失败,循环将重试。

最终结果是每次调用 getAndIncrement() 都会导致该值恰好增加一个。如果初始值为 5,两个线程调用 getAndIncrement(),则一个返回 6,另一个返回 7。

换一种说法:其中一个似乎完全在另一个之后发生,这就是“原子”的意思。

【讨论】:

  • 那么为什么要重试呢?我不明白这一点。假设它的原始值是5,现在我想把它变成6,但是如果其他线程已经把它变成了6,它为什么要重试让它变成7?
  • 你从 5 开始,增加了 2 个东西 - 最终一定是 7。
  • @Lily 如果你想要,它就不是原子的,也不是线程安全的。
  • 好东西。要指出的另一件重要的事情是循环的整个要点是避免synchronized 块。这是在尝试更新共享字段不必支付锁的开销。
【解决方案2】:

As already answered,

对 getAndIncrement() 的每次调用都会导致该值恰好增加一个

困惑似乎源于您的评论

假设它的原始值为5,现在我想将其设为6,但如果其他线程已设为6,为什么要重试设为7

好吧,所以希望系统以一种方式运行,但你使用的方法被设计成不同的。 getAndIncrement旨在确保every invocation causes an increment,你想要的是all invocations combined cause ONE increment。所以显然这里不应该使用 getAndIncrement。

值得注意的是,您期望的行为在单机系统中很少遇到,但在分布式系统中经常遇到。如果您不进行分布式,那么其他人在您的方法中发现错误是正确的。

【讨论】:

    【解决方案3】:

    理解这一点的关键是了解compareAndSet() 做了什么:

    /**
     * Atomically sets the value to the given updated value
     * if the current value {@code ==} the expected value.
     *
     * @param expect the expected value
     * @param update the new value
     * @return true if successful. False return indicates that
     * the actual value was not equal to the expected value.
     */
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
    

    Unsafe.java:

    /**
     * Atomically update Java variable to <tt>x</tt> if it is currently
     * holding <tt>expected</tt>.
     * @return <tt>true</tt> if successful
     */
    public final native boolean compareAndSwapInt(Object o, long offset,
                                                  int expected,
                                                  int x);
    

    所以这个方法使用JVM内部来原子地

    • 检查值是否有期望值
      • 如果不是,什么也不做,返回 false
      • 如果是,则设置为新值并返回 true

    compareAndSet() 返回 true 时,您找到的循环退出。

    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return current;
     }
    

    ... 等价于:

    boolean done = false;
    int current;
    while(!done) {
        current = get();
        int next = current + 1;
        done = compareAndSet(current, next);
    }
    return current;
    

    ...但稍微简洁明了。

    【讨论】:

      【解决方案4】:

      @Lily,正如@yshavit 解释的那样,compareAndSet 只有在当前仍然有效并且计数器没有被另一个线程更新时才会成功。所以它会自动更新计数器,否则它将返回 false。所以它将继续迭代,直到最终成功。每次迭代都会重新计算当前和下一个。所以它会准确地更新计数器 1 或根本不更新。

      这是乐观锁定的一种形式,这意味着它不会在其他线程必须检查它们是否可以继续或必须等待的情况下使用锁定,它根本不锁定,只是继续机会主义地尝试直到成功。理由是这比同步块更便宜,因为通常不需要这样做的开销,并且迭代和重试比锁定代码块更便宜。

      顺便说一句。在 Oracle java 8 中,实现发生了变化,它现在在内部使用sun.misc.Unsafe,这可能会调用一些本机逻辑来实现相同的目标。

      【讨论】:

        猜你喜欢
        • 2017-02-19
        • 2013-11-14
        • 1970-01-01
        • 2019-08-20
        • 1970-01-01
        • 2014-05-04
        • 1970-01-01
        • 1970-01-01
        • 2020-04-14
        相关资源
        最近更新 更多