Java原子操作Atomic类详解

2022-10-30 16:04:44 浏览数 (1)

前置知识(CAS部分)

  (1)什么是 CAS

      1.CAS(Compare And Swap,比较并交换),通常指的是这样一种原子操作:

        针对一个变量,首先比较它的内存值与某个期望值是否相同,如果相同,就给它赋一个新值。

      2.CAS 的逻辑用伪代码描述   : if (value == expectedValue) { value = newValue; }

        描述了一个由比较和赋值两阶段组成的复合操作,CAS可以看作是它们合并后的整体一个不可分割的原子操作,并且其原子性是直接在硬件层面得到保障的。

      3.CAS可以看做是乐观锁的一种实现方式,Java原子类中的递增操作就通过CAS自旋实现的。故这算是一种无锁算法,在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。

        展示代码:

代码语言:javascript复制
//构件CAS锁
public class CASLock {

    //加锁标记
    private volatile int state;
    private static final Unsafe UNSAFE;
    private static final long OFFSET;

    static {
        try {
            UNSAFE = UnsafeFactory.getUnsafe();
            OFFSET = UnsafeFactory.getFieldOffset(
                    UNSAFE, CASLock.class, "state");
        } catch (Exception e) {
            throw new Error(e);
        }
    }

    public boolean cas() {
        return UNSAFE.compareAndSwapInt(this, OFFSET, 0, 1);
    }

    public int getState() {
        return state;
    }

    public void setState(int state) {
        this.state = state;
    }

}

//使用锁部分
static CASLock casLock = new CASLock();

public static void main(String[] args) {
...
    for(;;){
        //获取锁标记,看是否能够加锁,可以则加锁
        if(casLock.getState()==0&&casLock.cas()) {
            try {
                //模拟业务逻辑
            } finally {
                //释放锁逻辑
                casLock.setState(0);
            }
            break;
        }
    }
...
}

  (2)CAS应用

      1.在 Java 中,CAS 操作是由 Unsafe 类提供支持的,该类定义了三种针对不同类型变量的 CAS 操作:

代码语言:javascript复制
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);

        它们都是 native 方法,由 Java 虚拟机提供具体实现,以 compareAndSwapInt 为例,Unsafe 的 compareAndSwapInt 方法接收 4 个参数,分别是:对象实例、内存偏移量、字段期望值、字段新值。该方法会针对指定对象实例中的相应偏移量的字段执行 CAS 操作。

      2.以 compareAndSwapInt 为例进行说明

        案例代码

代码语言:javascript复制
class Entity{ int x; }

public class UnsafeFactory {
    //获取 Unsafe 对象
    public static Unsafe getUnsafe() {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            return (Unsafe) field.get(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    //获取字段的内存偏移量
    public static long getFieldOffset(Unsafe unsafe, Class clazz, String fieldName) {
        try {
            return unsafe.objectFieldOffset(clazz.getDeclaredField(fieldName));
        } catch (NoSuchFieldException e) {
            throw new Error(e);
        }
    }
}

public static void main(String[] args) {
    Entity entity = new Entity();

    Unsafe unsafe = UnsafeFactory.getUnsafe();
    long offset = UnsafeFactory.getFieldOffset(unsafe, Entity.class, "x");
    boolean successful;

    // 4个参数分别是:对象实例、字段的内存偏移量、字段期望值、字段更新值,返回值为布尔值,成功为true,失败为false
    successful = unsafe.compareAndSwapInt(entity, offset, 0, 3);
    System.out.println(successful   "t"   entity.x);

    successful = unsafe.compareAndSwapInt(entity, offset, 0, 5);
    System.out.println(successful   "t"   entity.x);
}

        案例说明

  1. 通过内存操作,获得类中属性的偏移值,通过操作生成对象的内存偏移值,确定属性值在内存中的哪个位置,然后通过系统的原子操作指令来达到变更的目的。
  2. 通过操作系统的原子操作指令,保证了原子性(但是不保证可见性)。  
  3. 所以可见性和有序性需要在JVM层面进行保证,通过在原子指令的前面加上lock指令。 

  (3)CAS源码分析

    1、源码展示:

      先从寻找unsafe.compareAndSwapInt开始入手(绝对路径: openjdkhotspotsrcsharevmprimsunsafe.cpp):

代码语言:javascript复制
UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapObject(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jobject e_h, jobject x_h))
  UnsafeWrapper("Unsafe_CompareAndSwapObject");
  oop x = JNIHandles::resolve(x_h);
  oop e = JNIHandles::resolve(e_h);
  oop p = JNIHandles::resolve(obj);
  HeapWord* addr = (HeapWord *)index_oop_from_field_offset_long(p, offset);
  oop res = oopDesc::atomic_compare_exchange_oop(x, addr, e, true);
  jboolean success  = (res == e);
  if (success)
    update_barrier_set((void*)addr, x);
  return success;
UNSAFE_END

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  // 根据偏移量,计算value的地址
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  // Atomic::cmpxchg(x, addr, e) cas逻辑 x:要交换的值   e:要比较的值
  //cas成功,返回期望值e,等于e,此方法返回true 
  //cas失败,返回内存中的value值,不等于e,此方法返回false
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapLong(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jlong e, jlong x))
  UnsafeWrapper("Unsafe_CompareAndSwapLong");
  Handle p (THREAD, JNIHandles::resolve(obj));
  jlong* addr = (jlong*)(index_oop_from_field_offset_long(p(), offset));
  if (VM_Version::supports_cx8())
    return (jlong)(Atomic::cmpxchg(x, addr, e)) == e;
  else {
    jboolean success = false;
    ObjectLocker ol(p, THREAD);
    if (*addr == e) { *addr = x; success = true; }
    return success;
  }
