何时在Java中使用AtomicReference?


311

我们什么时候使用AtomicReference

是否需要在所有多线程程序中创建对象?

提供一个应使用AtomicReference的简单示例。

Answers:


215

原子引用应在需要对引用执行简单原子(即线程安全,非平凡)操作的环境中使用,这种情况不适用于基于监视器的同步。假设仅当对象的状态保持在上次检查时的状态,您才想检查是否有特定的字段:

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

由于原子引用的语义,即使cache对象在线程之间共享,也可以执行此操作,而无需使用synchronized。通常,最好是使用同步器或java.util.concurrent框架,而不是光秃秃的,Atomic*除非您知道自己在做什么。

两个优秀的死树引用将为您介绍该主题:

请注意,(我不知道是否一直如此)引用赋值(即=)本身就是原子的(更新原始的 64位类型(例如longdouble可能不是原子的);但是更新引用始终是原子的,即使它是64位的) ),而无需显式使用Atomic*
参见Java语言规范3ed,第17.7节


43
如果我错了,请纠正我,但是看来需要这样做的关键是因为您需要执行“ compareAndSet”。如果设置了我需要做的所有事情,那么由于引用更新本身是原子的,我根本就不需要AtomicObject吗?
2013年

执行cache.compareAndSet(cachedValue,someFunctionOfOld(cachedValueToUpdate))是否安全?即内联计算吗?
kaqqao 2013年

4
Java中的@veggen函数参数在函数本身之前进行求值,因此在这种情况下,内联没有区别。是的,这很安全。
德米特里(Dmitry)

29
@sMoZely正确,但是如果您不使用AtomicReference变量,则应标记该变量,volatile因为在运行时保证引用分配是原子的时,编译器可能会在假定变量未被其他线程修改的情况下执行优化。
kbolino

1
@BradCupit注意,我说“如果您使用AtomicReference”;如果您正在使用它,那么我的建议是朝相反的方向进行标记,final以便编译器可以进行相应的优化。
kbolino

91

当您需要在多个线程之间共享和更改不可变对象的状态时,原子引用是理想的选择。那是一个非常密集的陈述,所以我将其分解。

首先,不可变的对象是在构造后实际上不会改变的对象。通常,不可变对象的方法会返回同一类的新实例。一些示例包括Long和Double的包装类,以及String,仅举几个例子。(根据JVM不变对象上的编程并发性,这是现代并发性的关键部分)。

接下来,为什么AtomicReference在共享该共享值方面胜于易失性对象。一个简单的代码示例将显示差异。

volatile String sharedValue;
static final Object lock=new Object();
void modifyString(){
  synchronized(lock){
    sharedValue=sharedValue+"something to add";
  }
}

每次您想要基于其易失性字段修改该易失性字段引用的字符串时,首先需要获得对该对象的锁定。这样可以防止其他线程在此期间进入并在新字符串串联的中间更改值。然后,当您的线程恢复时,您将破坏另一个线程的工作。但老实说,该代码可以正常工作,看起来很整洁,并且可以使大多数人满意。

轻微问题。太慢了 尤其是当该锁对象存在很多争用时。那是因为大多数锁都需要OS系统调用,并且您的线程将被阻塞并被上下文切换到CPU之外,以便为其他进程腾出空间。

另一个选择是使用AtomicRefrence。

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()+"lets add something";
  // Compare and set
  success=shared.compareAndSet(prevValue,newValue);
}

现在为什么这样更好?老实说,这段代码比以前干净了一些。但是在AtomicRefrence的幕后发生了一件非常重要的事情,那就是比较和交换。使切换发生的是单个CPU指令,而不是OS调用。那是CPU上的一条指令。而且由于没有锁,因此在执行锁的情况下不会进行上下文切换,这可以节省更多时间!

对于AtomicReferences,要注意的是,它不使用.equals()调用,而是对期望值进行==比较。因此,请确保期望的是从get循环返回的实际对象。


14
您的两个示例的行为有所不同。您必须循环worked获取相同的语义。
CurtainDog 2014年

5
我认为您应该在AtomicReference构造函数中初始化该值,否则另一个线程在调用shared.set之前可能仍会看到null值。(除非shared.set在静态初始化程序中运行。)
Henno Vermeulen 2015年

