Java多线程:AtomicReference AtomicStampedReference AtomicMarkableReference 原子更新引用类型,Java多线程系列--“JUC原子类”04之 AtomicReference原子类

AtomicReference

AtomicReference和AtomicInteger非常类似,不同之处就在于AtomicInteger是对整数的封装,而AtomicReference则对应普通的对象引用。也就是它可以保证你在修改对象引用时的线程安全性。

// 使用 null 初始值创建新的 AtomicReference。
AtomicReference()
// 使用给定的初始值创建新的 AtomicReference。
AtomicReference(V initialValue)

// 如果当前对象与预期对象时同一个对象,则以原子方式将该值设置为给定的更新值。
boolean compareAndSet(V expect, V update)
// 获取当前值。
V get()
// 以原子方式设置为给定值,并返回旧值。
V getAndSet(V newValue)
// 最终设置为给定值。
void lazySet(V newValue)
// 设置为给定值。
void set(V newValue)
// 返回当前值的字符串表示形式。
String toString()
// 与compareAndSet方法作用一致
boolean weakCompareAndSet(V expect, V update)

 

AtomicReference的使用例子代码如下:

 1 import java.util.concurrent.atomic.AtomicReference;
 2 
 3 public class Demo
 4 {
 5     public static void main(String[] args)
 6     {
 7         User user1 = new User("zhangsan", 20);
 8         User user2 = new User("lisi", 30);
 9         AtomicReference<User> atom = new AtomicReference<>(user1);
10         
11         System.out.println("原始值:" + atom.get());
12 
13         System.out.println("调用atom.getAndSet(user2)返回值" + atom.getAndSet(user2));
14         System.out.println("调用后值变为:" + atom);
15 
16         System.out.println("调用atom.compareAndSet(user2, user1)返回值:" + atom.compareAndSet(user2, user1));
17         System.out.println("调用后值为:" + atom);
18     }
19 }
20 
21 class User
22 {
23     private String name;
24     private int age;
25 
26     public User(String name, int age)
27     {
28         this.name = name;
29         this.age = age;
30     }
31 
32     @Override
33     public String toString()
34     {
35         return "(" + name + " : " + age + ")";
36     }
37 }

执行结果为:

原始值:(zhangsan : 20)
调用atom.getAndSet(user2)返回值(zhangsan : 20)
调用后值变为:(lisi : 30)
调用atom.compareAndSet(user2, user1)返回值:true
调用后值为:(zhangsan : 20)

 

那 AtomicReference 类是如何实现原子操作的呢,我们直接看它的源码是如何实现的

 1 public class AtomicReference<V>  implements java.io.Serializable {
 2     private static final long serialVersionUID = -1848883965231344442L;
 3 
 4     // 获取Unsafe对象,Unsafe的作用是提供CAS操作
 5     private static final Unsafe unsafe = Unsafe.getUnsafe();
 6     private static final long valueOffset;
 7 
 8     static {
 9       try {
10         valueOffset = unsafe.objectFieldOffset
11             (AtomicReference.class.getDeclaredField("value"));
12       } catch (Exception ex) { throw new Error(ex); }
13     }
14 
15     // volatile类型
16     private volatile V value;
17 
18     public AtomicReference(V initialValue) {
19         value = initialValue;
20     }
21 
22     public AtomicReference() {
23     }
24 
25     public final V get() {
26         return value;
27     }
28 
29     public final void set(V newValue) {
30         value = newValue;
31     }
32 
33     public final void lazySet(V newValue) {
34         unsafe.putOrderedObject(this, valueOffset, newValue);
35     }
36 
37     public final boolean compareAndSet(V expect, V update) {
38         return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
39     }
40 
41     public final boolean weakCompareAndSet(V expect, V update) {
42         return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
43     }
44 
45     public final V getAndSet(V newValue) {
46         while (true) {
47             V x = get();
48             if (compareAndSet(x, newValue))
49                 return x;
50         }
51     }
52 
53     public String toString() {
54         return String.valueOf(get());
55     }
56 }