UNSAFE_END

      发现核心逻辑在Atomic::cmpxchg方法中,跟进Atomic的cmpxchg方法(openjdkhotspotsrcsharevmruntimeatomic.cpp):

代码语言:javascript复制
unsigned Atomic::cmpxchg(unsigned int exchange_value, volatile unsigned int* dest, unsigned int compare_value) {
  assert(sizeof(unsigned int) == sizeof(jint), "more work to do");
  // 定义位于hotspot/src/share/vm/runtime/atomic.inline.hpp,此文件根据不同的宏定义,导入不同的实现
  // 例如:Windows系统x86架构会使用hotspot/src/os_cpu/windows_x86/vm/atomic_windows_x86.inline.hpp
  // Linux系统x86架构会使用hotspot/src/os_cpu/linux_x86/vm/atomic_linux_x86.inline.hpp
  return (unsigned int)Atomic::cmpxchg((jint)exchange_value, (volatile jint*)dest,  (jint)compare_value);
}

      会发现这个方法根据不同操作系统和不同CPU会有不同的实现

      【atomic.cpp引入了atomic.inline.hpp,在atomic.inline.hpp中,会根据操作系统和CPU架构选择cmpxchg方法不同的实现】

      (注:java可以一次编译到处运行,其原理就是JVM在不同的平台有不同的实现。cmpxchg就是平台级的,在windows和linux系统上会有不同的实现方式),

      针对两个文件分别展示

        atomic_linux_x86.inline.hpp

代码语言:javascript复制
//openjdkhotspotsrcos_cpulinux_x86vmatomic_linux_x86.inline.hpp
inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  //判断当前执行环境是否为多处理器环境
  int mp = os::is_MP();
  //LOCK_IF_MP(%4) 在多处理器环境下,为 cmpxchgl 指令添加 lock 前缀,以达到内存屏障的效果
  //cmpxchgl 指令是包含在 x86 架构及 IA-64 架构中的一个原子条件指令,
  //它会首先比较 dest 指针指向的内存值是否和 compare_value 的值相等,
  //如果相等,则双向交换 dest 与 exchange_value,否则就单方面地将 dest 指向的内存值交给exchange_value。
  //这条指令完成了整个 CAS 操作,因此它也被称为 CAS 指令。
  __asm__ volatile (LOCK_IF_MP(%4) "cmpxchgl %1,(%3)"
                    : "=a" (exchange_value)
                    : "r" (exchange_value), "a" (compare_value), "r" (dest), "r" (mp)
                    : "cc", "memory");
  return exchange_value;
}

        atomic_windows_x86.inline.hpp

