当 set 在 Java 中已经是原子的时,为啥我们需要 compareAndSet?

Posted

技术标签:

【中文标题】当 set 在 Java 中已经是原子的时,为啥我们需要 compareAndSet?【英文标题】:Why do we need to compareAndSet when set is already atomic in Java?当 set 在 Java 中已经是原子的时,为什么我们需要 compareAndSet? 【发布时间】:2016-06-09 03:40:11 【问题描述】:

因为原子意味着线程安全。当 .set() 本身在 java 中是原子和线程安全的时候,我们什么时候使用 compareAndSet?

例如,我想以原子方式设置一个变量,以便每个其他线程都可以看到它(但我希望以线程安全的方式设置该变量)我可以简单地将其声明为 volatile AtomicBoolean 或 volatile AtomicInteger 并且应该好吗?在哪些情况下我需要使用 compareAndSet?

【问题讨论】:

例如,如果您想将值设置为100当且仅当当前值是300,您会怎么做。您无法读取该值并自己进行比较,因为在您比较期间,其他线程可能已经更新了该值。 线程安全不是可组合。即,完全用线程安全的类构建东西将保证您构建的东西是线程安全的。如果您的程序天真地执行原子比较操作,然后执行原子集合操作,则该 sequence 操作不是原子的。 【参考方案1】:

多线程环境中有两个重要的概念。

    原子性 可见性

Volatile 解决了可见性问题,但它不处理原子性,例如我++。这里 i++ 不是一条机器指令,而是三条机器指令。

    将值复制到寄存器 增加它 放回原处

AtomicIntegerAtomicReference 基于比较和交换指令。 CAS 具有三个操作数,即要对其进行操作的内存位置 V、预期的旧值 A 和新值 B。CAS 原子地将 V 更新为新值 B,但前提是 V 中的值与预期的旧值 A 匹配;否则它什么也不做。在任何一种情况下,它都会返回当前在 V 中的值。JVM 在AtomicIntegerAtomicReference 中使用该函数,如果底层处理器不支持此功能,则 JVM 将通过自旋锁实现它。 .

Set 是原子的(并不总是正确的),但是比较然后 set 不是原子的。因此,当您对此 有要求时,例如当值为 X 时,只需更改为 Y,因此要以原子方式执行此操作,您需要这种原语,您可以使用 AtomicIntegerAtomicReference 的 compareAndSet,例如atomicLong.compareAndSet(long expect, long update)

您实际上可以使用这些原语来开发强大的数据结构,例如并发堆栈。

import java.util.concurrent.atomic.AtomicReference;

public class MyConcurrentStack<T> 

    private AtomicReference<Node> head = new AtomicReference<Node>();

    public MyConcurrentStack() 
    

    public void push(T t) 
        if (t == null) 
            return;
        
        Node<T> n = new Node<T>(t);
        Node<T> current;

        do 
            current = head.get();
            n.setNext(current);
         while (!head.compareAndSet(current, n));
    

    public T pop() 
        Node<T> currentHead = null;
        Node<T> futureHead = null;
        do 
            currentHead = head.get();
            if (currentHead == null) 
                return null;
            
            futureHead = currentHead.next;
         while (!head.compareAndSet(currentHead, futureHead));

        return currentHead.data;
    

    /**
     *
     * @return null if no element present else return a element. it does not
     * remove the element from the stack.
     */
    public T peek() 
        Node<T> n = head.get();
        if (n == null) 
            return null;
         else 
            return n.data;
        
    

    public boolean isEmpty() 
        if (head.get() == null) 
            return true;
        
        return false;
    

    private static class Node<T> 

        private final T data;
        private Node<T> next;

        private Node(T data) 
            this.data = data;
        

        private void setNext(Node next) 
            this.next = next;
        
    

【讨论】:

是的,从我读到的 compareAndSet 也是原子的。另外我要问的是你什么时候使用 compareAndSet?有哪些典型用例? 所以 compareAndSet 不是原子的? 是的,我解释的是原子性的(它是如何通过 CAS 机器指令实现的),对吗? CAS 还有另一个更简单甚至更常见的用例:递增 int。如果您查看 incrementAndGet 的代码,您会在内部看到它uses a CAS loop。 除了可见性和原子性之外,另一个关键部分是排序。那么加载/存储是如何在全局内存顺序中相对于其他加载/存储进行排序的。【参考方案2】:

一个简单的写操作本质上是原子的(在大多数情况下)。 set() 没有什么特别之处。如果您查看source code 中的AtomicInteger.set(),您会看到:

public final void set(int newValue) 
    value = newValue;

原子类的神奇之处在于它们可以原子地读取和修改。在多线程环境中,如果您尝试使用简单的if 实现比较和设置逻辑,您可能会读取一个值,运行一些计算并尝试更新变量。但是在您的读取和写入操作之间,该值可能已被另一个线程更新,从而使您的计算无效。原子类确保在您的读取和写入之间没有任何内容。因此compareAndSet() 方法,以及getAndSet()getAndIncrement() 等。

【讨论】:

是 compareAndSet 原子的吗? @user1870400,它位于atomic classes。这就是重点。 为什么 set() 是原子的,如果 getAndSet() 被记录为原子而不像 set() cf @user1767316 原子意味着操作是不可分割的。写一个 int 总是一个单一的操作;因此原子性无关紧要。【参考方案3】:

当您需要更新 AtomicReference 中已经存在的一些数据,然后将其放回此引用时,您可以使用compareAndSet。

例如:您需要从多个线程中增加一个值(或以任何其他方式更新它)。

Number n = new Integer(10);
AtomicReference ref = new AtimicReference(n);

// later...
int i = ref.get().intValue(); // i == 10

// some other thread incrments the value in ref and now it is 11

ref.set(new Integer(i + 1));

// Oops! Now ref contains 11, but should be 12.

但是使用compareAndSet,我们可以原子地增加它。

Number n = new Integer(10);
AtomicReference ref = new AtimicReference(n);

// later...

boolean success = false;
do 
    Integer old = ref.get(); // old == 10 on first pass, 11 on second pass
    // On first pass some other thread incrments the value in ref and now it is 11
    Integer updated = new Integer(old + 1);
    success = ref.compareAndSet(old, updated);
    // On first pass success will be false and value in ref will not update
 while (!success);

// Now ref contains 12 if other thread increments it to 11 between get and set.

【讨论】:

【参考方案4】:

compareAndSet 是non-blocking algorithms 的基本原语。

例如,仅使用atomic read/write 来实现wait-free 算法基本上是不可能的——必须有compareAndSet 或类似的东西。

compareAndSetatomic read/write 强大得多。 功率用consensus number表示:

compareAndSet 的共识数为无穷大 atomic read/write 的共识编号为 1

共识数是并发对象可以解决consensus problem(线程提出它们的候选值并就单个共识值达成一致)在wait-free中的最大线程数(即没有阻塞,每个线程都有保证完成不超过一些固定的最大步骤数)实施。 事实证明,共识数为n的对象可以实现任何共识数为n或更低的对象,但不能实现任何共识数更高的对象。

更多解释可以在 M. Herlihy 和 N. Shavit 的“多处理器编程艺术”中找到。

【讨论】:

很好的答案....【参考方案5】:

Set 是原子的,用于设置新值。 compareAndSet 比较旧值,如果等于当前值,则设置新值。如果我们使用set 而不是compareAndSet

if(atomic.get().equals(12)) 
    atomic.set(13);

这不是线程安全的,因为它会导致竞争条件。执行结果取决于线程的时间和顺序。例如,当 thread1 获取值时,thread2 可以更改它。 check-and-act、read-modify-write 等复合操作必须以原子方式执行。

【讨论】:

以上是关于当 set 在 Java 中已经是原子的时,为啥我们需要 compareAndSet?的主要内容,如果未能解决你的问题,请参考以下文章

当标头是动态的时,避免批量数据导出到csv

为啥写入 24 位结构不是原子的(当写入 32 位结构时)?

为啥我看到多个 set-cookie 标头?

为啥原子操作需要独占缓存访问?

基础篇:JAVA原子组件和同步组件

java类中为啥设置set和get方法操作属性