JUC AtomicStampedReference源码解析 JDK8

news/2024/5/20 8:11:12 标签: java, JUC, 源码解析, 原子类

前言

大家都知道CAS操作有ABA问题的,且ABA问题是针对引用型对象的,而AtomicStampedReference的出现就是为了解决这一问题而出现的。通过加版本号来实现。

JUC框架 系列文章目录

成员

java">public class AtomicStampedReference<V> {
    private static class Pair<T> {
        final T reference;  //引用型对象
        final int stamp;  //版本号
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {  //静态方法以创建Pair
            return new Pair<T>(reference, stamp);
        }
    }

    private volatile Pair<V> pair;
...
}
  • 静态内部类Pair,reference成员保存你的引用型对象,stamp成员则保存你对象的版本号。
  • AtomicStampedReference有一个volatile的Pair<V>成员,这样,保证了pair成员的可见性。
  • 为了让Pair对象不可变,让两个域都是final的。你没有办法修改Pair对象的成员,所以只能通过静态函数重新构造一个Pair对象。这一点很重要。
  • 你可能想问,反正静态内部类Pair是只给自己用的,何必将Pair定义成泛型类呢,完全可以如下定义。一来这样就无法使用静态函数of了,构造器是私有的只能通过这个静态函数访问构造器(注释中有解释);二来Pair对象并不需要作为成员内部类而存在,即Pair不需要持有外部类的引用。
java">public class AtomicStampedReference<V> {
    private class Pair {
        final V reference;  //直接使用V
        final int stamp;  
        private Pair(V reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        //此静态函数会报错,因为这个静态域需要AtomicStampedReference的对象已存在才能得到它的泛型类型V是什么
        //一个静态域,居然依赖一个对象,所以会报错。
        static Pair of(V reference, int stamp) {  
            return new Pair(reference, stamp);
        }
    }

    private volatile Pair<V> pair;
...
}

常用操作

java">    public AtomicStampedReference(V initialRef, int initialStamp) {
        pair = Pair.of(initialRef, initialStamp);
    }
    
    V getReference() {
        return pair.reference;
    }

    public int getStamp() {
        return pair.stamp;
    }
  • getReference/getStamp都是通过volatile的成员pair获得的,所以具有可见性。
java">    public V get(int[] stampHolder) {
        Pair<V> pair = this.pair;
        stampHolder[0] = pair.stamp;
        return pair.reference;
    }
  • get函数是为了“一次性”获得存储对象和版本号。对象通过返回值,版本号通过int数组。
  • 因为int作为参数只能值传递,而int数组是一个对象,可以引用传递,所以要使用int数组。由于只需要得到版本号,所以数组大小大于1就可以了。
  • 当然,该函数是复合操作,不具有原子性。
java">    public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp) {
        Pair<V> current = pair;
        return
            //旧引用是相同的
            expectedReference == current.reference &&
            //旧版本号也是相同的
            expectedStamp == current.stamp &&
            //想设的新引用和新版本号,也和当前的相同
            ((newReference == current.reference && newStamp == current.stamp) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }

    private boolean casPair(Pair<V> cmp, Pair<V> val) {
        return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
    }

//Unsafe.java
    public final native boolean compareAndSwapObject(Object o, long offset,
                                                     Object expected,
                                                     Object x);
  • 首先expectedReference == current.reference判断旧引用,和当前的是否一样;expectedStamp == current.stamp判断旧版本,和当前的是否一样。
  • (newReference == current.reference && newStamp == current.stamp)判断 新引用和新版本号,是否和当前的相同,一般情况下,不会满足此判断的。
  • 既然上一条不满足,则调用casPair,最终调用到Unsafe的compareAndSwapObject
  • 注意,每次执行此函数时,都会重新构造Pair对象,这一点很关键。这保证调用compareAndSwapObject函数时,参数expected和x肯定两个不同的对象,就算最开始的expectedReference与newReference一样,且expectedStamp与newStamp一样,也会构造出一个新对象,当然这由于短路或不会执行到的。
  • 完全有可能,从expectedStamp == current.stampcasPair(current, Pair.of(newReference, newStamp))期间,线程被切换走了,切换的时候pair成员变量已经被修改了,等切换回来,current这个局部变量就不和成员pair相等了,自然casPair会失败。
java">    public void set(V newReference, int newStamp) {
        Pair<V> current = pair;
        if (newReference != current.reference || newStamp != current.stamp)
            this.pair = Pair.of(newReference, newStamp);
    }
  • 这是用来无条件设置的函数。因为不需要保持旧值是否相同。
  • 如果 新引用和新版本号 和 当前的 一样,那么不需要更新pair成员。