代码语言:javascript复制
inline jint     Atomic::cmpxchg    (jint     exchange_value, volatile jint*     dest, jint     compare_value) {
  // 检测CPU是否多核,多核会返回1,单核返回0
  int mp = os::is_MP();
  // 内联汇编,通过cmpxchg指令执行CAS操作
  // 如果是多核处理器,会在cmpxchg前加上lock前缀
  // cmpxchg dword ptr [edx], ecx 含义为若eax的值于edx指向的内存处的值相等,则使用ecx的值替换edx指向的内存处的值
  __asm {
    mov edx, dest
    mov ecx, exchange_value
    mov eax, compare_value
    LOCK_IF_MP(mp)
    cmpxchg dword ptr [edx], ecx
  }
}

      总结:

        1.在操作做系统层面,Atomic::cmpxchg这个函数最终返回值是exchange_value

        2.在java代码层面,Unsafe_CompareAndSwapInt的返回值(jint)(Atomic::cmpxchg(x, addr, e)) == e就是true,表明CAS成功

        3.其次是操作系统的CAS指令只能保证原子性,而有序性和可见性则是通过lock指令实现

    2、汇总说明

  • 不管是 Hotspot 中的 Atomic::cmpxchg 方法,还是 Java 中的 compareAndSwapInt 方法,它们本质上都是对相应平台的 CAS 指令的一层简单封装。CAS 指令作为一种硬件原语,有着天然的原子性,这也正是 CAS 的价值所在。 

  (4)CAS缺陷

      CAS 虽然高效地解决了原子操作,但是还是存在一些缺陷的,主要表现在三个方面:

  1. 只能保证一个共享变量原子操作(如果是多个的话,还是换成对象包装吧)。
  2. 构建CAS乐观锁的话,自旋 CAS 长时间地不成功,则会给 CPU 带来非常大的开销。
  3. ABA 问题

  (5)ABA问题及其解决方案

    1、什么是ABA问题

      CAS算法实现一个重要前提需要取出内存中某时刻的数据,而在下时刻比较并替换,那么在这个时间差中会导致数据的变化。

      当有多个线程对一个原子类进行操作的时候,某个线程在短时间内将原子类的值A修改为B,又马 上将其修改为A,此时其他线程不感知,还是会修改成功。

      图示

      说明

        说白了就是:Thread1不清楚Thread2对value的操作,误以为value=1没有修改过。(如果出现ABA问题,必然是Thread1,要依据Thread2的修改进行操作,不然一般情况哪里会管你改没改)

    2、ABA问题的解决方案

      数据库有个锁称为乐观锁,是一种基于数据版本实现数据同步的机制,每次修改一次数据,版本就会进行累加。(说白了就是,除了一个位置存储数据,还有要给个位置存储修改标记)       Java也提供了相应的原子引用类AtomicStampedReference<V> 与 AtomicMarkableReference<V>

Atomic原子操作类介绍

  (1)概念

    在并发编程中很容易出现并发安全的问题,有一个很简单的例子就是多线程更新变量i=1,比如多个线程执行i 操作,就有可能获取不到正确的值,而这个问题,最常用的方法是通过Synchronized进行控制来达到线程安全的目的。(共享变量导致的线程安全问题,一般优先考虑加锁)     但是由于synchronized是采用的是悲观锁策略,并不是特别高效的一种解决方案。(也就是这些操作指令简单而且很快,加锁的话效益不大,容易拖累性能)     实际上,在J.U.C下的atomic包提供了一系列的操作简单,性能高效,并能保证线程安全的类去更新基本类型变量,数组元素,引用类型以及更新对象中的字段类型。     atomic包下的这些类都是采用的是乐观锁策略去原子更新数据,在java中则是使用CAS操作具体实现。

  (2)汇总介绍

      基本类型:AtomicInteger、AtomicLong、AtomicBoolean;       引用类型:AtomicReference、AtomicStampedReference、AtomicMarkableReference;       数组类型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray       对象属性原子修改器:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater       原子类型累加器(jdk1.8增加的类):DoubleAccumulator、DoubleAdder、LongAccumulator、LongAdder、Striped64

  (3)逐个分析

    1.基本类型

  • AtomicInteger分析
    1. 使用介绍
    2. 源码分析
    3. 说明
      1. 核心方法就是调用Unsafe类compareAndSwapInt方法去调用操作系统的指令。      
    4. 问题发现

代码语言:javascript复制
//抽出AtomicInteger类两个方法
public final boolean weakCompareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
public final int getAndDecrement() {
    return unsafe.getAndAddInt(this, valueOffset, -1);
}

