本文首发于一世流云的专栏: https://segmentfault.com/blog...
AtomicReference,顾名思义,就是以原子方式更新对象引用。java
能够看到,AtomicReference持有一个对象的引用——value,并经过Unsafe类来操做该引用:segmentfault
为何须要AtomicReference?难道多个线程同时对一个引用变量赋值也会出现并发问题?
引用变量的赋值自己没有并发问题,也就是说对于引用变量var ,相似下面的赋值操做自己就是原子操做:
Foo var = ... ;
AtomicReference的引入是为了能够用一种相似乐观锁的方式操做共享资源,在某些情景下以提高性能。
咱们知道,当多个线程同时访问共享资源时,通常须要以加锁的方式控制并发:缓存
volatile Foo sharedValue = value; Lock lock = new ReentrantLock(); lock.lock(); try{ // 操做共享资源sharedValue } finally{ lock.unlock(); }
上述访问方式实际上是一种对共享资源加悲观锁的访问方式。安全
而AtomicReference提供了以无锁方式访问共享资源的能力,看看如何经过AtomicReference保证线程安全,来看个具体的例子:并发
public class AtomicRefTest { public static void main(String[] args) throws InterruptedException { AtomicReference<Integer> ref = new AtomicReference<>(new Integer(1000)); List<Thread> list = new ArrayList<>(); for (int i = 0; i < 1000; i++) { Thread t = new Thread(new Task(ref), "Thread-" + i); list.add(t); t.start(); } for (Thread t : list) { t.join(); } System.out.println(ref.get()); // 打印2000 } } class Task implements Runnable { private AtomicReference<Integer> ref; Task(AtomicReference<Integer> ref) { this.ref = ref; } @Override public void run() { for (; ; ) { //自旋操做 Integer oldV = ref.get(); if (ref.compareAndSet(oldV, oldV + 1)) // CAS操做 break; } } }
上述示例,最终打印“2000”。ide
该示例并无使用锁,而是使用自旋+CAS的无锁操做保证共享变量的线程安全。1000个线程,每一个线程对金额增长1,最终结果为2000,若是线程不安全,最终结果应该会小于2000。性能
经过示例,能够总结出AtomicReference的通常使用模式以下:优化
AtomicReference<Object> ref = new AtomicReference<>(new Object()); Object oldCache = ref.get(); // 对缓存oldCache作一些操做 Object newCache = someFunctionOfOld(oldCache); // 若是期间没有其它线程改变了缓存值,则更新 boolean success = ref.compareAndSet(oldCache , newCache);
上面的代码模板就是AtomicReference的常见使用方式,看下compareAndSet方法:this
该方法会将入参的expect变量所指向的对象和AtomicReference中的引用对象进行比较,若是二者指向同一个对象,则将AtomicReference中的引用对象从新置为update,修改为功返回true,失败则返回false。也就是说,AtomicReference实际上是比较对象的引用。spa
CAS操做可能存在 ABA的问题,就是说:
假如一个值原来是A,变成了B,又变成了A,那么CAS检查时会发现它的值没有发生变化,可是实际上却变化了。
通常来说这并非什么问题,好比数值运算,线程其实根本不关心变量中途如何变化,只要最终的状态和预期值同样便可。
可是,有些操做会依赖于对象的变化过程,此时的解决思路通常就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A - 2B - 3A。
AtomicStampedReference就是上面所说的加了版本号的AtomicReference。
先来看下如何构造一个AtomicStampedReference对象,AtomicStampedReference只有一个构造器:
能够看到,除了传入一个初始的引用变量initialRef外,还有一个initialStamp变量,initialStamp其实就是版本号(或者说时间戳),用来惟一标识引用变量。
在构造器内部,实例化了一个Pair对象,Pair对象记录了对象引用和时间戳信息,采用int做为时间戳,实际使用的时候,要保证时间戳惟一(通常作成自增的),若是时间戳若是重复,还会出现ABA的问题。
AtomicStampedReference的全部方法,其实就是Unsafe类针对这个 Pair对象的操做。
和AtomicReference相比,AtomicStampedReference中的每一个引用变量都带上了pair.stamp这个版本号,这样就能够解决CAS中的ABA问题了。
来看下AtomicStampedReference的使用:
AtomicStampedReference<Foo> asr = new AtomicStampedReference<>(null,0); // 建立AtomicStampedReference对象,持有Foo对象的引用,初始为null,版本为0 int[] stamp=new int[1]; Foo oldRef = asr.get(stamp); // 调用get方法获取引用对象和对应的版本号 int oldStamp=stamp[0]; // stamp[0]保存版本号 asr.compareAndSet(oldRef, null, oldStamp, oldStamp + 1) //尝试以CAS方式更新引用对象,并将版本号+1
上述模板就是AtomicStampedReference的通常使用方式,注意下compareAndSet方法:
咱们知道,AtomicStampedReference内部保存了一个pair对象,该方法的逻辑以下:
但这里有段优化逻辑,就是若是 newReference == current.reference && newStamp == current.stamp
,说明用户修改的新值和AtomicStampedReference中目前持有的值彻底一致,那么其实不须要修改,直接返回true便可。
咱们在讲ABA问题的时候,引入了AtomicStampedReference。
AtomicStampedReference能够给引用加上版本号,追踪引用的整个变化过程,如:
A -> B -> C -> D - > A,经过AtomicStampedReference,咱们能够知道,引用变量中途被更改了3次。
可是,有时候,咱们并不关心引用变量更改了几回,只是单纯的关心是否更改过,因此就有了AtomicMarkableReference:
能够看到,AtomicMarkableReference的惟一区别就是再也不用int标识引用,而是使用boolean变量——表示引用变量是否被更改过。
从语义上讲,AtomicMarkableReference对于那些不关心引用变化过程,只关心引用变量是否变化过的应用会更加友好。