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



所以我有两个AtomicBoolean,我需要检查它们。类似的东西:

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

但两者之间存在竞争条件:(

有没有一种方法可以将两个原子布尔检查合并为一个,而不使用同步(即同步块)?

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

一种方法是"欺骗"并使用AtomicMarkableReference<布尔值>:

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);
}

但这是一种痛苦,只会给你带来两个价值观。

因此,您可以使用带有位标志的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);
}

这也是一种痛苦,但它会给你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));
}
}

真不错。在get中复制数组时,可能会执行一个集合,但关键是您可以获得以原子方式设置所有32个集合。(compare和set 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);
}

如果你想原子地对数据执行临时操作(get->change->set,没有干扰),你必须使用锁或同步。就我个人而言,我会使用锁定或同步,因为通常情况下,整个更新都是你想要保留的

**不安全**

不要这样!

这可以(可能)通过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);
}
}

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

这为整个单词设置了一个易失性读取,但也(可能)为其任意一半设置了一次易失性写入。可能的putByteVolatile可用于将一个长段拆分为8个段。

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

我只能想到两种方法:使用AtomicInteger的低两位或使用spinlock。我认为Hotspot可以自己优化某些锁,直到旋转锁。

使用锁:

Lock l = ...;
l.lock();
try {
// access the resource protected by this lock
} finally {
l.unlock();
}

从技术上讲,它不是一个同步块,尽管它是一种同步形式,但我认为你所要求的是同步的定义,所以我认为在没有同步的情况下不可能做到这一点。