//对应于Unsafe类的
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5   var4));

    return var5;
}
代码语言:javascript复制
AtomicInteger.set(newValue)  //设置存储的值为新值
AtomicInteger.compareAndSet(expect, update)  //比较存储的值与expect是否相同,相同才将存储值更新为update,返回布尔值结果
AtomicInteger.incrementAndGet() //存储的值自增,返回自增后结果
AtomicInteger.decrementAndGet() //存储的值自减,返回自减后结果
AtomicInteger.getAndAdd(delta)  //存储的值加上变动值delta(可正可负),返回旧值
AtomicInteger.getAndDecrement() //存储的值自减,返回旧值
AtomicInteger.get()  //获取存储的值

            getAndAddInt中:这种CAS失败自旋的操作存在什么问题?

              首先采用的是自旋的方式,那么必然要加锁成功执行完代码才能跳出自旋,所以会不断尝试自旋加锁。

              但是如果线程很多的情况,那么必然只有一个进行业务,其余的都在自旋加锁(这些都没什么作用,但是这些线程依旧会占据着CPU资源【说白了就是占着茅坑不拉屎的感觉,按照时间片轮转机制,这些线程越多,获得时间片的几率越大,而其他需要正真用CPU资源的线程得不到资源就会陷入等待】),那么如果这段代码是写在并发很高,且业务时间很长的逻辑中就会出现CPU迅速飙升的情况,整个系统出现卡顿。故,这种加锁方法,要么考虑控制线程数,要么考虑控制业务逻辑(需要时间很短),要么考虑加锁次数限制(一定次数后进入休眠,等待唤醒)。需要在这三者中进行衡量。

  • AtomicLong分析(这个与AtomicInteger类简直一样,只是类型上存在差别)  
  • AtomicBoolean分析 
    • 源码分析
    • 问题发现

代码语言:javascript复制
//存储数据的属性值
private volatile int value;
//比较与交换的方法
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);
}

为什么AtomicBoolean类中记录value值的类型是int类型,而不是boolean类型?

              因为原子操作会涉及到一个底层调用,这里的底层指的是C语言,在C语言中是没有boolean类型的,对标java的boolean类型采用的是int类型,所以底层间会存在一层转换,故java直接用int型来记录,省去转换的步骤。

    2.引用类型

  • AtomicReference分析
    • 使用介绍
    • 源码分析
    • 说明

代码语言:javascript复制
public final boolean compareAndSet(V expect, V update) {
    return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}
代码语言:javascript复制
User user1 = new User("张三", 23);
User user2 = new User("李四", 25);

//初始化为 user1
AtomicReference<User> atomicReference = new AtomicReference<>();
atomicReference.set(user1);
//把 user2 赋给 atomicReference
atomicReference.compareAndSet(user1, user2);

            本质上与AtomicInteger类也很相似,只是类型为通配对象。

            AtomicReference作用是对普通对象的封装,它可以保证你在修改对象引用时的线程安全性

  • AtomicStampedReference分析
    • 使用介绍
    • 源码分析
    • 说明

代码语言:javascript复制
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;

public boolean compareAndSet(V   expectedReference, V  newReference, boolean expectedMark, boolean newMark) {
    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);
}
代码语言:javascript复制
AtomicStampedReference atomicStampedReference = new AtomicStampedReference(newReference,newStamp);
Object reference = atomicStampedReference.getReference(); //获取存储对象
int stamp = atomicStampedReference.getStamp(); //获取版本号
//参数为(用于和存储值比对的值,新的存储值,用于和版本号比对的值,新版本好)
atomicStampedReference.compareAndSet(expectedReference,newReference,expectedStamp,newStamp);

            本质核心是基于AtomicReference类,AtomicReference类里面存储的是通用对象,而AtomicStampedReference类则在通用对象上面封装一层(有点类似于修饰器的思维)。

            Pair类便是用于封装的类(如果把Pair类当做通用对象,是不是跟AtomicReference类的逻辑变得一样了),stamp 存储版本号 ,reference 存储通用对象

  • AtomicMarkableReference分析
    • 说明

            AtomicMarkableReference可以理解为上面AtomicStampedReference的简化版,就是不关心修改过几次,仅仅关心是否修改过。因此变量mark是boolean类型,仅记录值是否有过修改。

    3.数组类型

  • AtomicIntegerArray分析
    • 使用介绍
    •  源码分析
    代码语言:javascript复制
    public AtomicIntegerArray(int[] array) {
        this.array = array.clone();
    }
    
    public final int getAndSet(int i, int newValue) {
        return unsafe.getAndSetInt(array, checkedByteOffset(i), newValue);
    }
    public final int getAndSetInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var4));
    
        return var5;
    }
    •  说明
