Java AtomicInteger和AtomicStampedReference源码深度解析

Posted 刘Java

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java AtomicInteger和AtomicStampedReference源码深度解析相关的知识,希望对你有一定的参考价值。

基于JDK1.8详细介绍了JUC下面的AtomicInteger和AtomicStampedReference原子类源码和原理。

1 原子类AtomicInteger

1.1 重要属性

AtomicInteger用于实现通过原子的方式更新单个变量。AtomicInteger 中保存了一个核心字段value,它就代表了Atomiclnteger 的当前实际取值,所有的方法都是围绕该值进行的。

还有一个属性valueOffset,它保存着value 字段在Atomiclnteger 对象中的偏移量。Unsafe中的CAS方法都是通过字段的偏移量来操作字段的。

/**
 * 内部的value属性,它就代表了Atomiclnteger 的当前实际取值。
 * 所有的方法都是围绕该值进行的
 */
private volatile int value;

/**
 * 使用给定值初始化value
 *
 * @param initialValue 给定值
 */
public AtomicInteger(int initialValue) 
    value = initialValue;


/**
 * 初始化value值为0
 */
public AtomicInteger() 


/**
 * 内部实际上依赖于Unsafe类的方法,堆value值进行操作
 */
private static final Unsafe unsafe = Unsafe.getUnsafe();
/**
 * value字段的偏移量
 */
private static final long valueOffset;

static 
    try 
        //初始化value字段的偏移量
        valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
     catch (Exception ex) 
        throw new Error(ex);
    

1.2 重要方法

/**
 * 获取当前最新值
 *
 * @return 当前最新值
 */
public final int get() 
    return value;


/**
 * 设置给定新值
 *
 * @param newValue 新值
 */
public final void set(int newValue) 
    value = newValue;


/**
 * 原子性的将当前值设为给定新值,返回旧值
 *
 * @param newValue 新值
 * @return 旧值
 */
public final int getAndSet(int newValue) 
    return unsafe.getAndSetInt(this, valueOffset, newValue);



/**
 * 如果当前值等于预期值,则以原子方式将该值设置为给定的新值
 *
 * @param expect 预期值
 * @param update the new value
 * @return true 更新成功 false 更新失败
 */
public final boolean compareAndSet(int expect, int update) 
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);


/**
 * 原子性的将当前值加1,返回旧值
 *
 * @return 旧值
 */
public final int getAndIncrement() 
    return unsafe.getAndAddInt(this, valueOffset, 1);



/**
 * 原子性的将当前值减1,返回旧值
 *
 * @return 返回旧值
 */
public final int getAndDecrement() 
    return unsafe.getAndAddInt(this, valueOffset, -1);



/**
 * 原子性的将当前值增加delta,返回旧值
 *
 * @param delta 增加的值
 * @return 旧值
 */
public final int getAndAdd(int delta) 
    return unsafe.getAndAddInt(this, valueOffset, delta);



/**
 * 原子性的将当前值加1,返回新值
 *
 * @return 更新后的值
 */
public final int incrementAndGet() 
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;



/**
 * 原子性的将当前值减1,返回新值
 *
 * @return 更新后的值
 */
public final int decrementAndGet() 
    return unsafe.getAndAddInt(this, valueOffset, -1) - 1;



/**
 * 原子性的将当前值增加delta,返回新值
 *
 * @param delta 增加的值
 * @return 更新后的值
 */
public final int addAndGet(int delta) 
    return unsafe.getAndAddInt(this, valueOffset, delta) + delta;



/**
 1. 最终会设置成newValue,使用lazySet设置值后,可能导致其他线程在之后的一小段时间内还是可以读到旧的值。
 2. 关于该方法的更多信息可以参考并发编程网翻译的一篇文章《AtomicLong.lazySet是如何工作的?》,文章地址是“http://ifeve.com/how-does-atomiclong-lazyset-work/”。
 3.  4. @param newValue 新值
 */
public final void lazySet(int newValue) 
    unsafe.putOrderedInt(this, valueOffset, newValue);

可以看到,里面的方法都是调用的Unsafe类方法,进行的CAS操作。

Atomic包实际上只提供了3种基本类型的原子更新:int、long、boolean,其中boolean也是转换为int的0、1进行更新的,实际上并没有char、float和double等的CAS操作,实际上char、 float、double都可以转换为int或者long在进行操作,如果DoubleAdder就是采用Double.doubleToRawLongBits将double转换为long类型的值在进行操作。

/*Unsafe只提供了3种CAS方法.*/
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

/*AtomicBoolean源码中,它是先把Boolean转换成int类型,再使用compareAndSwapInt进行CAS操作*/
public final boolean compareAndSet(boolean expect, boolean update) 
    int e = expect ? 1 : 0;
    int u = update ? 1 : 0;
    return unsafe.compareAndSwapInt(this, valueOffset, e, u);