java">    public boolean attemptStamp(V expectedReference, int newStamp) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&
            (newStamp == current.stamp ||
             casPair(current, Pair.of(expectedReference, newStamp)));
    }
  • 此函数只更新版本号,不更新引用对象。
  • 只需保证旧引用和当前的相同,casPair函数保证了 只有current局部变量与当前pair成员是同一个对象时,才更新。
java">    private static final sun.misc.Unsafe UNSAFE = sun.misc.Unsafe.getUnsafe();
    private static final long pairOffset =
        objectFieldOffset(UNSAFE, "pair", AtomicStampedReference.class);

    static long objectFieldOffset(sun.misc.Unsafe UNSAFE,
                                  String field, Class<?> klazz) {
        try {
            return UNSAFE.objectFieldOffset(klazz.getDeclaredField(field));
        } catch (NoSuchFieldException e) {
            // Convert Exception to corresponding Error
            NoSuchFieldError error = new NoSuchFieldError(field);
            error.initCause(e);
            throw error;
        }
    }
  • 获得pair域的偏移量。

总结

  • 最终调用Unsafe的compareAndSwapObject方法时,是不关心版本号的。compareAndSwapObject只关心是不是同一个对象。(但这样不会造成问题)
  • 虽然根据上一条,感觉可能会有问题。但是由于静态内部类Pair每次都会新构造对象出来,即使T reference, int stamp两个参数完全一样,也会构造出两个不同的对象,所以最终调用的compareAndSwapObject不关心版本号也没有关系。
  • 综上,版本号是在Unsafe的CAS操作上进行的附加判断,准确的说,是先判断版本号,再通过CAS操作判断对象。

http://www.niftyadmin.cn/n/768207.html

相关文章

JUC AtomicIntegerArray源码解析 JDK8

前言 AtomicIntegerArray提供了对数组元素的原子操作&#xff0c;与其他非数组的原子类相比&#xff0c;它的成员不是volatile的而是final的了。 JUC框架 系列文章目录 成员 private static final Unsafe unsafe Unsafe.getUnsafe();private static final int base unsafe…

AQS深入理解 hasQueuedPredecessors源码分析 JDK8

文章目录前言流程hasQueuedPredecessors分析为什么先读取tail&#xff0c;再读取head先读取tail&#xff0c;再读取head的好处是否需要考虑 指令重排序虚假返回的true和false虚假返回true虚假返回false前言 Queries whether any threads have been waiting to acquire longer t…

Java并发 volatile可见性的验证

文章目录普通读 无法及时获得 主内存变量volatile读 及时获得 主内存变量普通读sleep普通读同步块同步块 遭遇 锁消除普通读System.out.println总结普通读 无法及时获得 主内存变量 public class volatileTest {static boolean flag false;//非volatile变量public static voi…

AQS深入理解 setHeadAndPropagate源码分析 JDK8

文章目录前言共享锁获取流程setHeadAndPropagate分析总结前言 Sets head of queue, and checks if successor may be waiting in shared mode, if so propagating if either propagate > 0 or PROPAGATE status was set. 此函数被共享锁操作而使用。这个函数用来将传入参数设…

AQS深入理解 doReleaseShared源码分析 JDK8

文章目录前言调用doReleaseShared的流程doReleaseShared分析head状态为0的情况特殊情况PROPAGATE状态设置后&#xff0c;并没有被检测到总结前言 Release action for shared mode – signals successor and ensures propagation. (Note: For exclusive mode, release just amou…

AQS深入理解 shouldParkAfterFailedAcquire源码分析 状态为0或PROPAGATE的情况分析

文章目录流程分析谁是调用shouldParkAfterFailedAcquire的线程什么时候会从parkAndCheckInterrupt中唤醒释放锁后&#xff0c;唤醒head后继的条件shouldParkAfterFailedAcquire源码分析关于shouldParkAfterFailedAcquire的疑问为什么检测到0后&#xff0c;一定要设置成SIGNAL&a…

AQS深入理解系列(一) 独占锁的获取过程

文章目录前言AQS实现核心state等待队列CAS根据实现核心找AQS的成员state等待队列CAS观察ReentrantLock的内部类公平的、不响应中断的 独占锁的获取tryAcquireaddWaiterenqenq的尾分叉 与 prev的有效性acquireQueuedshouldParkAfterFailedAcquireparkAndCheckInterrupt不公平的、…

AQS深入理解系列(二) 独占锁的释放过程

文章目录前言ReentrantLock.unlock()releasetryReleaseunparkSuccessorhead后继从哪里被唤醒前言 在上一篇文章AQS深入理解 独占锁的获取过程中&#xff0c;我们分析了独占锁的获取过程。相对于获取过程&#xff0c;独占锁的释放过程则相对简单了很多。 Lock lock new Reent…