代码语言:javascript复制
int[] value = new int[]{ 1, 2, 3, 4, 5 };
AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(value);
//设置索引0的元素为100
atomicIntegerArray.set(0, 100);
//以原子更新的方式将数组中索引为1的元素与输入值相加
atomicIntegerArray.getAndAdd(1,5);

            本质上还是通过拷贝将数据存储在AtomicIntegerArray原子类的array属性上,通过获取属性对象的数据偏移值*偏移个数得出数据所在位置,采用系统CAS原子操作进行变更。

  • AtomicLongArray分析(与AtomicIntegerArray很相似)
  • AtomicReferenceArray分析
    • 源码分析
    • 说明

代码语言:javascript复制
public AtomicReferenceArray(int length) {
    array = new Object[length];
}
    
public AtomicReferenceArray(int length) {
    array = new Object[length];
}

public AtomicReferenceArray(E[] array) {
    this.array = Arrays.copyOf(array, array.length, Object[].class);
}


public final boolean compareAndSet(int i, E expect, E update) {
    return compareAndSetRaw(checkedByteOffset(i), expect, update);
}

private boolean compareAndSetRaw(long offset, E expect, E update) {
    return unsafe.compareAndSwapObject(array, offset, expect, update);
}

            这种比前面两个更多的是可以自定义数组长度,而不存放数据,但是核心逻辑不变,不过是将Int数据改为Object数据。

            AtomicReference作用是对普通对象的封装,它可以保证你在修改对象引用时的线程安全性

    4.对象属性原子修改器

  • AtomicIntegerFieldUpdater分析
    • 使用介绍
    • 源码分析
    • 说明

代码语言:javascript复制
//AtomicIntegerFieldUpdater 是一个抽象类,具体是由子类 AtomicIntegerFieldUpdaterImpl 实现的
public static <U> AtomicIntegerFieldUpdater<U> newUpdater(Class<U> tclass, String fieldName) {
    return new AtomicIntegerFieldUpdaterImpl<U> (tclass, fieldName, Reflection.getCallerClass());
}


AtomicIntegerFieldUpdaterImpl(final Class<T> tclass, final String fieldName, final Class<?> caller) {
    final Field field;
    final int modifiers;
    try {
        field = AccessController.doPrivileged(
            new PrivilegedExceptionAction<Field>() {
                public Field run() throws NoSuchFieldException {
                    return tclass.getDeclaredField(fieldName);
                }
            });
        modifiers = field.getModifiers();
        sun.reflect.misc.ReflectUtil.ensureMemberAccess(
            caller, tclass, null, modifiers);
        ClassLoader cl = tclass.getClassLoader();
        ClassLoader ccl = caller.getClassLoader();
        if ((ccl != null) && (ccl != cl) &&
            ((cl == null) || !isAncestor(cl, ccl))) {
            sun.reflect.misc.ReflectUtil.checkPackageAccess(tclass);
        }
    } catch (PrivilegedActionException pae) {..} catch (Exception ex) {..}

    if (field.getType() != int.class)
        throw new IllegalArgumentException("Must be integer type");

    if (!Modifier.isVolatile(modifiers))
        throw new IllegalArgumentException("Must be volatile type");

    this.cclass = (Modifier.isProtected(modifiers) &&
                   tclass.isAssignableFrom(caller) &&
                   !isSamePackage(tclass, caller))
                  ? caller : tclass;
    this.tclass = tclass;
    this.offset = U.objectFieldOffset(field);
}
代码语言:javascript复制
public static class Candidate {
    //字段必须是volatile类型
    volatile int score = 0;
    AtomicInteger score2 = new AtomicInteger();
}

public static final AtomicIntegerFieldUpdater<Candidate> scoreUpdater = AtomicIntegerFieldUpdater.newUpdater(Candidate.class, "score");