2 版本号原子类AtomicStampedReference

通过原子的方式更新单个变量的原子类的升级版,Atomic包提供了以下2个类:

  1. AtomicMarkableReference< V >:维护带有标记位的对象引用,可以原子方式对其进行更新。
  2. AtomicStampedReference< V >:维护带有整数标志的对象引用,可用原子方式对其进行更新。

上面两个原子类的方法以及原理几乎一致,属于带有版本号的原子类。我们知道CAS操作的三大问题之一就是“ABA”问题:CAS在操作值的时候,需要检查预期值有没有发生变化,如果没有发生变化则更新。但是,如果一个线程t1首先获取了预期值A,此时另一个线程t2则将值从A变成了B,随后又变成了A,随后t1再使用CAS进行比较交换的时候,会发现它的预期值“没有变化”,但实际上是变化过的。这就是ABA问题的由来。

ABA问题的解决思路就是使用版本号,1A->2B->3A,在Atomic包中,提供了一个现成的AtomicStampedReference类来解决ABA问题,使用的就是添加版本号的方法。还有一个AtomicMarkableReference实现类,它比AtomicStampedReference更加简单,AtomicStampedReference中每更新一次数据版本号也会更新一次,这样可以使用版本号统计到底更新了多少次,而AtomicMarkableReference仅仅使用了一个boolean值来表示值是否改变过,因此使用的比较少。

这里我们以AtomicStampedReference来讲解。

2.1 重要属性

AtomicStampedReference内部不仅维护了我们的传递的对象reference,还维护了一个int类型的版本号stamp,它们都被存放到一个Pair类型的内部类实例中。当AtomicStampedReference 对应的数据被修改时,除了更新数据本身外,还必须要更新版本号,这个版本号一般都是自增的。当AtomicStampedReference 设置对象值时,对象值及版本号都必须满足期望值,才会更新成功。

/**
 * Pair内部类,用于维护reference和stamp
 *
 * @param <T>
 */
private static class Pair<T> 
    /**
     * 真正的数据
     */
    final T reference;
    /**
     * 版本号
     */
    final int stamp;

    private Pair(T reference, int stamp) 
        this.reference = reference;
        this.stamp = stamp;
    

    /**
     * 返回Pair实例
     */
    static <T> Pair<T> of(T reference, int stamp) 
        return new Pair<T>(reference, stamp);
    


/**
 * 由于要维护两个属性,因此干脆使用一个内部类对象来维护这两个属性
 */
private volatile Pair<V> pair;

/**
 * 创建具有给定初始值的新 AtomicStampedReference。
 *
 * @param initialRef   初始值
 * @param initialStamp 初始版本号
 */
public AtomicStampedReference(V initialRef, int initialStamp) 
    //初始化一个Pair对象,并初始化属性值
    pair = Pair.of(initialRef, initialStamp);

2.2 重要方法

最重要的就是compareAndSet方法,它需要传递:期望值、新值、期望版本号、新版本号,当期望值和期望版本号都与此时内部的真实值和真实版本号相等的时候,就会调用compareAndSwapObject使用一个新的Pair对象替换旧的Pair对象,同时完成reference和stamp的更新。

/**
 * 如果当前引用 == 预期引用,并且当前版本号等于预期版本号,则以原子方式将该引用和该标志的值设置为给定的更新值。
 *
 * @param expectedReference 预期引用
 * @param newReference      新引用
 * @param expectedStamp     预期版本号
 * @param newStamp          新版本号
 * @return 如果成功,则返回 true
 */
public boolean compareAndSet(V expectedReference,
                             V newReference,
                             int expectedStamp,
                             int newStamp) 
    Pair<V> current = pair;
    //一系列的判断,如果两个预期值都相等,那么尝试调用compareAndSwapObject使用新的Pair对象替代旧的Pair对象
    //这样就同时完成了reference和stamp的更新
    return
            expectedReference == current.reference &&
                    expectedStamp == current.stamp &&
                    ((newReference == current.reference &&
                            newStamp == current.stamp) ||
                            casPair(current, Pair.of(newReference, newStamp)));


/**
 * CAS替换内部的Pair对象的方法
 *
 * @param cmp 预期pair对象
 * @param val 新pair对象
 * @return 如果成功,则返回 true
 */
private boolean casPair(Pair<V> cmp, Pair<V> val) 
    return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);



/**
 * @return 获得当前保存的对象引用
 */
public V getReference() 
    return pair.reference;


/**
 * @return 获得当前保存的版本号
 */
public int getStamp() 
    return pair.stamp;



