问题
我们什么时候使用AtomicReference?
是否需要在所有多线程程序中创建对象?
提供一个应该使用AtomicReference的简单示例。
#1 热门回答(152 赞)
原子引用应该用在需要对引用执行简单(即线程安全,非平凡)操作的设置中,基于监视器的同步不适合。假设你要检查特定字段是否仅在上次检查时对象的状态仍然存在时:
AtomicReference cache = new AtomicReference();
Object cachedValue = new Object();
cache.set(cachedValue);
//... time passes ...
Object cachedValueToUpdate = cache.get();
//... do some work to transform cachedValueToUpdate into a new version
Object newValue = someFunctionOfOld(cachedValueToUpdate);
boolean success = cache.compareAndSet(cachedValue,cachedValueToUpdate);
由于原子引用语义,即使在线程之间共享cache对象,也可以执行此操作,而不使用synchronized。一般来说,你最好不要使用同步器或2666192030框架而不是裸Atomic*,除非你知道你在做什么。
两个优秀的死树引用,将向你介绍此主题:
Herlihy卓越的多处理器编程艺术
实践中的Java并发
需要注意的是(我不知道这个一向如此)referenceassignment(i.e.=)本身是原子(updatingprimitive64位类型likelongordoublemay不是原子,但在更新areferenceis总是原子,即使是64位),但没有明确使用anAtomic*。
参见Java Language Specification 3ed,Section 17.7。
#2 热门回答(65 赞)
当你需要在多个线程之间共享和更改不可变对象的状态时,原子引用是理想的选择。这是一个超级密集的声明,所以我会稍微分解一下。
首先,不可变对象是在构造之后实际上不会改变的对象。通常,不可变对象的方法返回同一个类的新实例。一些例子包括Long和Double的包装类,以及String,仅举几例。 (根据编程并发JVMimmutable对象是现代并发的关键部分)。
接下来,为什么AtomicReference比共享该共享值的volatile对象更好。一个简单的代码示例将显示差异。
volatile String sharedValue;
static final Object lock=new Object();
void modifyString(){
synchronized(lock){
sharedValue=sharedValue+"something to add";
}
}
每次要根据当前值修改该volatile字段引用的字符串时,首先需要获取该对象的锁定。这可以防止其他一些线程在此期间进入并更改新字符串连接中间的值。然后当你的线程恢复时,你破坏了另一个线程的工作。但老实说,代码会起作用,看起来很干净,这会让大多数人开心。
轻微问题。这很慢。特别是如果有很多争用的锁对象。这是因为大多数锁需要OS系统调用,并且你的线程将阻塞并从CPU切换出上下文以便为其他进程腾出空间。
另一种选择是使用AtomicRefrence。
public static AtomicReference shared = new AtomicReference<>();
String init="Inital Value";
shared.set(init);
//now we will modify that value
boolean success=false;
while(!success){
String prevValue=shared.get();
// do all the work you need to
String newValue=shared.get()+"lets add something";
// Compare and set
success=shared.compareAndSet(prevValue,newValue);
}
现在为什么这样更好?老实说,代码比以前干净一点。但是在AtomicRefrence的引擎盖下发生了一些非常重要的事情,那就是比较和交换。它是一个单CPU指令,而不是一个OS调用,使切换发生。这是CPU上的单个指令。并且因为没有锁,所以在锁被运用的情况下没有上下文切换,这节省了更多的时间!
问题是,对于AtomicReferences,它不使用.equals()调用,而是使用==比较预期值。因此,请确保期望是从循环中获取的实际对象。
#3 热门回答(23 赞)
以下是AtomicReference的用例:
考虑这个充当数字范围的类,并使用单独的AtmomicInteger变量来维护较低和较高的数字范围。
public class NumberRange {
// INVARIANT: lower <= upper
private final AtomicInteger lower = new AtomicInteger(0);
private final AtomicInteger upper = new AtomicInteger(0);
public void setLower(int i) {
// Warning -- unsafe check-then-act
if (i > upper.get())
throw new IllegalArgumentException(
"can't set lower to " + i + " > upper");
lower.set(i);
}
public void setUpper(int i) {
// Warning -- unsafe check-then-act
if (i < lower.get())
throw new IllegalArgumentException(
"can't set upper to " + i + " < lower");
upper.set(i);
}
public boolean isInRange(int i) {
return (i >= lower.get() && i <= upper.get());
}
}
setLower和setUpper都是check-then-act序列,但它们没有使用足够的锁定来使它们成为原子序列。如果数字范围成立(0,10),并且一个线程调用setLower(5)而另一个线程调用setUpper(4),那么一些不幸的时间都会通过setter中的检查,并且将应用这两个修改。结果是该范围现在保持(5,4)无效状态。因此,虽然底层的AtomicIntegers是线程安全的,但复合类不是。这可以通过使用AtomicReference而不是使用单个AtomicIntegers来修复上限和下限来解决。
public class CasNumberRange {
//Immutable
private static class IntPair {
final int lower; // Invariant: lower <= upper
final int upper;
...
}
private final AtomicReference values =
new AtomicReference(new IntPair(0, 0));
public int getLower() { return values.get().lower; }
public int getUpper() { return values.get().upper; }
public void setLower(int i) {
while (true) {
IntPair oldv = values.get();
if (i > oldv.upper)
throw new IllegalArgumentException(
"Can't set lower to " + i + " > upper");
IntPair newv = new IntPair(i, oldv.upper);
if (values.compareAndSet(oldv, newv))
return;
}
}
// similarly for setUpper
}