public static void main(String[] args) throws InterruptedException {

    final Candidate candidate = new Candidate();

    //两者作用相等
    candidate.score2.incrementAndGet();        //基于原子类
    scoreUpdater.incrementAndGet(candidate);  //基于反射

}

            AtomicIntegerFieldUpdater可以线程安全地更新对象中的整型变量。( 基于反射的工具,可用CompareAndSet对volatile int进行原子更新;)

            对于AtomicIntegerFieldUpdater 的使用稍微有一些限制和约束,约束如下:

  1. 字段必须是volatile类型的,在线程之间共享变量时保证立即可见.eg:volatile int value = 3
  2. 字段的描述类型(修饰符public/protected/default/private)与调用者与操作对象字段的关系一致。也就是说调用者能够直接操作对象字段,那么就可以反射进行原子操作。但是对于父类的字段,子类是不能直接操作的,尽管子类可以访问父类的字段。
  3. 只能是实例变量,不能是类变量,也就是说不能加static关键字。
  4. 只能是可修改变量,不能使final变量,因为final的语义就是不可修改。实际上final的语义和volatile是有冲突的,这两个关键字不能同时存在。
  5. 对于AtomicIntegerFieldUpdater和AtomicLongFieldUpdater只能修改int/long类型的字段,不能修改其包装类型(Integer/Long)。如果要修改包装类型就需要使用AtomicReferenceFieldUpdater。

  • AtomicLongFieldUpdater分析(与AtomicIntegerFieldUpdater很相似)
  • AtomicReferenceFieldUpdater分析(与AtomicIntegerFieldUpdater很相似)

    5.原子类型累加器

  • Striped64分析
    • 源码分析
    • 说明

代码语言:javascript复制
// CPU核数,用来决定槽数组的大小
static final int NCPU = Runtime.getRuntime().availableProcessors();

// 数组槽,大小为2的次幂
transient volatile Cell[] cells;

//基数,在两种情况下会使用:
//    1. 没有遇到并发竞争时,直接使用base累加数值
//    2. 初始化cells数组时,必须要保证cells数组只能被初始化一次(即只有一个线程能对cells初始化),其他竞争失败的线程会讲数值累加到base上
transient volatile long base;

//扩容时用到的加锁标记
transient volatile int cellsBusy;

            这是下面四个类的父类,主要逻辑在于Striped64#longAccumulate方法。

  • DoubleAccumulator分析(与LongAccumulator很相似,不过类型不同)
  • DoubleAdder分析(与LongAdder很相似,不过类型不同)
  • LongAccumulator分析
    • 使用介绍

            1.LongAccumulator是LongAdder的增强版。LongAdder只能针对数值的进行加减运算,而 LongAccumulator提供了自定义的函数操作。

            2.如:LongAccumulator accumulator = new LongAccumulator((x, y) -> x * y, 0);// 累乘 x*y

  • 源码分析

代码语言:javascript复制
public LongAccumulator(LongBinaryOperator accumulatorFunction, long identity) {
    this.function = accumulatorFunction;
    base = this.identity = identity;
}

//这里的逻辑可以参考LongAdder#add方法,区别在于function为自定义的函数(也可以说是算术逻辑)
public void accumulate(long x) {
    Cell[] as; long b, v, r; int m; Cell a;
    if ((as = cells) != null ||
        (r = function.applyAsLong(b = base, x)) != b && !casBase(b, r)) {
        boolean uncontended = true;
        if (as == null || (m = as.length - 1) < 0 ||
            (a = as[getProbe() & m]) == null ||
            !(uncontended =
              (r = function.applyAsLong(v = a.value, x)) == v ||
              a.cas(v, r)))
            longAccumulate(x, function, uncontended);
    }
}

LongAdder引入的初衷:解决高并发环境下AtomicInteger,AtomicLong的自旋瓶颈问题。

            AtomicLong是利用了底层的CAS操作来提供并发性的,如addAndGet方法,逻辑是采用自旋的方式不断更新目标值,直到更新成功。

            在并发量较低的环境下,线程冲突的概率比较小,自旋的次数不多。但在高并发环境下,N个线程同时进行自旋操作,会出现大量失败并不断自旋的情况,此时AtomicLong的自旋会成为瓶颈

  • 设计思路

            思路说明(是不是有一种ConcurrentHashMap的分段锁的既视感)

              1.AtomicLong中有个内部变量value保存着实际的long值,所有的操作都是针对该变量进行。               2.也就是说,高并发环境下,value变量其实是一个热点,也就是N个线程竞争一个热点。               3.LongAdder的基本思路就是分散热点,将value值分散到一个数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多。               4.如果要获取真正的long值,只要将各个槽中的变量值累加返回。

            图示

  • 源码分析

            1.LongAdder#sum方法

              代码展示