/**
 * 设置新对象引用和版本号
 *
 * @param newReference 新对象引用
 * @param newStamp     新版本号
 */
public void set(V newReference, int newStamp) 
    Pair<V> current = pair;
    //如果新对象引用以及新版本号和之前的都一样那就不设置
    //否则就是新建一个Pair对象并设置相应的属性,替代原来的Pair对象
    if (newReference != current.reference || newStamp != current.stamp)
        this.pair = Pair.of(newReference, newStamp);

3 案例

实际上,如果更新的数据是无状态的数据,那么使用基本的原子类也可以完成目的,即如果线程A将值从1->2->1,而线程B仅仅是使用了值,这是没什么问题的,但是如果和业务相关联,比较的对象是有状态的,那么可能会出现严重问题。

比如还是线程A将值从1->2->1,而线程B的业务逻辑是如果发现数据改变过,那么就不能操作,这样的话就不能单纯的比较值了,这就需要用到版本号了。

/**
 * @author lx
 */
public class AtomicStampedReferenceDemo 

    public static void main(String args[]) 
        //初始值为0,版本号为0
        AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(0, 0);

        Thread thread = new Thread(() -> 
            //先获取标志位
            int timestamp = atomicStampedReference.getStamp();
            //获取原值
            int reference = atomicStampedReference.getReference();
            System.out.println("原值reference: " + reference);
            //阻塞,等待被唤醒
            LockSupport.park();
            if (atomicStampedReference.compareAndSet(reference, reference + 1, timestamp, timestamp + 1)) 
                System.out.println("更新成功,新值reference: " + atomicStampedReference.getReference());
             else 
                System.out.println("更新失败,新值reference: " + atomicStampedReference.getReference());
                System.out.println("虽然原值和新值相等,但是是在线程阻塞过程中值发生了变化,变化了" + atomicStampedReference.getStamp() + "次");
            
        );
        thread.start();


        Thread thread1 = new Thread(() -> 
            //对数据先加一再减一,反复4次,最终reference的值是不变的
            for (int i = 0; i < 4; i++) 
                int timestamp = atomicStampedReference.getStamp();
                int reference = atomicStampedReference.getReference();
                if (i % 2 == 0) 
                    atomicStampedReference.compareAndSet(reference, reference + 1, timestamp, timestamp + 1);
                 else 
                    atomicStampedReference.compareAndSet(reference, reference - 1, timestamp, timestamp + 1);
                
            
            //唤醒阻塞的thread线程
            LockSupport.unpark(thread);
        );
        thread1.start();
    

同样的逻辑,使用普通原子类就能更新成功:

/**
 1. @author lx
 */
public class AtomicRefrenceDemo 

    public static void main(String args[]) 
        //初始值为0
        AtomicReference<Integer> atomicReference = new AtomicReference<Integer>(0);

        Thread thread = new Thread(() -> 
            int reference = atomicReference.get();
            System.out.println("原值reference: " + reference);
            //阻塞,等待被唤醒
            LockSupport.park();
            if (atomicReference.compareAndSet(reference, reference + 1)) 
                System.out.println("更新成功,新值reference: " + atomicReference.get());
             else 
                System.out.println("更新失败,新值reference: " + atomicReference.get());
            
        );
        thread.start();


        Thread thread1 = new Thread(() -> 
            //对数据先加一再减一,反复4次,最终的值是不变的
            for (int i = 0; i < 4; i++) 
                int reference = atomicReference.get();
                if (i % 2 == 0) 
                    atomicReference.compareAndSet(reference, reference + 1);
                 else 
                    atomicReference.compareAndSet(reference, reference - 1);
                
            
            //唤醒阻塞的thread线程
            LockSupport.unpark(thread);
        );
        thread1.start();
    

4 总结

AtomicInteger和AtomicStampedReference都可用于通过原子的方式更新单个变量。

AtomicInteger的操作存在CAS中的“ABA”问题,而AtomicStampedReference的出现则是为了解决这个问题而生的。

相关文章:

  1. Unsafe:JUC—Unsafe类的原理详解与使用案例
  2. volatile:Java中的volatile实现原理深度解析以及应用
  3. CAS:Java中的CAS实现原理深度解析与应用案例
  4. 伪共享:Java中的伪共享深度解析以及避免方法
  5. JMH:Java使用JMH进行方法性能优化测试

如果有什么不懂或者需要交流,可以留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!

以上是关于Java AtomicInteger和AtomicStampedReference源码深度解析的主要内容,如果未能解决你的问题,请参考以下文章

Java多线程系列一——Atomic类

三atomic

JAVA私房菜专栏之Atomic原子类总结

Java AtomicInteger的用法

J.U.C Atomic基本类型原子操作

JDK源码分析-AtomicInteger