【问题标题】:When to use AtomicReference in Java?何时在 Java 中使用 AtomicReference?
【发布时间】:2011-04-27 05:24:44
【问题描述】:

我们什么时候使用AtomicReference

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

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

【问题讨论】:

    标签: java multithreading atomicreference


    【解决方案1】:

    这是 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),则在一些不幸的时机下,两者都将通过设置器中的检查,并且将应用这两个修改。结果是范围现在保持 (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】:

    原子引用应该用于需要对引用执行简单原子(即thread-safe,非平凡)操作的设置中,基于监视器的同步不适合。假设您要检查是否仅当对象的状态保持上次检查时的特定字段:

    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*

    两个优秀的死树参考将向您介绍这个主题:

    请注意(我不知道这是否一直是真的)reference 赋值(即=)本身是原子的(更新 primitive 64 位类型像 longdouble 可能不是原子的;但更新 reference 始终是原子的,即使它是 64 位的)无需显式使用 Atomic*
    请参阅Java Language Specification 3ed, Section 17.7

    【讨论】:

    • 如果我错了,请纠正我,但似乎需要这个的关键是因为你需要做一个“compareAndSet”。如果我需要做的只是设置,我根本不需要 AtomicObject,因为引用更新本身是原子的?
    • @veggen Java 中的函数参数在函数本身之前被评估,因此在这种情况下内联没有任何区别。是的,它是安全的。
    • @sMoZely 没错,但如果你不使用AtomicReference,你应该标记变量volatile,因为虽然runtime保证引用分配是原子的, 编译器可以在变量没有被其他线程修改的假设下执行优化。
    • 最后一行不应该是boolean success = cache.compareAndSet(cachedValueToUpdate, newValue);吗?
    • @haikalpribadi 是的,我很确定你是对的。 newValue 被扔掉了。放入缓存中的值是从缓存中取出的值,cachedValueToUpdate。令人着迷的是,它花了 10 年时间和 236 次投票才找到。
    【解决方案3】:

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

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

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

    private volatile V value;
    

    get() 只返回变量的最新值(就像 volatiles 以“发生在之前”的方式所做的那样)。

    public final V get()
    

    Following是AtomicReference最重要的方法。

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

    compareAndSet(expect,update) 方法调用 Java 的 unsafe 类的 compareAndSwapObject() 方法。这个不安全的方法调用调用了本地调用,它调用了一条到处理器的指令。 “期望”和“更新”每个都引用一个对象。

    当且仅当 AtomicReference 实例成员变量“value”引用了“expect”引用的同一个对象时,“update”现在分配给该实例变量,并返回“true”。否则,返回 false。整个事情是原子完成的。没有其他线程可以在两者之间进行拦截。 由于这是单处理器操作(现代计算机架构的魔力),它通常比使用同步块更快。但请记住,当需要以原子方式更新多个变量时,AtomicReference 将无济于事。

    我想添加一个完整的运行代码,可以在 eclipse 中运行。它会清除许多混乱。这里有 22 个用户(MyTh 线程)试图预订 20 个座位。以下是代码 sn -p 后跟完整代码。

    代码 sn-p,其中 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
                }
            }
        }
    
    }    
    

    【讨论】:

      【解决方案4】:

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

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

      public void doSomethingUsingLambdas() {
      
          AtomicReference<YourObject> yourObjectRef = new AtomicReference<>();
      
          soSomethingThatTakesALambda(() -> {
              yourObjectRef.set(youObject);
          });
      
          soSomethingElseThatTakesALambda(() -> {
              YourObject yourObject = yourObjectRef.get();
          });
      }
      

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

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

      【讨论】:

        【解决方案5】:

        我们什么时候使用 AtomicReference?

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

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

        有多种方法可以通过高级concurrent API 实现线程安全。原子变量是多个选项之一。

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

        Executors 定义了一个用于启动和管理线程的高级 API。 java.util.concurrent 提供的 Executor 实现提供了适合大规模应用的线程池管理。

        并发集合可以更轻松地管理大型数据集合,并且可以大大减少同步需求。

        原子变量具有最小化同步并有助于避免内存一致性错误的功能。

        提供一个应该使用 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 等。

        【讨论】:

          【解决方案6】:

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

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

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

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

          每次你想根据它的当前值修改那个 volatile 字段引用的字符串时,你首先需要获得一个对该对象的锁定。这可以防止其他线程在此期间进入并更改新字符串连接中间的值。然后,当您的线程恢复时,您会破坏另一个线程的工作。但老实说,代码会起作用,看起来很干净,而且会让大多数人开心。

          小问题。它很慢。特别是如果该锁定对象有很多争用。那是因为大多数锁需要操作系统系统调用,并且您的线程将阻塞并被上下文切换出 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 指令,而不是操作系统调用。那是 CPU 上的一条指令。而且因为没有锁,所以在锁被执行的情况下没有上下文切换,从而节省了更多时间!

          问题在于,对于 AtomicReferences,这不使用 .equals() 调用,而是使用 == 比较期望值。因此,请确保预期是从循环中返回的实际对象。

          【讨论】:

          • 您的两个示例的行为不同。您必须循环 worked 才能获得相同的语义。
          • 我认为你应该在 AtomicReference 构造函数中初始化值,否则在你调用 shared.set 之前另一个线程可能仍然会看到值 null。 (除非 shared.set 在静态初始化程序中运行。)
          • 在你的第二个例子中,你应该从 Java 8 开始使用类似的东西: shared.updateAndGet( (x) -> (x+"lets add something")); ...这将重复调用 .compareAndSet 直到它工作。这相当于总是会成功的同步块。您需要确保传入的 lambda 没有副作用,因为它可能会被多次调用。
          • 不需要使volatile String sharedValue。同步(锁定)足以在关系之前建立发生。
          • “...改变不可变对象的状态”在这里是不精确的,部分原因是你不能改变不可变对象的状态。该示例演示了将引用从一个不可变对象实例更改为不同的对象实例。我意识到这很迂腐,但考虑到 Thread 逻辑的混乱程度,我认为值得强调一下。
          【解决方案7】:

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

          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

          【讨论】:

            【解决方案8】:

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

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

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

            【讨论】:

            • 乐观锁是我一直在寻找的概念。
            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2010-09-21
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            相关资源
            最近更新 更多