Loading

juc-atomic 原子类框架

本博客系列是学习并发编程过程中的记录总结。由于文章比较多,写的时间也比较散,所以我整理了个目录贴(传送门),方便查阅。

【神奇的传送门】java并发编程系列

juc-atomic 原子类框架

概述

在本章节开始之前,大家需要了解的知识点有两个

Unsafe类和CAS原理。

Unsafe类详解可见我的另一篇文章
javaUnsafe类
CAS操作原理详见:
并发编程的基石——CAS机制


早期的JDK版本中,如果要并发的对Integer、Long、Double之类的Java原始类型或引用类型进行操作,一般都需要通过锁来控制并发,以防数据不一致。

从JDK1.5开始,引入了java.util.concurrent.atomic工具包,该包提供了许多Java原始/引用类型的映射类,如AtomicIntegerAtomicLongAtomicBoolean,这些类可以通过一种“无锁算法”,线程安全的操作Integer、Long、Boolean等原始类型。

所谓“无锁算法”,我们在讲juc-locks锁框架系列中,已经接触过太多次了,其实底层就是通过Unsafe类实现的一种比较并交换的算法CAS,大致的结构如下(具体入参,根据上下文有所不同):
boolean compareAndSet(expectedValue, updateValue);
当希望修改的值与expectedValue相同时,则尝试将值更新为updateValue,更新成功返回true,否则返回false。

java.util.concurrent.atomic包结构如下:

原子类分类

根据修改的数据类型,可以将JUC包中的原子操作类可以分为4类。

原子类型划分

为了方面对这些类逐级掌握,我将这些原子类型分为以下几类:

  • 普通原子类型:提供对boolean、int、long和引用对象的原子性操作。
    • AtomicBoolean
    • AtomicInteger
    • AtomicLong
    • AtomicReference
  • 原子类型数组:提供对数组元素的原子性操作。
    • AtomicLongArray
    • AtomicIntegerArray
    • AtomicReferenceArray
  • 原子类型字段更新器:提供对指定对象的指定字段进行原子性操作。
    • AtomicLongFieldUpdater
    • AtomicIntegerFieldUpdater
    • AtomicReferenceFieldUpdater
  • 带版本号的原子引用类型:以版本戳的方式解决原子类型的ABA问题。
    • AtomicStampedReference
    • AtomicMarkableReference
  • 原子累加器(JDK1.8):AtomicLong和AtomicDouble的升级类型,专门用于数据统计,性能更高。
    • DoubleAccumulator
    • DoubleAdder
    • LongAccumulator
    • LongAdder

咱们就从他的分类逐个讲解。

操作基本类型和引用的原子类

因为他们的实现原理相似,底层都是通过Unsafe类做CAS操作,来原子的更新状态值。所以我们就拿AtomicInteger原子类来讲解。

2. AtomicInteger的应用

AtomicInteger,应该是atomic框架中用得最多的原子类了。顾名思义,AtomicInteger是Integer类型的线程安全原子类,可以在应用程序中以原子的方式更新int值。

来看下面这个示例程序:

public class Main {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger ai = new AtomicInteger();

        List<Thread> list = new ArrayList<>();
        for (int i = 0; i < 10; i++) {
            Thread t = new Thread(new Accumlator(ai), "thread-" + i);
            list.add(t);
            t.start();
        }

        for (Thread t : list) {
            t.join();
        }

        System.out.println(ai.get());
    }

    static class Accumlator implements Runnable {
        private AtomicInteger ai;

        Accumlator(AtomicInteger ai) {
            this.ai = ai;
        }

        @Override
        public void run() {
            for (int i = 0, len = 1000; i < len; i++) {
                ai.incrementAndGet();
            }
        }
    }
}

结果:10000

可以看出我们循环创建了10个线程对AtomicInteger变量ai.incrementAndGet();操作。