说明
AtomicReference的源码比较简单。它是通过”volatile”和”Unsafe提供的CAS函数实现”原子操作。
(01) value是volatile类型。这保证了:当某线程修改value的值时,其他线程看到的value值都是最新的value值,即修改之后的volatile的值。
(02) 通过CAS设置value。这保证了:当某线程池通过CAS函数(如compareAndSet函数)设置value时,它的操作是原子的,即线程在操作value时不会被中断。

 

AtomicStampedReference 和AtomicMarkableReference 

按照前面讲的,线程判断被修改对象是否可以正确写入的条件是对象的当前值和期望是否一致。这个逻辑从一般意义上来说是正确的。但有可能出现一个小小的例外,就是当你获得对象当前数据后,在准备修改为新值前,对象的值被其他线程连续修改了2次,而经过这2次修改后,对象的值又恢复为旧值。这样,当前线程就无法正确判断这个对象究竟是否被修改过。如图显示了这种情况。

《Java多线程:AtomicReference AtomicStampedReference AtomicMarkableReference 原子更新引用类型,Java多线程系列--“JUC原子类”04之 AtomicReference原子类》

一般来说,发生这种情况的概率很小。而且即使发生了,可能也不是什么大问题。比如,我们只是简单得要做一个数值加法,即使在我取得期望值后,这个数字被不断的修改,只要它最终改回了我的期望值,我的加法计算就不会出错。也就是说,当你修改的对象没有过程的状态信息,所有的信息都只保存于对象的数值本身。

但是,在现实中,还可能存在另外一种场景。就是我们是否能修改对象的值,不仅取决于当前值,还和对象的过程变化有关,这时,AtomicReference就无能为力了。打一个比方,如果有一家蛋糕店,为了挽留客户,绝对为贵宾卡里余额小于20元的客户一次性赠送20元,刺激消费者充值和消费。但条件是,每一位客户只能被赠送一次。如果用户正好正在进行消费,就在赠予金额到账的同时,他进行了一次消费,使得总金额又小于20元,并且正好累计消费了20元。使得消费、赠予后的金额等于消费前、赠予前的金额。这时,后台的赠予进程就会误以为这个账户还没有赠予,所以,存在被多次赠予的可能。这样就会使得CAS操作无法正确判断当前数据状态。虽然说这种情况出现的概率不大,但是依然是有可能的出现的。因此,当业务上确实可能出现这种情况时,我们也必须多加防范。

 

上面所描素的就是我们常说的CAS过程中的ABA问题,为了避免这个问题,体贴的JDK也已经为我们考虑到了这种情况,使用AtomicStampedReference就可以很好的解决这个问题。

AtomicStampedReference 相当于一个 [引用,integer] 的二元数组,AtomicMarkableReference 相当于一个 [引用,boolean] 的二元数组

AtomicStampedReference 可用来作为带版本号的原子引用,而 AtomicMarkableReference 可用于表示如:已删除的节点

AtomicReference无法解决上述问题的根本是因为对象在修改过程中,丢失了状态信息。对象值本身与状态被画上了等号。因此,我们只要能够记录对象在修改过程中的状态值,就可以很好的解决对象被反复修改导致线程无法正确判断对象状态的问题。

AtomicStampedReference正是这么做的。它内部不仅维护了对象值,还维护了一个时间戳或版本戳(我这里把它称为时间戳,实际上它可以使任何一个整数,它使用整数来表示状态值)。当AtomicStampedReference对应的数值被修改时,除了更新数据本身外,还必须要更新时间戳。当AtomicStampedReference设置对象值时,对象值以及时间戳都必须满足期望值,写入才会成功。因此,即使对象值被反复读写,写回原值,只要时间戳发生变化,就能防止不恰当的写入。

参考资料:

Java多线程系列–“JUC原子类”04之 AtomicReference原子类

Java中的Atomic包使用指南

《实战Java高并发程序设计》葛一鸣 郭超

    原文作者:JUC
    原文地址: https://www.cnblogs.com/2015110615L/p/6749608.html
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