如何在没有同步块的情况下在一个安全操作中原子地检查Java中的两个AtomicBooleans(即低成本锁)?

Joh*_*ine 10 java concurrency multithreading

所以我有两个AtomicBoolean,我需要检查它们.像这样的东西:

if (atomicBoolean1.get() == true && atomicBoolean2.get() == false) {

   // ...
}
Run Code Online (Sandbox Code Playgroud)

但是之间存在竞争条件:(

有没有办法在不使用同步(即同步块)的情况下将两个原子布尔检查合并为一个?

Rad*_*def 5

嗯,我可以想到几种方法,但这取决于您需要的功能。

一种方法是“作弊”并使用AtomicMarkableReference<Boolean>

final AtomicMarkableReference<Boolean> twoBooleans = (
    new AtomicMarkableReference<Boolean>(true, false)
);

void somewhere() {
    boolean b0;
    boolean[] b1 = new boolean[1];
    b0 = twoBooleans.get(b1);

    b0 = false;
    b1[0] = true;

    twoBooleans.set(b0, b1);
}
Run Code Online (Sandbox Code Playgroud)

但这有点痛苦,而且只能得到两个值。

那么你可以将 AtomicInteger 与位标志一起使用:

static final int FLAG0 = 1;
static final int FLAG1 = 1 << 1;

final AtomicInteger intFlags = new AtomicInteger(FLAG0);

void somewhere() {
    int flags = intFlags.get();

    int both = FLAG0 | FLAG1;
    if((flags & both) == FLAG0) { // if FLAG0 has a 1 and FLAG1 has a 0
        something();
    }

    flags &= ~FLAG0; // set FLAG0 to 0 (false)
    flags |=  FLAG1; // set FLAG1 to 1 (true)

    intFlags.set(flags);
}
Run Code Online (Sandbox Code Playgroud)

也有点痛苦,但它会给你 32 个值。如果您真的愿意,您可以围绕它创建一个包装类。例如:

public class AtomicBooleanArray {
    private final AtomicInteger intFlags = new AtomicInteger();

    public void get(boolean[] arr) {
        int flags = intFlags.get();

        int f = 1;
        for(int i = 0; i < 32; i++) {
            arr[i] = (flags & f) != 0;
            f <<= 1;
        }
    }

    public void set(boolean[] arr) {
        int flags = 0;

        int f = 1;
        for(int i = 0; i < 32; i++) {
            if(arr[i]) {
                flags |= f;
            }
            f <<= 1;
        }

        intFlags.set(flags);
    }

    public boolean get(int index) {
        return (intFlags.get() & (1 << index)) != 0;
    }

    public void set(int index, boolean b) {
        int f = 1 << index;

        int current, updated;
        do {
            current = intFlags.get();
            updated = b ? (current | f) : (current & ~f);
        } while(!intFlags.compareAndSet(current, updated));
    }
}
Run Code Online (Sandbox Code Playgroud)

那很好。也许在 get 中复制数组时会执行设置,但重点是您可以原子地获取设置所有 32 个值。(比较和设置 do-while 循环非常丑陋,但这就是原子类本身如何处理getAndAdd之类的事情。)

AtomicReference 在这里似乎不切实际。它允许原子获取和设置,但是一旦您掌握了内部对象,您就不再以原子方式更新。您每次都必须创建一个全新的对象。

final AtomicReference<boolean[]> booleanRefs = (
    new AtomicReference<boolean[]>(new boolean[] { true, true })
);

void somewhere() {
    boolean[] refs = booleanRefs.get();

    refs[0] = false; // not atomic!!

    boolean[] copy = booleanRefs.get().clone(); // pretty safe
    copy[0] = false;
    booleanRefs.set(copy);
}
Run Code Online (Sandbox Code Playgroud)

如果要以原子方式对数据执行临时操作(获取 -> 更改 -> 设置,无干扰),则必须使用锁或同步。就我个人而言,我会使用锁或同步,因为通常情况下,您想要保留整个更新。

** 不安全!**

不要这样做!

这可以(可能)通过 来完成sun.misc.Unsafe。这是一个使用 Unsafe 写入易失性长牛仔风格的两半的类。

public class UnsafeBooleanPair {
    private static final Unsafe UNSAFE;

    private static final long[] OFFS = new long[2];
    private static final long[] MASKS = new long[] {
        -1L >>> 32L, -1L << 32L
    };

    static {
        try {
            UNSAFE = getTheUnsafe();

            Field pair = UnsafeBooleanPair.class.getDeclaredField("pair");
            OFFS[0] = UNSAFE.objectFieldOffset(pair);
            OFFS[1] = OFFS[0] + 4L;

        } catch(Exception e) {
            throw new RuntimeException(e);
        }
    }

    private volatile long pair;

    public void set(int ind, boolean val) {
        UNSAFE.putIntVolatile(this, OFFS[ind], val ? 1 : 0);
    }

    public boolean get(int ind) {
        return (pair & MASKS[ind]) != 0L;
    }

    public boolean[] get(boolean[] vals) {
        long p = pair;
        vals[0] = (p & MASKS[0]) != 0L;
        vals[1] = (p & MASKS[1]) != 0L;
        return vals;
    }

    private static Unsafe getTheUnsafe()
    throws Exception {
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        return (Unsafe)theUnsafe.get(null);
    }
}
Run Code Online (Sandbox Code Playgroud)

重要的是,Open JDK 源代码中的 Javadoc表示fieldOffset不要偏移量进行算术运算。然而,用它做算术似乎确实有效,因为我没有得到垃圾。

这不仅可以对整个字进行一次易失性读取,还可以(可能)对其任意一半进行易失性写入。可能putByteVolatile可用于将 long 拆分为 8 段。

我不建议任何人使用这个(不要使用这个!),但它有点奇怪,很有趣。