什么时候在Java中使用AtomicReference?

436

我们何时会使用AtomicReference呢?

在所有多线程程序中都需要创建对象吗?

请提供一个简单的例子,说明何时应该使用AtomicReference

8个回答

286
原子引用应该在需要对引用执行简单的原子(即线程安全的、非平凡的)操作,并且基于监视器的同步不适用的情况下使用。假设您只想在处理过程中对象的状态发生变化时设置特定字段:
AtomicReference<Object> cache = new AtomicReference<Object>();

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,newValue);

因为原子引用语义的缘故,即使在多个线程之间共享cache对象的情况下,您也可以这样做,而无需使用synchronized。一般来说,除非您知道自己在做什么,否则最好使用同步器或java.util.concurrent框架,而不是裸露的Atomic*
以下是两本优秀的纸质参考资料,可以帮助您了解这个主题: 请注意(我不知道这是否一直如此),引用赋值(即=)本身是原子的(更新像longdouble这样的原始64位类型可能不是原子的;但是更新引用始终是原子的,即使它是64位的),而无需显式使用Atomic*
请参阅Java语言规范第3版,第17.7节

56
如果我理解错了,请纠正我,似乎这是因为你需要执行“compareAndSet”,才需要使用这个关键字。如果我只需要进行设置,由于引用更新本身是原子性的,我根本不需要原子对象(AtomicObject)? - sMoZely
4
Java中的函数参数在函数本身之前被求值,因此在这种情况下内联不会产生任何影响。是的,它是安全的。 - Dmitry
42
如果您没有使用AtomicReference,那么要使变量具有原子性,需要将其标记为volatile。尽管运行时保证了引用分配的原子性,但编译器可能会在假定变量未被其他线程修改的情况下执行优化。 - kbolino
5
最后一行不应该是 boolean success = cache.compareAndSet(cachedValueToUpdate, newValue); 吗? - haikalpribadi
3
@haikalpribadi 是的,我非常确定你是正确的。 newValue 只是被丢弃了。放入缓存中的值是从其中出来的那个值,即 cachedValueToUpdate。有趣的是,这要花费十年和 236 个赞才发现。 - Mark
显示剩余11条评论

135
一个原子引用是在需要通过替换一个可被多个线程访问到的不可变对象的内容(使用一个新的拷贝)来更新时所使用的理想工具。这是一个非常密集的陈述,因此我会稍微解释一下。首先,不可变对象是指在构造之后实际上不会被改变的对象。经常情况下,不可变对象的方法会返回一个新的该类的实例。一些例子包括包装类Long和Double,以及String等等。接下来,为什么原子引用比volatile对象更适合共享该共享值?一个简单的代码示例将展示它们之间的区别。
volatile String sharedValue;

static final Object lock = new Object();

void modifyString() {
    synchronized (lock) {
        sharedValue = sharedValue + "something to add";
    }
}

每次您想修改由那个volatile字段引用的字符串的值时,您首先需要在该对象上获取锁。这可以防止其他线程在此期间进入并更改新字符串连接的中间值。然后当您的线程恢复时,您会覆盖另一个线程的工作。但是,老实说,那段代码将起作用,看起来很清晰,并且会让大多数人感到满意。

稍微有点问题。它很慢。特别是如果该锁对象存在很多争用。这是因为大多数锁需要进行操作系统系统调用,并且您的线程将被阻塞并被上下文切换出CPU以为其他进程腾出空间。

另一个选择是使用AtomicReference。

public static AtomicReference<String> 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() + "let's add something";
    // Compare and set
    success = shared.compareAndSet(prevValue, newValue);
}

为什么这样更好呢?说实话,那段代码比以前略微不太干净。但在 AtomicReference 内部发生了非常重要的事情,那就是比较和交换。

这是一条单个 CPU 指令,不是操作系统调用,它使得切换发生。这是 CPU 上的一个指令。由于没有锁,当锁被使用时,也没有上下文切换,这节省了更多的时间!

但问题在于,对于 AtomicReference,这不使用 .equals() 方法,而是使用 == 来比较期望值。所以请确保期望值是从循环中 get 到的实际对象。


15
您提供的两个示例表现不同。为了实现相同的语义,您需要在 worked 上进行循环操作。 - CurtainDog
6
我认为你应该在AtomicReference构造函数中初始化值,否则在调用shared.set之前,另一个线程仍可能看到null值。(除非shared.set在静态初始化程序中运行。) - Henno Vermeulen
10
在您的第二个示例中,应从Java 8开始使用类似以下内容的代码:shared.updateAndGet((x) -> (x+"lets add something")); ...这将重复调用.compareAndSet直到成功为止。这相当于始终成功的synchronized块。但是,您需要确保传递的lambda表达式是无副作用的,因为它可能会被多次调用。 - Tom Dibble
9
不需要将易变的字符串共享化。使用 synchronized(lock) 已足以建立 happens-before 关系。 - Jai Pandit
11
在这里,“改变不可变对象的状态”是不精确的,部分原因是因为字面上你无法改变不可变对象的状态。该示例演示了将引用从一个不可变对象实例更改为另一个对象实例。我意识到这有点迂腐,但考虑到线程逻辑可能会让人感到困惑,因此我认为值得强调。 - Mark Phillips
显示剩余6条评论

