何时在Java中使用AtomicReference?

时间:2010-10-18 23:15:25

标签: java multithreading

我们何时使用AtomicReference?

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

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

7 个答案:

答案 0 :(得分:185)

原子引用应该用在需要对引用执行简单的原子(即线程安全,非平凡)操作的设置中,对于哪个监视器 - 基于同步是不合适的。假设您只想检查特定字段是否仅在上次检查时对象的状态仍然存在:

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 Language Specification 3ed, Section 17.7

答案 1 :(得分:78)

当您需要在多个线程之间共享和更改不可变对象的状态时,理想的是使用原子引用。这是一个超级密集的陈述,所以我会稍微分解一下。

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

接下来,为什么AtomicReference比共享该共享值的易失性对象更好。一个简单的代码示例将显示差异。

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<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()调用,而是使用==比较期望值。因此,请确保期望是从循环中获取的实际对象。

答案 2 :(得分:26)

以下是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<IntPair> values =
        new AtomicReference<IntPair>(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
}

答案 3 :(得分:19)

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

  1. 您可以创建共享对象的副本
  2. 修改共享对象
  3. 您需要检查共享对象是否仍然与之前相同 - 如果是,则使用修改后的副本的引用进行更新。
  4. 由于其他线程可能已修改它并且/可以在这两个步骤之间进行修改。您需要在原子操作中执行此操作。这是AtomicReference可以提供帮助的地方

答案 4 :(得分:6)

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

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

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

private volatile V value;

get()只返回变量的最新值(因为挥发性在&#34中发生;在&#34;之前发生)。

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()方法。此方法调用unsafe调用本机调用,该调用向处理器调用单个指令。 &#34;期望值&#34;和&#34;更新&#34;每个引用一个对象。

当且仅当AtomicReference实例成员变量&#34;值&#34;指的是&#34;期待&#34;,&#34;更新&#34;现在分配给此实例变量,&#34; true&#34;退回。否则,返回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 :(得分:5)

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

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

public void doSomethingUsingLambdas() {

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

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

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

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

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

答案 6 :(得分:2)

  

我们何时使用AtomicReference?

AtomicReference是一种灵活的方式,可以在不使用同步的情况下以原子方式更新变量值。

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

使用高级concurrent 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 / synchronized等。