【问题标题】:Setting an AtomicBoolean again再次设置 AtomicBoolean
【发布时间】:2019-04-25 09:01:22
【问题描述】:

我正在使用AtomicBoolean 来强制线程之间的volatile 可见性。一个线程正在更新值,另一个线程只是读取它。

假设当前值为true。现在说一个写线程再次将其值设置为true

final AtomicBoolean b = new AtomicBoolean(); // shared between threads

b.set(true);
// ... some time later
b.set(true);

在这个“虚拟”set(true) 之后,读取线程 调用get() 时是否会降低性能? 读取线程是否必须重新读取并缓存该值?

如果是这样的话,写线程可以做到:

b.compareAndSet(false, true);

这样,读取线程只需对真正的更改无效。

【问题讨论】:

    标签: java multithreading caching concurrency volatile


    【解决方案1】:

    compareAndSet():

    public final boolean compareAndSet(boolean expect, boolean update) {
        int e = expect ? 1 : 0;
        int u = update ? 1 : 0;
        return unsafe.compareAndSwapInt(this, valueOffset, e, u);
    }
    

    compareAndSwapInt() 已经是原生的:

    UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
      UnsafeWrapper("Unsafe_CompareAndSwapInt");
      oop p = JNIHandles::resolve(obj);
      jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
      return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
    UNSAFE_END
    

    其中Atomic::cmpxchggenerated 在JVM 执行开始时的某处

      address generate_atomic_cmpxchg() {
        StubCodeMark mark(this, "StubRoutines", "atomic_cmpxchg");
        address start = __ pc();
    
        __ movl(rax, c_rarg2);
       if ( os::is_MP() ) __ lock();
        __ cmpxchgl(c_rarg0, Address(c_rarg1, 0));
        __ ret(0);
    
        return start;
      }
    

    cmpxchgl() 生成 x86 代码(它也有更长的遗留代码路径,所以我不在这里复制那个):

     InstructionMark im(this);
     prefix(adr, reg);
     emit_byte(0x0F);
     emit_byte(0xB1);
     emit_operand(reg, adr);
    

    0F B1 真的是CMPXCHG 操作。如果你检查上面的代码,if ( os::is_MP() ) __ lock(); 在多处理器机器上会发出一个LOCK 前缀(让我跳过引用lock(),它会发出一个F0 字节),所以几乎无处不在。

    正如CMPXCHG 文档所说:

    该指令可以与 LOCK 前缀一起使用,以允许指令以原子方式执行。为了简化与处理器总线的接口,目标操作数接收一个写周期,而不考虑比较结果。如果比较失败,则写回目标操作数;否则,源操作数被写入目标。 (处理器不会在不产生锁定写入的情况下产生锁定读取。

    因此,在多处理器 x86 机器上,NOP-CAS 也会进行写入,从而影响高速缓存行。 (重点是我加的)

    【讨论】:

    • 感谢您的参考。 +1
    • TL;DR 结论:b.set(true)b.compareAndSet(false, true) 对阅读线程没有任何影响。在这两种情况下,它都必须重新加载值。
    【解决方案2】:

    写入和 CAS 都“触及”缓存行,触发缓存行变脏。

    但是成本相对较小,大约为 30 - 50 ns。

    由于尚未运行 10,000 次而未预热代码的成本可能要高得多。

    【讨论】:

    • 为什么空操作CAS 也会触及缓存行?例如compareAndSet(true, true).
    • @OhNoOh 可以,但在 Intel x64 上,它似乎没有。例如,我们在堆外内存上使用 compareAndSet(0L, 0L) 来拉入一个新页面而不改变它。即预先触摸它以减少按需创建页面的影响。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-07-25
    • 1970-01-01
    • 2015-01-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多