即以原子的操作对int值进行自增。如果不使用AtomicInteger,使用原始的int或Integer,最终结果值可能会小于10000(并发时读到了过时的数据或存在值覆盖的问题)。

它里面有很多类似的方法,大部分都是基于Unsafe类实现的,实现了原子操作。

(Unsafe类详解可见我的另一篇文章

。CAS操作原理详见:

源码解读

它有了两种构造方法

有参构造器

public AtomicInteger(int initialValue)

用给定的初始值创建一个新的AtomicInteger。

  • 参数

    initialValue - 初始值

无参构造器

public AtomicInteger()

创建一个新的AtomicInteger,初始值为 0

源码摘录:

// 设置为使用 Unsafe.compareAndSwapInt 进行更新
private static final Unsafe unsafe = Unsafe.getUnsafe();//获取Unsafe实例
private static final long valueOffset;

//静态初始化块
static {
    try {
        valueOffset = unsafe.objectFieldOffset
            (AtomicInteger.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
}

AtomicInteger中大部分操作都依靠Unsafe类完成,所以他一上来就直接Unsafe.getUnsafe()获取Unsafe实例。

valueOffset这个是指类中相应字段在该类的偏移量,在这里具体即是指value这个字段在AtomicInteger类的内存中相对于该类首地址的偏移量。

`一个对象一创建,然后给他分配内存空间,这样他就有一个开始的地址,比如是000100。然后对象里面有一堆属性,他们也需要空间呀。
那么他们就在 地址000100 后面挨个分配好自己的空间,比如参数A就分配到了000111地址。
那么valueOffset方法就可以获取到地址偏移量:000111-000100=000011
后面我们就可以用一些方法用地址偏移量从起始地址出发获取到这个参数A了。

然后可以看一个有一个静态初始化块,这个块的作用即是求出value这个字段的偏移量。具体的方法使用的反射的机制得到valueField对象,再根据objectFieldOffset这个方法求出value这个变量内存中在该对象中的偏移量。

因为其中的方法大同小异,基本都是依靠Unsafe的CAS操作方法来实现的,所以这里就列举两个方法来讲解。

方法一:AtomicInteger.incrementAndGet()

看源码:

/**
    以原子方式将当前值加一。
    return :更新的值
 */
public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

他onlyonlyonly调用了unsafe的getAndAddInt() 方法。

public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2)//正确的读取咱们的int值。
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));//一直循环去CAS更新直到成功

    return var5;//返回更新后的值
}

getIntVolatile方法用于在对象指定偏移地址处volatile读取一个intvolatile读写可以保证可见性和有序性。(详见Unsafe和并发编程的基石CAS)

方法二:AtomicInteger的特殊方法

AtomicInteger中有一个比较特殊的方法——lazySet

它也是AtomicX 类都支持的方法。

看源码

private volatile int value;
/**
 * 设置为给定值
 *
 * @param  newValue – 新值
 */
public final void set(int newValue) {
    value = newValue;
}

/**
 * 最终设置为给定值。
 *  参数:newValue – 新值
 * @since JDK1.6
 */
public final void lazySet(int newValue) {
    unsafe.putOrderedInt(this, valueOffset, newValue);
}

可以看到value是用volatile修饰的。我们知道通过volatile修饰的变量,可以保证在多处理器环境下的“可见性”。也就是说当一个线程修改一个共享变量时,其它线程能立即读到这个修改的值。volatile的实现最终是加了内存屏障:

  1. 保证写volatile变量会强制把CPU写缓存区的数据刷新到内存
  2. 读volatile变量时,使缓存失效,强制从内存中读取最新的值
  3. 由于内存屏障的存在,volatile变量还能阻止重排序

lazySet内部调用了Unsafe类的putOrderedInt方法,(Unsafe类不了解的同学可以看我的这篇文章

)通过该方法对共享变量值的改变,不一定能被其他线程立即看到。也就是说以普通变量的操作方式来写变量。

为什么会有这种奇怪方法?什么情况下需要使用lazySet呢?

考虑下面这样一个场景:

private AtomicInteger ai = new AtomicInteger();
lock.lock();
try
{
    // ai.set(1);
}
finally
{
    lock.unlock();
}

由于锁的存在:

  • lock()方法获取锁时,和volatile变量的读操作一样,会强制使CPU缓存失效,强制从内存读取变量。
  • unlock()方法释放锁时,和volatile变量的写操作一样,会强制刷新CPU写缓冲区,把缓存数据写到主内存

所以,上述ai.set(1)可以用ai.lazySet(1)方法替换提高性能。

简单总结:

Doug Lea 写道:它是一个小众的方法

由锁来保证共享变量的可见性,以设置普通变量的方式来修改共享变量,减少不必要的内存屏障,从而提高程序执行的效率。

JDK bug database上有对lazySet的更加详细描述:

https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6275329

原子类框架之AtomicReference

AtomicReference,顾名思义:原子的引用,就是以原子方式更新对象引用。

AtomicReference他的底层也是和其他Atomic原子类相似,都是以Unsafe类为基础。调用了Unsafe的compareAndSet方法。

原子意味着尝试更改相同AtomicReference的多个线程(例如,使用比较和交换操作)不会使AtomicReference最终达到不一致的状态。

AtomicReference应用场景

*为什么需要AtomicReference?难道多个线程同时对一个引用变量赋值也会出现并发问题?*
引用变量的赋值本身没有并发问题,也就是说对于引用变量var ,类似下面的赋值操作本身就是原子操作:
Foo var = ... ;

AtomicReference的引入是为了可以用一种类似乐观锁的方式操作共享资源,在某些情景下以提升性能。

我们知道,当多个线程同时访问共享资源时,一般需要以加锁的方式控制并发:

volatile Foo sharedValue = value;
Lock lock = new ReentrantLock();

lock.lock();
try{
    // 操作共享资源sharedValue
}
finally{
    lock.unlock();
}

上述访问方式其实是一种对共享资源加悲观锁的访问方式。

而AtomicReference提供了以无锁方式访问共享资源的能力(而是使用底层CAS),看看如何通过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”。

该示例并没有使用锁,而是使用自旋+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);

简单总结

总体来说,AtomicBoolean、AtomicInteger、AtomicLong和AtomicReference原理比较简单:使用CAS保证原子性,使用volatile保证可见性,最终能保证共享变量操作的线程安全。

操作数组的Atomic类

  • AtomicLongArray:提供对int[]数组元素的原子性更新操作。
  • AtomicIntegerArray:提供对long[]数组元素的原子性更新操作。
  • AtomicReferenceArray:提供对引用类型[]数组元素的原子性更新操作。

这里就说说和基本类型原子类不同的地方吧。

AtomicIntegerArray类它里面就维护了这个东西

private final int[] array;

array数组是final类型,保证了:

  • array在使用的时候,已经初始化了
  • array不能再重新指向其他对象

但是,array数组里面并不是volatile类型的,能确保可见性么?

答案是不能。

所以它的操作元素方法调用的都是unsafe里面具有volatile语义的方法。

也就是整个通过内存xia地址对数组元素的操作,也是有volatile语义的,即具有可见性。

简单总结

操作数组的Atomic类是用unsafe的CAS机制配合final机制来实现共享变量操作的线程安全的。

带版本号的原子引用类型AtomicStampedReference

为什么我们需要带版本号的原子引用类型呢?这涉及到《CAS中的ABA问题》

CAS,即 compareAndSwap。在Java中使用 Unsafe 类提供的native方法可以直接操作内存

ABA问题:CAS在操作的时候会检查变量的值是否被更改过,如果没有则更新值,但是带来一个问题,最开始的值是A,接着变成B,最后又变成了A。经过检查这个值确实没有修改过,因为最后的值还是A,但是实际上这个值确实已经被修改过了。为了解决这个问题,在每次进行操作的时候加上一个版本号,每次操作的就是两个值,一个版本号和某个值,A——>B——>A问题就变成了1A——>2B——>3A。

它是怎么实现的呢?

AtomicStampedReference 内部维护了一个 Pair的数据结构,用volatile修饰,保证可见性,用于打包数据对象和版本号。

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) {
        return new Pair<T>(reference, stamp);
    }
}

private volatile Pair<V> pair;

它的compareAndSet方法如下:

public boolean compareAndSet(V   expectedReference,
                             V   newReference,
                             int expectedStamp,
                             int newStamp) {
    Pair<V> current = pair;
    return
        expectedReference == current.reference && //期望值是否与Pair的引用reference
        expectedStamp == current.stamp &&//期望版本号是否与Pair的版本号相同
        ((newReference == current.reference && 
          newStamp == current.stamp) || //如果传进来的值和Pair中的一样,直接返回true,不用更新;
         casPair(current, Pair.of(newReference, newStamp)));
}

最后casPair又调用Unsafe.compareAndSwapObject来交互Pair属性。

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

原理总结:

所以简单来说,AtomicStampedReference是通过加版本号来解决CAS的ABA问题。至于怎么加版本号,因为compareAndSwapObject只能对比交互一个对象,所以只需要将数据和版本号打包到一个对象里就解决问题了。

同样Java中提供了AtomicMarkableReference,与 AtomicStampedReference 原理类似,只不过 AtomicMarkableReference 将版本号换成了一个 bool 值,只关心数据是否“被修改过”,而 AtomicStampedReference 可以关心数据被修改了多少次。

那么 AtomicStampedReference 的基本用法是什么呢?看如下:

//构造方法, 传入引用和戳
public AtomicStampedReference(V initialRef, int initialStamp)
//返回引用
public V getReference()
//返回版本戳
public int getStamp()
//如果当前引用 等于 预期值并且 当前版本戳等于预期版本戳, 将更新新的引用和新的版本戳到内存
public boolean compareAndSet(V   expectedReference,
                                 V   newReference,
                                 int expectedStamp,
                                 int newStamp)
//如果当前引用 等于 预期引用, 将更新新的版本戳到内存
public boolean attemptStamp(V expectedReference, int newStamp)
//设置当前引用的新引用和版本戳
public void set(V newReference, int newStamp) 

测试 Demo

public static void main(String[] args) {
 
        String str1 = "aaa";
        String str2 = "bbb";
        AtomicStampedReference<String> reference = new AtomicStampedReference<String>(str1,1);
        reference.compareAndSet(str1,str2,reference.getStamp(),reference.getStamp()+1);
        System.out.println("reference.getReference() = " + reference.getReference());
 
        boolean b = reference.attemptStamp(str2, reference.getStamp() + 1);
        System.out.println("b: "+b);
        System.out.println("reference.getStamp() = "+reference.getStamp());
 
        boolean c = reference.weakCompareAndSet(str2,"ccc",4, reference.getStamp()+1);
        System.out.println("reference.getReference() = "+reference.getReference());
        System.out.println("c = " + c);
    }

输出:
reference.getReference() = bbb
b: true
reference.getStamp() = 3
reference.getReference() = bbb
c = false
c为什么输出false呢, 因为版本戳不一致

其它原子类

至于操作其它类型的原子类,老实说,我本来想每种类型都写篇文章介绍一下他们的使用和实现的,不过我感觉很没必要,因为它们都是换汤不换药的,实现的套路都差不多,可能操作的类型不同所以功能不太同,但是看API的话你肯定可以知道它是干嘛的,比如说操作整型数组的原子类java.util.concurrent.atomic.AtomicIntegerArray有个方法int getAndSet(int i, int newValue)方法,看名字就能猜到,先获取索引为i的元素再将索引i的元素设置为newValue,真的是没什么好讲的。

posted @ 2022-02-03 23:51  程序员小小宇  阅读(112)  评论(0编辑  收藏  举报