45

以下是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 IntPair(int lower, int upper) {
            this.lower = lower;
            this.upper = upper;
        }
    }

    private final AtomicReference<IntPair> values = 
            new AtomicReference<IntPair>(new IntPair(0, 0));

    public int getLower() {
        return values.get().lower;
    }

    public void setLower(int lower) {
        while (true) {
            IntPair oldv = values.get();
            if (lower > oldv.upper)
                throw new IllegalArgumentException(
                    "Can't set lower to " + lower + " > upper");
            IntPair newv = new IntPair(lower, oldv.upper);
            if (values.compareAndSet(oldv, newv))
                return;
        }
    }

    public int getUpper() {
        return values.get().upper;
    }

    public void setUpper(int upper) {
        while (true) {
            IntPair oldv = values.get();
            if (upper < oldv.lower)
                throw new IllegalArgumentException(
                    "Can't set upper to " + upper + " < lower");
            IntPair newv = new IntPair(oldv.lower, upper);
            if (values.compareAndSet(oldv, newv))
                return;
        }
    }
}

3
这篇文章与你的答案相似,但深入探讨了更复杂的内容。很有趣!https://www.ibm.com/developerworks/java/library/j-jtp04186/ - LppEdd
嗨!这个链接已经失效了,你能否找到这篇文章的有效链接?@LppEdd - zysaaa
1
@zysaaa 发现了一个缓存的 URL - https://web.archive.org/web/20201109033000/http://www.ibm.com/developerworks/java/library/j-jtp04186/ - Dinesh Babu K G
在调用compareAndSet()时,是否总是需要while(true) {}?是否有一种可自动循环的方法可用? - Dinesh Babu K G

30

如果你想在多个线程中更改共享对象,可以使用AtomicReference应用乐观锁定。

  1. 创建共享对象的副本
  2. 修改共享对象
  3. 需要检查共享对象是否仍与之前相同——如果是,则使用已修改的副本的引用进行更新。

由于其他线程可能在这两个步骤之间修改它,因此您需要以原子操作的方式执行。这就是AtomicReference的作用所在。


1
乐观锁是我正在寻找的概念。 - Gabriel Aramburu

17

这是一个非常简单的用例,与线程安全无关。

为了在lambda调用之间共享一个对象,AtomicReference是一个选项:

public void doSomethingUsingLambdas() {

    AtomicReference<YourObject> yourObjectRef = new AtomicReference<>();

    soSomethingThatTakesALambda(() -> {
        yourObjectRef.set(youObject);
    });

    soSomethingElseThatTakesALambda(() -> {
        YourObject yourObject = yourObjectRef.get();
    });
}

我并不是说这是好的设计或什么的(这只是一个琐碎的例子),但是如果你有需要在lambda调用之间共享对象的情况,AtomicReference 是一个选项。

实际上,你可以使用任何持有引用的对象,甚至是只有一个项目的集合。不过,AtomicReference 是最完美的选择。


14

我们什么时候使用AtomicReference?

AtomicReference是一种灵活的方式,可以在不使用同步的情况下原子性地更新变量值。它支持单个变量上的无锁线程安全编程。

有多种方法可以实现线程安全并发 API。原子变量就是其中之一。

Lock对象支持锁定习惯用法,简化了许多并发应用程序。

Executors定义了一个高级API,用于启动和管理线程。

Concurrent collections使得管理大型数据集合更加容易,并且可以极大地减少同步的需求。

原子变量具有最小化同步和帮助避免内存一致性错误的特性。

提供一个简单的例子,说明何时应该使用AtomicReference。

带有AtomicReference的示例代码:

String name1 = "Ravindra";

AtomicReference<String> reference =
    new AtomicReference<String>(name1 );

String name2 = "Ravindra Babu";
boolean result = reference.compareAndSet(name1 , name2 );
System.out.println("compareAndSet result: " + result );

在所有多线程程序中都需要创建对象吗?

并不是所有的多线程程序都需要使用AtomicReference

如果你想保护一个单独的变量,请使用AtomicReference。如果你想保护一个代码块,请使用其他构造,如Lock/synchronized等。

来源:docs.oracle.com


6

我不会多说。我的尊敬的朋友们已经提供了他们宝贵的意见。这篇博客最后的完整运行代码应该能够消除任何困惑。它是关于一个多线程场景下的电影座位预订小程序。

以下是一些重要的基本事实。 1> 不同的线程只能争夺堆空间中的实例和静态成员变量。 2> volatile读或写是完全原子化和序列化/发生在之前,并且仅从内存中完成。我的意思是,任何读取都将遵循内存中的先前写入。并且任何写入都将遵循内存中的先前读取。因此,使用volatile工作的任何线程始终会看到最新的值。 AtomicReference使用了volatile的这个属性。