8
在第二个示例中,从Java 8开始,您应该使用诸如:shared.updateAndGet((x)->(x +“ lets add something”))); ...,它将反复调用.compareAndSet,直到它起作用为止。这等效于将始终成功的同步块。您需要确保传入的lambda不会产生副作用,因为它可能会被多次调用。
Tom Dibble 2015年

2
不需要使易失性String sharedValue。synced(lock)足以在关系发生之前建立事件。
杰·潘迪特

2
“ ...更改不可变对象的状态”在这里是不精确的,部分原因是您不能立即改变不可变对象的状态。该示例演示了如何将引用从一个不可变对象实例更改为另一个实例。我意识到这很花哨,但是考虑到线程逻辑的混乱性,我认为值得强调。
马克·菲利普斯

30

这是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都是先检查后执行的序列,但是它们没有使用足够的锁定使其成为原子。如果数字范围保持(0,10),并且一个线程调用setLower(5),而另一个线程调用setUpper(4),则在一些不幸运的时间安排下,两个线程都将通过设置程序中的检查,并且将应用两个修改。结果是该范围现在保持(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;
        }
    }
}

2
本文与您的答案相似,但涉及更复杂的事情。这真有趣!ibm.com/developerworks/java/library/j-jtp04186
LppEdd

20

应用乐观锁时可以使用AtomicReference。您有一个共享库,并且想要从多个线程中更改它。

  1. 您可以创建共享库的副本
  2. 修改共享对象
  3. 您需要检查共享对象是否与以前相同-如果是,则使用修改后的副本的引用进行更新。

由于其他线程可能已经对其进行了修改,并且//可以在这两个步骤之间进行修改。您需要在原子操作中执行此操作。这是AtomicReference可以提供帮助的地方


7

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

要在lambda调用之间共享对象,AtomicReference可以使用选项

public void doSomethingUsingLambdas() {

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

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

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

我并不是说这是好的设计或其他任何东西(这只是一个简单的例子),但是如果您遇到需要在lambda调用之间共享一个对象的情况,则AtomicReference是一个选择。

实际上,您可以使用包含引用的任何对象,甚至可以使用仅包含一项的Collection。但是,AtomicReference非常适合。


6

我不会说太多。我尊敬的朋友们已经提供了宝贵的意见。本博客最后的完整运行代码应消除任何混乱。这是关于在多线程场景中预订电影座位的小程序。

一些重要的基本事实如下。1>不同的线程只能竞争堆空间中的实例变量和静态成员变量。2>易失性读或写完全是原子的并且已序列化/发生过,并且只能从内存中进行。说这是我的意思是任何读取都将跟随内存中的先前写入。并且任何写操作都将遵循先前从内存中读取的内容。因此,任何使用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的不安全类的compareAndSwapObject()方法。这种不安全的方法调用会调用本地调用,该本地调用会向处理器调用一条指令。“期望”和“更新”均引用一个对象。

仅当AtomicReference实例成员变量“值”所引用的对象相同时,才将“更新”分配给该实例变量,并返回“ 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
            }
        }
    }

}    

5

我们什么时候使用AtomicReference?

AtomicReference是一种灵活的方式,可以自动更新变量值,而无需使用同步。

AtomicReference 支持对单个变量进行无锁线程安全编程。

有多种使用高级并发 API 实现线程安全的方法。原子变量是多个选项之一。

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

Executors定义用于启动和管理线程的高级API。java.util.concurrent提供的执行器实现提供适用于大规模应用程序的线程池管理。

并发收集使管理大型数据收集更加容易,并且可以大大减少同步需求。

原子变量具有可最大程度减少同步并有助于避免内存一致性错误的功能。

提供一个应使用AtomicReference的简单示例。

带有AtomicReference以下示例代码:

String initialReference = "value 1";

AtomicReference<String> someRef =
    new AtomicReference<String>(initialReference);

String newReference = "value 2";
boolean exchanged = someRef.compareAndSet(initialReference, newReference);
System.out.println("exchanged: " + exchanged);

是否需要在所有多线程程序中创建对象?

您不必AtomicReference在所有多线程程序中使用。

如果要保护单个变量,请使用AtomicReference。如果要保护代码块,请使用其他构造,例如Lock/ synchronizedetc。


-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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.