代码语言:javascript复制
//返回累加的和,也就是"当前时刻"的计数值
//注意: 高并发时,除非全局加锁,否则得不到程序运行中某个时刻绝对准确的值此返回值可能不是绝对准确的,因为调用这个方法时还有其他线程可能正在进行计数累加,方法的返回时刻和调用时刻不是同一个点,在有并发的情况下,这个值只是近似准确的计数值
public long sum() {
    Cell[] as = cells; Cell a;
    long sum = base;
    if (as != null) {
        for (int i = 0; i < as.length;   i) {
            if ((a = as[i]) != null)
                sum  = a.value;
        }
    }
    return sum;
}

              代码说明

                由于计算总和时没有对Cell数组进行加锁,所以在累加过程中可能有其他线程对Cell中的值进行了修改,也有可能对数组进行了扩容,所以sum返回的值并不是非常精确的,其返回值并不是一个调用sum方法时的原子快照值。

            2.LongAdder#add方法

              代码展示

代码语言:javascript复制
public void add(long x) {
    Cell[] as; long b, v; int m; Cell a;
    /**
    * 这里优先判断了cell数组是否为空,之后才判断base字段的cas累加
    * 意味着如果线程不发生竞争,cell数组一直为空,那么所有的累加操作都会累加到base上
    * 而一旦发生过一次竞争导致cell数组不为空,那么所有的累加操作都会优先作用于数组中的对象上
    */
    if ((as = cells) != null || !casBase(b = base, b   x)) {
        /**
        * 这个字段是用来标识在对cell数组中的对象进行累加操作时是否发生了竞争
        * 如果发生了竞争,那么在longAccumulate方法中会多进行一次rehash的自旋
        * 这个在后面的方法中详细说明,这里先有个印象
        * true表示未发生竞争
        */
        boolean uncontended = true;
        // 如果cell数组为空或者长度为0则直接进入主逻辑方法
        if (as == null || (m = as.length - 1) < 0 || 
                /**
                * 这里的getProbe()方法可以认为就是获取线程的hash值
                * hash值与(数组长度-1)进行位与操作后得到对应的数组下标
                * 判断该元素是否为空,如果不为空那么就会尝试累加
                * 否则进入主逻辑方法
                */
                (a = as[getProbe() & m]) == null || 
                    //对数组下标的元素进行cas累加,如果成功了,那么就可以直接返回,否则进入主逻辑方法
                    !(uncontended = a.cas(v = a.value, v   x)))  //到此判断结束
            //主逻辑
            longAccumulate(x, null, uncontended);
    }
}

//Striped64#casBase方法
final boolean casBase(long cmp, long val) {
    return UNSAFE.compareAndSwapLong(this, BASE, cmp, val);
}
final boolean casCellsBusy() {
    return UNSAFE.compareAndSwapInt(this, CELLSBUSY, 0, 1);
}