以下是一些AtomicReference的源代码。 AtomicReference是指对象引用。该引用是AtomicReference实例中的volatile成员变量,如下所示。

private volatile V value;

get() 简单地返回变量的最新值(就像 volatile 在“happens before”方式下一样)。

public final V get()

以下是AtomicReference最重要的方法。

public final boolean  compareAndSet(V expect, V update) {
        return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}

compareAndSet(expect,update)方法调用了Java的unsafe类的compareAndSwapObject()方法。这个unsafe的方法调用了本地调用,从而调用处理器上的单个指令。"expect"和"update"分别引用一个对象。

仅当AtomicReference实例成员变量"value"引用的是由"expect"引用的同一对象时,才将"update"分配给此实例变量,并返回"true"。否则,返回false。整个过程是原子性完成的。没有其他线程可以在其中拦截。 由于这是单个处理器操作(现代计算机体系结构的神奇之处),它通常比使用同步块更快。但请记住,当需要原子更新多个变量时,AtomicReference无法帮助。

我想添加一个完整的运行代码,该代码可以在eclipse中运行。它将消除许多困惑。这里有22个用户(MyTh线程)正在尝试预订20个座位。以下是代码片段,后跟完整代码。

22个用户正在尝试预订20个座位的代码片段。

for (int i = 0; i < 20; i++) {// 20 seats
    seats.add(new AtomicReference<Integer>());
}
Thread[] ths = new Thread[22];// 22 users
for (int i = 0; i < ths.length; i++) {
    ths[i] = new MyTh(seats, i);
    ths[i].start();
}

以下是完整的运行代码。
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;

public class Solution {

    static List<AtomicReference<Integer>> seats;// Movie seats numbered as per
                                                // list index

    public static void main(String[] args) throws InterruptedException {
        // TODO Auto-generated method stub
        seats = new ArrayList<>();
        for (int i = 0; i < 20; i++) {// 20 seats
            seats.add(new AtomicReference<Integer>());
        }
        Thread[] ths = new Thread[22];// 22 users
        for (int i = 0; i < ths.length; i++) {
            ths[i] = new MyTh(seats, i);
            ths[i].start();
        }
        for (Thread t : ths) {
            t.join();
        }
        for (AtomicReference<Integer> seat : seats) {
            System.out.print(" " + seat.get());
        }
    }

    /**
     * id is the id of the user
     * 
     * @author sankbane
     *
     */
    static class MyTh extends Thread {// each thread is a user
        static AtomicInteger full = new AtomicInteger(0);
        List<AtomicReference<Integer>> l;//seats
        int id;//id of the users
        int seats;

        public MyTh(List<AtomicReference<Integer>> list, int userId) {
            l = list;
            this.id = userId;
            seats = list.size();
        }

        @Override
        public void run() {
            boolean reserved = false;
            try {
                while (!reserved && full.get() < seats) {
                    Thread.sleep(50);
                    int r = ThreadLocalRandom.current().nextInt(0, seats);// excludes
                                                                            // seats
                                                                            //
                    AtomicReference<Integer> el = l.get(r);
                    reserved = el.compareAndSet(null, id);// null means no user
                                                            // has reserved this
                                                            // seat
                    if (reserved)
                        full.getAndIncrement();
                }
                if (!reserved && full.get() == seats)
                    System.out.println("user " + id + " did not get a seat");
            } catch (InterruptedException ie) {
                // log it
            }
        }
    }

}    

你写道:“AtomicReference 最重要的方法是 compareAndSet。”那么“set()”方法呢?它也是原子性的,对吗?如果是这样,为什么不使用 set 方法呢? - user10239441
compareAndSet首先检查值是否为null,只有在这种情况下才会设置id。否则不会设置。set方法不会执行类似的操作。 - sankar banerjee
compareAndSet首先检查值是否为null,只有在这种情况下才会设置id。否则不会设置。set不会执行这样的操作。 - undefined

-1

另一个简单的例子是在会话对象中进行安全线程修改。

public PlayerScore getHighScore() {
    ServletContext ctx = getServletConfig().getServletContext();
    AtomicReference<PlayerScore> holder 
        = (AtomicReference<PlayerScore>) ctx.getAttribute("highScore");
    return holder.get();
}

public void updateHighScore(PlayerScore newScore) {
    ServletContext ctx = getServletConfig().getServletContext();
    AtomicReference<PlayerScore> holder 
        = (AtomicReference<PlayerScore>) ctx.getAttribute("highScore");
    while (true) {
        HighScore old = holder.get();
        if (old.score >= newScore.score)
            break;
        else if (holder.compareAndSet(old, newScore))
            break;
    } 
}

来源:http://www.ibm.com/developerworks/library/j-jtp09238/index.html


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接