//Striped64#longAccumulate方法
final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {
    int h; //线程的hash值,在后面的逻辑中会用到,在下面进行赋值,主要用于获取下标
    if ((h = getProbe()) == 0) {
        ThreadLocalRandom.current(); // force initialization
        h = getProbe();
        wasUncontended = true;
    }
    //用来标记某个线程在上一次循环中找到的数组下标是否已经有Cell对象了,如果为true,则表示数组下标为空
    boolean collide = false;    
    //死循环,提供自旋操作          
    for (;;) {
        Cell[] as; Cell a; int n; long v;
        //如果cells数组不为空,且已经被某个线程初始化成功,那么就会进入主逻辑
        if ((as = cells) != null && (n = as.length) > 0) {
            //判断槽位是否为空
            if ((a = as[(n - 1) & h]) == null) {
                //判断能否加锁
                if (cellsBusy == 0) {       
                    Cell r = new Cell(x);   //创建槽位对象
                    //判断能否加锁,能则尝试CAS加锁
                    if (cellsBusy == 0 && casCellsBusy()) {
                        boolean created = false; //设置创建标志,用于跳出循环
                        try {               
                            Cell[] rs; int m, j;
                            //判断cells数组不为空,且对应槽位为null,则将槽位对象赋予过去
                            if ((rs = cells) != null &&
                                (m = rs.length) > 0 &&
                                rs[j = (m - 1) & h] == null) {
                                rs[j] = r;
                                created = true;
                            }
                        } finally {
                            //解锁
                            cellsBusy = 0;
                        }
                        if (created) //创建成功则跳出循环
                            break;
                        continue;           
                    }
                }
                collide = false;
            }
       //这个字段如果为false,说明之前已经和其他线程发过了竞争,即使此时可以直接取尝试cas操作,但是在高并发场景下,这2个线程之后依然可能发生竞争,而每次竞争都需要自旋的话会很浪费cpu资源,因此在这里先直接增加自旋一次,在for的最后会做一次rehash,使得线程尽快地找到自己独占的数组下标
            else if (!wasUncontended)       // CAS already known to fail
                wasUncontended = true;      // Continue after rehash
            //CAS尝试槽位加值
            else if (a.cas(v = a.value, ((fn == null) ? v   x : fn.applyAsLong(v, x))))
                break;
            //这里判断下cpu的核数,因为即使有100个线程,能同时并行运行的线程数等于cpu数,因此如果数组的长度已经大于cpu数目了,那就不应当再扩容了
            else if (n >= NCPU || cells != as)
                collide = false;    
            //走到这里,说明当前循环中根据线程hash值找到的数组下标已经有元素了,如果此时collide为false,说明上一次循环中找到的下边是没有元素的,那么就自旋一次并rehash,如果再次运行到这里,并且collide为true,就说明明竞争非常激烈,应当扩容了(是不是有点疑惑?本质上是采用满足条件进入赋值操作来结束这次循环,当第二次到这里的时候才会跳过,走到下面的扩容)        
            else if (!collide)
                collide = true;
            //尝试加锁
            else if (cellsBusy == 0 && casCellsBusy()) {
                //数组进行2倍扩容,将旧数组遍历值赋予新数组
                try {
                    if (cells == as) {      // Expand table unless stale
                        Cell[] rs = new Cell[n << 1];
                        for (int i = 0; i < n;   i)
                            rs[i] = as[i];
                        cells = rs;
                    }
                } finally {
                    cellsBusy = 0;
                }
                collide = false;
                continue;                   // Retry with expanded table
            }
            h = advanceProbe(h);
        }
        //判断能否加锁,能则尝试CAS加锁
        else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
            boolean init = false;
            //初始化数组为长度2的数组,并创建槽位对象new Cell(x)赋予到数组对应下标槽位中
            try {                           
                if (cells == as) {
                    Cell[] rs = new Cell[2];
                    rs[h & 1] = new Cell(x);
                    cells = rs;
                    init = true;
                }
            } finally {
                cellsBusy = 0;
            }
            if (init)
                break;
        }
        //CAS尝试往base上加数
        else if (casBase(v = base, ((fn == null) ? v   x : fn.applyAsLong(v, x))))
            break;                          
    }
}

             代码逻辑图:

  (4)思想总结

    1. AtomicInteger遇到的问题:单个资源的竞争导致自旋的发生
    2. 解决的思路:将单个对象的竞争扩展为多个对象的竞争(分治的思想,大体像是热点分散的思路)
    3. 扩展的可控性:多个竞争对象需要付出额外的存储空间,因此不能无脑地扩展(极端情况:一个线程一个计数的对象,【这明显不合理,虽然线程很多但是能跑起来也就是CPU核数的数量而已,盲目开线程不合适】)
    4. 问题的分层:因为使用类的时候的场景是不可控的,因此需要根据并发的激烈程度动态地扩展额外的存储空间(类似于synchronized的膨胀)
    5. 策略细节:在自旋的时候增加rehash,此时虽然付出了一定的运算时间计算hash、比较数组对象等,但是这会使得并发的线程尽快地找到专属于自己的对象,在之后就不会再发生任何竞争(特别注意wasUncontended字段的相关注解)

0 人点赞