三、CAS原理与原子类

一、CAS底层原理

1、CAS概述

1、CAS(Compare And Swap)比较并交换,是一条CPU的原子指令,它的功能是判断内存某个位置的值是否为预期值,如果是则更新为新的值,这个过程是原子的
2、CAS并发原语提现在Java语言中就是sun.miscUnSaffe类中的各个方法,调用UnSafe类中的CAS方法,JVM会帮我实现CAS汇编指令。这是一种完全依赖于硬件功能,通过它实现了原子操作。由于CAS是一种系统原语,原语属于操作系统用于范畴,是由若干条指令组成,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许中断,也即是说CAS是一条原子指令,不会造成所谓的数据不一致的问题
3、CAS有3个操作数,内存位置值V,旧的预期值A,要修改的更新值B。当且仅当旧的预期值A和内存值V相等时,将内存值V修改为B,否则什么都不做或重来

在这里插入图片描述

4、简述:
  • 如果线程的期望值跟物理内存的真实值一样,就更新值到物理内存当中,并返回true
  • 如果线程的期望值跟物理内存的真实值不一样,返回是false,那么本次修改失败,那么此时需要重新获得主物理内存的新值
/**
 * @Date: 2022/5/10
 * CAS测试
 */
public class CASTest1 {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(5);
        System.out.println("是否更新成功:" + atomicInteger.compareAndSet(5, 10) + ",值为:" + atomicInteger.get());
        System.out.println("是否更新成功:" + atomicInteger.compareAndSet(5, 20) + ",值为:" + atomicInteger.get());
    }
}
/**
 * 运行结果如下:
 * 是否更新成功:true,值为:10
 * 是否更新成功:false,值为:10
 */

2、CAS使用示例

1、如果不使用CAS,在高并发下,多线程同时修改一个变量的值时需要synchronized加锁(可能有人说可以用Lock加锁,Lock底层的AQS也是基于CAS进行获取锁的)。
2、Java中为我们提供了AtomicInteger原子类(底层基于CAS进行更新数据的),不需要加锁就在多线程并发场景下实现数据的一致性。
/**
 * @Date: 2022/4/27
 * 使用synchronized保证原子性
 * 使用原子类保证原子性
 */
public class VolatileTest5 {
    volatile int num = 0;

    //原子类
    AtomicInteger atomicInteger = new AtomicInteger();

    /**
     * 在addNum方法上加synchronized关键字,保证原子性
     */
    public synchronized void addNum(){
        num ++;
    }

    /**
     * 使用原子类,每次加1
     */
    public void addAtomicInteger() {
        //以原子方式将当前值加1
        atomicInteger.getAndIncrement();
    }
}
查看atomicInteger.getAndIncrement()方法的源代码如下:发现有一个Unsafe类

在这里插入图片描述

3、getAndIncrement源码分析

1、调用过程如下:

在这里插入图片描述

2、流程分析:
  • 假设线程A和线程B两个线程同时执行getAndAddInt操作(分别跑在不同CPU上)。
  • AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存。
  • 线程A通过getIntVolatile(var1, var2)拿到value值3,这时线程A被挂起。
  • 线程B也通过getIntVolatile(var1, var2)方法获取到value值3,此时刚好线程B没有被挂起并执行compareAndSwapInt方法比较内存值也为3,成功修改内存值为4,线程B执行完成。
  • 此时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值数字3和主内存的值数字4不一致,说明该值已经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新来一遍了
  • 线程A重新获取value值,因为变量value被volatile修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功。

二、Unsafe类

1、Unsafe类概述

1、是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。
2、注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务
3、Unsafe对象不能直接调用,要通过反射才能获得
public class Test {
    public static void main(String[] args) throws Exception {
        //通过反射方式获取theUnsafe(也即Unsafe)
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        //设置访问私有变量
        theUnsafe.setAccessible(true);
        //强转为Unsafe对象
        Unsafe unsafe = (Unsafe)theUnsafe.get(null);
        System.out.println(unsafe);
    }
}

2、使用Unsafe进行CAS操作

public class Test2 {
    public static void main(String[] args) throws Exception {
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        //设置访问私有变量
        theUnsafe.setAccessible(true);
        Unsafe unsafe = (Unsafe)theUnsafe.get(null);
        System.out.println(unsafe);

        //1、获取域(属性)的偏移地址
        long id = unsafe.objectFieldOffset(Student.class.getDeclaredField("id"));
        long name = unsafe.objectFieldOffset(Student.class.getDeclaredField("name"));

        //2、执行cas操作
        Student s = new Student();
        //参数:对象,偏移量,初始值,要设置的值
        unsafe.compareAndSwapInt(s, id, 0, 1); //返回true
        unsafe.compareAndSwapObject(s, name, null, "张三");

        //3、验证
        System.out.println(s.getName());//输出张三
    }
}

@Data
class Student{
    private Integer id;
    private String name;
}

3、Unsafe与CAS

1、从源码中发现,内部使用自旋的方式进行CAS更新(while循环进行CAS更新,如果更新失败,则循环再次重试)。
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;
}

public final long getAndAddLong(Object var1, long var2, long var4) {
    long var6;
    do {
        var6 = this.getLongVolatile(var1, var2);
    } while(!this.compareAndSwapLong(var1, var2, var6, var6 + var4));

    return var6;
}

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;
}

public final long getAndSetLong(Object var1, long var2, long var4) {
    long var6;
    do {
        var6 = this.getLongVolatile(var1, var2);
    } while(!this.compareAndSwapLong(var1, var2, var6, var4));

    return var6;
}

public final Object getAndSetObject(Object var1, long var2, Object var4) {
    Object var5;
    do {
        var5 = this.getObjectVolatile(var1, var2);
    } while(!this.compareAndSwapObject(var1, var2, var5, var4));

    return var5;
}
2、Unsafe只提供了3种CAS方法,都是native方法且是原子操作的:
  • compareAndSwapObject
  • compareAndSwapInt
  • compareAndSwapLong
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);

4、Unsafe底层

1、compareAndSwapXXXXX()方法来实现CAS操作,它是一个本地方法,实现位于unsafe.cpp中。
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在内存中的地址,根据偏移量valueOffset,计算value的地址
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  //调用Atomic中的函数cmpxchg来进行比较交换,其中参数x是即将更新的值,参数e是原内存的值
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END
2、通过调用Atomic中的函数cmpxchg来进行比较交换。其中参数x是即将更新的值,参数e是原内存的值。
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");
    //根据操作系统类型调用不同平台下的重载函数,这个在预编译期间编译器会决定调用哪个平台下的重载函数
    return (unsigned int)Atomic::cmpxchg((jint)exchange_value, (volatile jint*)dest, (jint)compare_value);
}
3、根据操作系统类型调用不同平台下的重载函数,这个在预编译期间编译器会决定调用哪个平台下的重载函数。
inline jint Atomic::cmpxchg (jint exchange_value, volatile jint* dest, jint compare_value) {
  //判断是否是多核CPU
  int mp = os::is_MP();
  __asm {
    //三个move指令表示的是将后面的值移动到前面的寄存器上
    mov edx, dest
    mov ecx, exchange_value
    mov eax, compare_value
    //CPU原语级别,CPU触发
    LOCK_IF_MP(mp)
    //比较并交换指令
    //cmpxchg: 即“比较并交换”指令
    //dword: 全称是double word表示两个字,一共四个字节
    //ptr: 全称是pointer,与前面的dword连起来使用,表明访问的内存单元是一个双字单元 
    //将eax寄存器中的值(compare_value)与[edx]双字内存单元中的值进行对比,
    //如果相同,则将ecx寄存器中的值(exchange_value)存入[edx]内存单元中
    cmpxchg dword ptr [edx], ecx
  }
}
4、总结:
  • CAS是靠硬件实现的从而在硬件层面提升效率,最底层还是交给硬件来保证原子性和可见性
  • 实现方式是基于硬件平台的汇编指令,在intel的CPU中(X86机器上),使用的是汇编指令cmpxchg指令
  • 核心思想就是:比较要更新变量的值V和预期值E(compare),相等才会将V的值设为新值N(swap)如果不相等自旋再来

5、CAS缺点

1、循环时间长开销大:方法中有一个do while,如果CAS失败,会一直进行尝试,如果长时间不成功,可能会给CPU带来很大的开销。

在这里插入图片描述

2、只能保证一个共享变量的原子操作:
  • 当对一个共享变量执行操作时,可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。
  • 也可以把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i = 2,j = a,合并一下ij = 2a,然后用CAS来操作ij。
  • 从Java 1.5开始,JDK提供了AtomicReference类(原子引用)来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。
3、ABA问题:
  • 因为CAS需要在操作值的时候,检查值有没有发生变化,比如没有发生变化则更新,比如线程1从内存位置V中取出A,线程2也从内存中取出A,线程2将值变成了B,然后又将值改回了A,这时候线程1进行CAS操作发现内存中值仍然是A,然后线程1修改成功。使用CAS进行检查时则会发现它的值没有发生变化,但是实际上却变化了。
  • ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A->B->A就会变成1A->2B->3A。
  • 从Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

6、ABA问题示例

/**
 * @Date: 2022/5/14
 * ABA问题产生
 */
public class ABAQuestion {
    static AtomicReference<String> reference = new AtomicReference<>("A");

    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            //先将A改为B,再又改回A
            System.out.println("线程 " + Thread.currentThread().getName() + " change A -> B " + reference.compareAndSet(reference.get(),"B"));
            System.out.println("线程 " + Thread.currentThread().getName() + " change B -> A " + reference.compareAndSet(reference.get(), "A"));
        },"t1").start();

        //暂停1s,保证t1线程能够完成一次ABA操作
        Thread.sleep(1000);
        new Thread(() -> {
            System.out.println("线程 " + Thread.currentThread().getName() + " change A -> C " + reference.compareAndSet(reference.get(), "C"));
        },"t2").start();
    }
}
/**
 * 运行结果如下:
 * 线程 t1 change A -> B true
 * 线程 t1 change B -> A true
 * 线程 t2 change A -> C true
 */
分析:主线程仅能判断出共享变量的值与最初值A是否相同,不能感知到这种从A改为B又改回A的情况。

7、解决ABA问题

1、可以使用AtomicStampedReference与AtomicMarkableReference原子引用类来解决ABA问题。
2、区别:
  • AtomicStampedReference 本质是有一个int值作为版本号,每次更改前先取到这个int值的版本号,等到修改的时候,比较当前版本号与当前线程持有的版本号是否一致,如果一致,则进行修改,并修改版本号(通常+1能明确知道被修改过几次)。
  • AtomicMarkableReference不再用int标识引用,而是将一个boolean值作是否有更改的标记,本质就是它的版本号只有两个,true和false,修改的时候在这两个版本号之间来回切换,这样做并不能解决ABA的问题,只是会降低ABA问题发生的几率而已
/**
 * @Date: 2022/5/14
 * 解决ABA问题
 */
public class SolveABAQuestion {
    static AtomicStampedReference<String> reference1 = new AtomicStampedReference<>("A",1);

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            //获取版本号
            int stamp = reference1.getStamp();
            System.out.println("线程 " + Thread.currentThread().getName() + " 初始版本号 " + stamp);
            //暂停2s,保证t2线程能够取得初始版本号
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //先将A改为B,再又改回A,每次修改版本号+1
            boolean oneFlag = reference1.compareAndSet("A", "B", reference1.getStamp(), reference1.getStamp() + 1);
            System.out.println("线程 " + Thread.currentThread().getName() + " change A -> B " + oneFlag + " 最新值为 " + reference1.getReference() + " 版本号为 " + reference1.getStamp());

            boolean twoFlag = reference1.compareAndSet("B", "A", reference1.getStamp(), reference1.getStamp() + 1);
            System.out.println("线程 " + Thread.currentThread().getName() + " change B -> A " + twoFlag + " 最新值为 " + reference1.getReference() + " 版本号为 " + reference1.getStamp());
        }, "t1").start();

        new Thread(() -> {
            //获取版本号
            int stamp = reference1.getStamp();
            System.out.println("线程 " + Thread.currentThread().getName() + " 初始版本号 " + stamp);
            //暂停2s,保证t1线程能够完成一次ABA操作
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean flag = reference1.compareAndSet("A", "C", stamp, stamp + 1);
            System.out.println("线程 " + Thread.currentThread().getName() + " change A -> C " + flag + " 最新值为 " + reference1.getReference() + " 版本号为 " + reference1.getStamp());
        }, "t2").start();
    }
}

三、原子类

1、概述

1、从JDK1.5开始提供了java.util.concurrent.atomic包(以下简称Atomic包),这个包中的原子操作类提供了一种用法简单、性能高效、线程安全地更新一个变量的方式。
2、因为变量的类型有很多种,所以在Atomic包里一共提供了16个类,属于4种类型的原子更新方式,分别是原子更新基本类型、原子更新数组、原子更新引用和原子更新属性(字段)。 Atomic包里的类基本都是使用Unsafe实现的包装类。

在这里插入图片描述

2、基本类型原子类

1、基本类型原子类:AtomicBoolean、AtomicInteger、AtomicLong
2、以AtomicInteger为例的常用API:
  • public final int get():获取当前的值。
  • public final int getAndSet(int newValue):获取当前的值,并设置新的值
  • public final int getAndIncrement():获取当前的值,并加1
  • public final int incrementAndGet():将当前值加1,并返回
  • public final int getAndDecrement():获取当前的值,并减1
  • public final int decrementAndGet():将当前值减1,并返回
  • public final int getAndAdd(int delta):获取当前的值,并加上预期的值
  • public final int addAndGet(int delta):加上预期的值,并返回
  • public final boolean compareAndSet(int expect, int update):如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)

3、数组类型原子类

1、数组类型原子类:
  • AtomicIntegerArray:原子更新整型数组里的元素
  • AtomicLongArray:原子长整型数组里的元素
  • AtomicReferenceArray:原子引用类型数组里的元素

4、引用类型原子类

1、基本类型原子类只能更新一个变量,如果需要原子更新多个变量,需要使用引用类型原子类。
2、引用类型原子类:
  • AtomicReference:原子更新引用类型
  • AtomicStampedReference:携带版本号的引用类型原子类,可以解决ABA问题,用int值作为版本号
  • AtomicMarkableReference:原子更新带有标记位的引用类型对象,用boolean值作是否有更改的标记,并不能很好的解决ABA问题
/**
 * @Author: ye.yanbin
 * @Date: 2022/5/14
 */
public class AtomicMarkableReferenceTest {
    static AtomicMarkableReference<String> reference1 = new AtomicMarkableReference<>("A",false);

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            //获取标记
            boolean marked = reference1.isMarked();
            System.out.println("线程 " + Thread.currentThread().getName() + " 初始标记 " + marked);
            //暂停2s,保证t2线程能够取得初始版本号
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean flag = reference1.compareAndSet("A", "B", marked, !marked);
            System.out.println("线程 " + Thread.currentThread().getName() + " change A -> B " + flag + " 最新值为 " + reference1.getReference() + " 标记为 " + reference1.isMarked());
        }, "t1").start();

        new Thread(() -> {
            //获取标记
            boolean marked = reference1.isMarked();
            System.out.println("线程 " + Thread.currentThread().getName() + " 初始标记 " + marked);
            //暂停2s,保证t1线程能够完成一次ABA操作
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            boolean flag = reference1.compareAndSet("A", "C", marked, !marked);
            System.out.println("线程 " + Thread.currentThread().getName() + " change A -> C " + flag + " 最新值为 " + reference1.getReference() + " 版本号为 " + reference1.isMarked());
        }, "t2").start();
    }
}
/**
 * 运行结果如下:
 * 线程 t1 初始标记 false
 * 线程 t2 初始标记 false
 * 线程 t1 change A -> B true 最新值为 B 标记为 true
 * 线程 t2 change A -> C false 最新值为 B 版本号为 true
 */

5、对象属性修改原子类

1、保护的是对象的属性(或成员变量)的安全性,以一种线程安全的方式操作非线程安全对象内的某些字段。在内部通过Unsafe类的native方法保证操作的原子性
2、对象属性修改原子类:
  • AtomicIntegerFieldUpdater:基于反射的工具类,可以原子性的更新指定对象中int类型字段
  • AtomicLongFieldUpdater:基于反射的工具类,可以原子性的更新指定对象中long类型字段
  • AtomicReferenceFieldUpdater:基于反射的工具类,可以原子性的更新指定对象中引用类型字段。
3、使用规则:
  • 属性字段必须是volatile类型,用于保证可见性,否则报错java.lang.IllegalArgumentException: Must be volatile type
  • 属性类型必须和原子类中的类型一致
  • 属性字段必须非private、protected(如果是当前类是可以的)
  • 属性字段只能是实例变量,不能是类变量(static)
  • 属性字段不能是final变量,因为这样的字段不可修改
  • 如果要处理Integer和Long类型,则需要使用AtomicReferenceFieldUpdater
/**
 * @Date: 2022/5/14
 */
public class AtomicIntegerFieldUpdaterTest {
    public static void main(String[] args) throws InterruptedException {
        User user = new User();
        for (int i = 1; i <= 10000; i++) {
            int j = i;
            new Thread(() -> {
                // user.addScore(); //最后结果可能小于10000
                user.addScore(user);//最后结果为10000
            }, String.valueOf(i)).start();
        }
        Thread.sleep(5000);
        System.out.println("结果:" + user.score);
    }
}

class User {
    private String name;

    public volatile int score;

    AtomicIntegerFieldUpdater<User> fieldUpdater = AtomicIntegerFieldUpdater.newUpdater(User.class, "score");

    public void addScore() {
        score = score + 1;
    }

    /**
     * 不加锁的方式,进行自增操作,使用原子更新器来操作变量,保证原子性
     */
    public void addScore(User user) {
        fieldUpdater.incrementAndGet(user);
    }
}

四、原子增强类

1、概述

1、JDK 1.8新增的部分,是对AtomicLong等类的改进。比如LongAccumulator与LongAdder在高并发环境下比AtomicLong更高效(减少乐观锁的重试次数)。
2、包含如下几个类:
  • DoubleAccumulator:一个或多个变量共同维护使用提供的函数更新的运行double值
  • DoubleAdder:一个或多个变量共同保持初始为零的double和
  • LongAccumulator:一个或多个变量共同维护使用提供的函数更新的运行long值
  • LongAdder:一个或多个变量共同保持初始为零的long和

2、常用方法以LongAdder为例

方法说明
LongAdder()只有一个无参的构造器,会构造一个sum=0的实例对象
void add(long x)增量计算,+x
void increment()自增(+1)
void decrement()自减(-1)
long sum()返回当前的总和,不存在并发的情况下,会返回精确值,存在并发下,不保证精确值
void reset()重置为零,可用于替代重新new一个LongAdder,只能在没有并发情况下使用
long sumThenReset()计算sum的和并重置sum为0

3、性能比较

/**
 * @Author: ye.yanbin
 * @Date: 2022/5/14
 * 性能对比
 */
public class compareAtomicLongAndLongAdder {
    /** 累加次数 */
    private static final int ADD_COUNT = 10000;
    /** 线程数 */
    private static final int THREAD_COUNT = 50;

    public static void main(String[] args) throws InterruptedException {
        long startTime, endTime;
        //资源类
        CompareResource compareResource = new CompareResource();
        //计数器
        CountDownLatch c1 = new CountDownLatch(THREAD_COUNT);
        CountDownLatch c2 = new CountDownLatch(THREAD_COUNT);
        CountDownLatch c3 = new CountDownLatch(THREAD_COUNT);
        CountDownLatch c4 = new CountDownLatch(THREAD_COUNT);
        //开始比较
        startTime = System.currentTimeMillis();
        for (int i = 1; i <= THREAD_COUNT; i++) {
            new Thread(() -> {
                try {
                    for (int j = 1; j <= 100 * ADD_COUNT; j++) {
                        compareResource.addSynchronizedNum();
                    }
                } finally {
                    c1.countDown();
                }
            }, String.valueOf(i)).start();
        }
        c1.await();
        endTime = System.currentTimeMillis();
        System.out.println("使用Synchronized方式计算耗时:" + (endTime - startTime) + " 毫秒" + " 最后结果: " + compareResource.number);

        startTime = System.currentTimeMillis();
        for (int i = 1; i <= THREAD_COUNT; i++) {
            new Thread(() -> {
                try {
                    for (int j = 1; j <= 100 * ADD_COUNT; j++) {
                        compareResource.addAtomicLongNum();
                    }
                } finally {
                    c2.countDown();
                }
            }, String.valueOf(i)).start();
        }
        c2.await();
        endTime = System.currentTimeMillis();
        System.out.println("使用AtomicLong方式计算耗时:" + (endTime - startTime) + " 毫秒" + " 最后结果: " + compareResource.atomicLong.get());

        startTime = System.currentTimeMillis();
        for (int i = 1; i <= THREAD_COUNT; i++) {
            new Thread(() -> {
                try {
                    for (int j = 1; j <= 100 * ADD_COUNT; j++) {
                        compareResource.addLongAdderNum();
                    }
                } finally {
                    c3.countDown();
                }
            }, String.valueOf(i)).start();
        }
        c3.await();
        endTime = System.currentTimeMillis();
        System.out.println("使用LongAdder方式计算耗时:" + (endTime - startTime) + " 毫秒" + " 最后结果: " + compareResource.longAdder.sum());

        startTime = System.currentTimeMillis();
        for (int i = 1; i <= THREAD_COUNT; i++) {
            new Thread(() -> {
                try {
                    for (int j = 1; j <= 100 * ADD_COUNT; j++) {
                        compareResource.addLongAccumulatorNum();
                    }
                } finally {
                    c4.countDown();
                }
            }, String.valueOf(i)).start();
        }
        c4.await();
        endTime = System.currentTimeMillis();
        System.out.println("使用LongAccumulator方式计算耗时:" + (endTime - startTime) + " 毫秒" + " 最后结果: " + compareResource.longAccumulator.get());
    }
}

/**
 * 资源类
 */
class CompareResource {
    public int number = 0;

    /**
     * 使用synchronized进行累加
     */
    public synchronized void addSynchronizedNum() {
        number ++;
    }

    /**
     * 使用原子类进行累加
     */
    AtomicLong atomicLong = new AtomicLong(0);
    public void addAtomicLongNum() {
        atomicLong.getAndIncrement();
    }

    /**
     * 使用LongAdder进行累加
     */
    LongAdder longAdder = new LongAdder();
    public void addLongAdderNum() {
        longAdder.increment();
    }

    /**
     * 使用LongAccumulator进行累加,是long类型的聚合器,
     * 需要传入一个long类型的二元操作,可以用来计算各种聚合操作,包括加乘等
     */
    LongAccumulator longAccumulator = new LongAccumulator((x, y) -> x + y, 0);
    public void addLongAccumulatorNum() {
        longAccumulator.accumulate(1);
    }
}
/**
 * 运行结果如下:可以发现LongAdder与LongAccumulator计算耗时最少,性能最高
 * 使用Synchronized方式计算耗时:3522 毫秒 最后结果: 50000000
 * 使用AtomicLong方式计算耗时:1696 毫秒 最后结果: 50000000
 * 使用LongAdder方式计算耗时:357 毫秒 最后结果: 50000000
 * 使用LongAccumulator方式计算耗时:358 毫秒 最后结果: 50000000
 */

五、LongAdder原理分析

1、架构

在这里插入图片描述

2、Striped64类

1、LongAdder只有一个空构造器,其本身也没有什么特殊的地方,所有复杂的逻辑都在它的父类Striped64中
2、Striped64类实现一些核心操作,处理64位数据。它只有一个空构造器,初始化时,通过Unsafe获取到类字段的偏移值,以便后续CAS操作
abstract class Striped64 extends Number {
    Striped64() {
    }
    // Unsafe mechanics
    private static final sun.misc.Unsafe UNSAFE;
    private static final long BASE;
    private static final long CELLSBUSY;
    private static final long PROBE;
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> sk = Striped64.class;
            BASE = UNSAFE.objectFieldOffset
                (sk.getDeclaredField("base"));
            CELLSBUSY = UNSAFE.objectFieldOffset
                (sk.getDeclaredField("cellsBusy"));
            Class<?> tk = Thread.class;
            PROBE = UNSAFE.objectFieldOffset
                (tk.getDeclaredField("threadLocalRandomProbe"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }
}
几个重要的成员变量或方法的定义
/** 
 * CPU核数,用来决定cells(槽数组)的最大长度,cells扩容时会使用到
 */
static final int NCPU = Runtime.getRuntime().availableProcessors();

/**
 * 槽数组,大小为2的幂,2,4,8,16...等
 */
transient volatile Cell[] cells;

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

/**
 * 锁标识:0->无锁,1->持有锁
 * cells初始化或扩容时,通过CAS操作将此标识设置为1(加锁状态),初始化或者扩容完毕时,将此标识设置为0(无锁状态)
 */
transient volatile int cellsBusy;

/**
 * 通过CAS操作修改cellsBusy的值,CAS成功表示获取锁,返回true
 */
final boolean casCellsBusy() {
    return UNSAFE.compareAndSwapInt(this, CELLSBUSY, 0, 1);
}

/**
 * 获取当前线程的hash值
 */
static final int getProbe() {
    return UNSAFE.getInt(Thread.currentThread(), PROBE);
}

 /**
  * 重置当前线程的hash值
  */
static final int advanceProbe(int probe) {
    probe ^= probe << 13;   // xorshift
    probe ^= probe >>> 17;
    probe ^= probe << 5;
    UNSAFE.putInt(Thread.currentThread(), PROBE, probe);
    return probe;
}
内部 Cell 类(也就是槽),每个Cell对象存有一个value值,可以通过Unsafe来CAS操作它的值
@sun.misc.Contended static final class Cell {
    volatile long value;
    Cell(long x) { value = x; }
    //最重要的方法,用来CAS方式进行累加,cmp表示旧值,val表示新值
    final boolean cas(long cmp, long val) {
        return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
    }

    // Unsafe mechanics
    private static final sun.misc.Unsafe UNSAFE;
    private static final long valueOffset;
    static {
        try {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> ak = Cell.class;
            valueOffset = UNSAFE.objectFieldOffset
                (ak.getDeclaredField("value"));
        } catch (Exception e) {
            throw new Error(e);
        }
    }
}

3、LongAdder的基本原理

1、LongAdder的基本思想是分散热点,将value值分散到一个Cell数组中,不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率也就小很多,如果要获取真正的long值,只要将各个槽中的变量值累加返回。
2、sum()会将所有Cell数组中的value和base累加作为返回值,核心思想就是将AtomicLong的一个value的更新压力分散到多个value上进行更新
3、LongAdder原理
  • 在无竞争的情况,跟AtomicLong一样,对同一个base进行操作(无并发,单线程下直接CAS操作更新base值,直接累加到变量base上)。
  • 当出现竞争关系时则是采用化整为零的做法,以空间换时间,用一个数组cells,将一个value拆分进这个数组cells。多个线程需要同时对value进行操作时,可以对线程id进行hash得到hash值,再根据hash值映射到这个数组cells的某个下标,再对该下标所对应的值进行自增操作。当所有线程操作完毕,将数组cells的所有值和无竞争值base都加起来作为最终结果

在这里插入图片描述

4、 最终结果 = B a s e + ∑ i = n n C e l l [ i ] 最终结果 = Base + \sum_{i=n}^{n}Cell[i] 最终结果=Base+i=nnCell[i]

4、LongAdder源码之add方法分析

public void add(long x) {
    /*
     * as是Striped64中的cells数组属性
     * b是Striped64中的base属性
     * v是当前线程hash到的具体的Cell中存储的值
     * m是cells的长度减1,hash时作为掩码使用
     * a是当前线程hash到的具体的Cell
     */
    Cell[] as; long b, v; int m; Cell a;
    /*
     * 首次((as = cells) != null)一定是false,此时走casBase方法,以CAS的方式更新base值,当CAS失败时,才会走到if中,有两个条件:
     * 1、as有值,表示已经发生过竞争,Cell[]已经被创建,进入if
     * 2、CAS给base累加时失败了,说明其他线程先修改了base,发生了竞争,进入if
     */
    if ((as = cells) != null || !casBase(b = base, b + x)) {
        // true:无竞争    false:竞争激烈,多个线程hash到同一个Cell,可能需要扩容
        boolean uncontended = true;
        /*
         * 几个判断条件
         * 1、cells为空,说明正在出现竞争,也即CAS给base累加时失败了
         * 2、一般不会出现
         * 3、当前线程通过hash到的Cell为空,说明当前线程还没有更新过Cell,应初始化一个Cell
         * 4、更新当前线程所在的Cell失败,说明竞争很激烈,多个线程通过hash到了同一个Cell,应该扩容
         */
        if (as == null || (m = as.length - 1) < 0 ||
            /*
             * getProbe()方法返回的是线程中的threadLocalRandomProbe字段
             * 它是通过随机数生成的一个值,对于一个确定的线程,这个值是固定的
             */
            (a = as[getProbe() & m]) == null || !(uncontended = a.cas(v = a.value, v + x)))
            // 调用Striped64中的方法处理
            longAccumulate(x, null, uncontended);
    }
}
1、流程说明:
  • 如果Cells数组为空,尝试CAS更新base字段,成功则退出。
  • 如果Cells数组为空,CAS更新base字段失败,表示出现竞争,uncontended为true,调用longAccumulate。
  • 如果Cells数组不为空,当前线程通过getProbe&m得到的Cell为空,uncontended为true,调用longAccumulate。
  • 如果Cells数组不为空,当前线程通过getProbe&m得到的Cell不为空,CAS更新Cell中的值,成功则返回,否则uncontended设置为false,取反之后进入if,调用longAccumulate。
2、流程图如下:

在这里插入图片描述

5、LongAdder源码之longAccumulate方法分析

1、方法入参说明:
  • long x:需要增加的值,一般默认都是1
  • LongBinaryOperator fn:默认传递的null,表示相加,在LongAccumulator可以自定义计算
  • boolean wasUncontended:竞争标识,false表示有竞争,只有cells初始化之后,并且当前线程CAS竞争修改失败,才会是false

在这里插入图片描述

2、总体结构:

在这里插入图片描述

final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) {
    // 存储线程的probe值
    int h;
    // 如果getProbe方法返回0,说明随机数还未初始化
    if ((h = getProbe()) == 0) {
        // 使用ThreadLocalRandom为当前线程重新计算一个hash值,强制初始化
        ThreadLocalRandom.current(); // force initialization
        // 重新获取probe值,hash值被重置就好比一个全新的线程一样,所以设置竞争状态wasUncontended为true
        h = getProbe();
        // 重新计算了当前线程的hash后认为此次不算是一次竞争,都未初始化,肯定还不存在竞争激烈,状态为true
        wasUncontended = true;
    }
    // 如果hash取模映射到的Cell单元不是null,则为true,可以看作是否扩容
    boolean collide = false;                // True if last slot nonempty
    
	for (;;) {
        Cell[] as; Cell a; int n; long v;
        //这一段先不看,后面再分析
        if ((as = cells) != null && (n = as.length) > 0) {
            //......
        }
        /*
         * 首次新建,cells数组还没有初始化的情况:对应cells创建流程
         * 1、cellsBusy锁标识,初始为0(没有加锁),通过casCellsBusy()方法尝试占有锁,把cellsBusy状态位从0变为1
         * 2、加锁成功, 初始化cells, 开始长度为2, 并填充一个cell,跳出循环 
         */
        else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
            // 初始化状态为false
            boolean init = false;
            // 初始化
            try {                           // Initialize table
                /*
                 * 判断两次cells == as,双重检查机制,如果不双重检查,就会再次new一个Cell数组,
                 * 上一个线程对应数组中的值将会被篡改
                 */
                if (cells == as) {
                    // 新建一个大小为2的Cell数组
                    Cell[] rs = new Cell[2];
                    /*
                     * 通过当前线程hash到数组中的索引位置,并创建一个新的Cell元素,并将x值赋值给Cell的value变量,默认为1
                     * h & 1类似于HashMap常用的计算散列桶index的算法,得到一个具体的索引位置
                     */
                    rs[h & 1] = new Cell(x);
                    // 将cells引用指向新建的Cell数组地址
                    cells = rs;
                    // 初始化状态设置为true
                    init = true;
                }
            } finally {
                // 将锁标识设置为0
                cellsBusy = 0;
            }
            // 初始化完成,结束循环
            if (init)
                break;
        }
        /*
         * 上两种情况失败, 尝试给base累加值,表示其它线程正在初始化,多个线程正在更新base的值
         */
        else if (casBase(v = base, ((fn == null) ? v + x :
                                    fn.applyAsLong(v, x))))
            break;                          // Fall back on using base
    }
}
最复杂的一段代码分析:Cell数组已经初始化的情况,流程图如下:

在这里插入图片描述

Cell[] as; Cell a; int n; long v;
// cells已经初始化
if ((as = cells) != null && (n = as.length) > 0) {
    // 当前线程的hash值运算后映射得到的Cell数组中索引位置元素是否为null,如果为null说明该Cell元素没有被使用
    if ((a = as[(n - 1) & h]) == null) {
        // 锁标志位为0,说明还没有正在扩容
        if (cellsBusy == 0) {       // Try to attach new Cell
            // 创建一个新的Cell元素,并将x值赋值给Cell的value变量,默认为1
            Cell r = new Cell(x);   // Optimistically create
            // CAS尝试加锁,成功后cellsBusy=1
            if (cellsBusy == 0 && casCellsBusy()) {
                // 结束标志位
                boolean created = false;
                try {               // Recheck under lock
                    Cell[] rs; int m, j;
                    // 获取到锁的情况下,再进行一次检验逻辑,检验成功就将新的Cell添加到cells中
                    if ((rs = cells) != null &&
                        (m = rs.length) > 0 &&
                        rs[j = (m - 1) & h] == null) {
                        // 将新建的Cell元素添加到cells中
                        rs[j] = r;
                        // 结束标志位设置为true,用于结束循环
                        created = true;
                    }
                } finally {
                    // 将锁标志位设置为0,类似释放锁
                    cellsBusy = 0;
                }
                // 结束循环
                if (created)
                    break;
                // 跳出循环,继续自旋
                continue;           // Slot is now non-empty
            }
        }
        collide = false;
    }
    /*
     * 有竞争,当前线程竞争修改失败,入参wasUncontended为false,重新设置wasUncontended为true
     * 紧接着执行h = advanceProbe(h)重置当前线程的hash,重新循环
     */
    else if (!wasUncontended)       // CAS already known to fail
        wasUncontended = true;      // Continue after rehash
    /*
     * 说明当前线程对应的数组索引位置元素有值,也重置过hash值
     * 这时通过CAS操作尝试对当前元素中的value值进行累加x操作,x默认为1,成功直接结束循环,
     * 失败重置hash,重新循环
     */
    else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
        break;
    /*
     * 扩容判断,如果n大于CPU最大数量,说明不可扩容
     * 紧接着执行h = advanceProbe(h)重置当前线程的hash,重新循环
     */
    else if (n >= NCPU || cells != as)
        collide = false;            // At max size or stale
    /*
     * 如果collide为false则修改为true,然后重新计算当前线程的hash,重新循环
     * 确保collide为false进入此分支, 就不会进入下面的else if进行扩容了
     */
    else if (!collide)
        collide = true;
    /*
     * 进行扩容操作
     */
    else if (cellsBusy == 0 && casCellsBusy()) {
        try {
            // 当前的cells数组和最先赋值的as是同一个,表示没有被其他线程扩容过
            if (cells == as) {      // Expand table unless stale
                // 按位左移1位来操作,扩容大小为之前容量的2倍
                Cell[] rs = new Cell[n << 1];
                for (int i = 0; i < n; ++i)
                    // 扩容之后再将之前数组的元素拷贝到新数组中
                    rs[i] = as[i];
               	// cells引用指向扩容后的新数组
                cells = rs;
            }
        } finally {
            // 将锁标识设置为0
            cellsBusy = 0;
        }
        // 设置扩容标识位false,继续循环执行
        collide = false;
        continue;                   // Retry with expanded table
    }
    h = advanceProbe(h);
}

6、最终通过sum方法将结果统计

1、sum方法会将当前时刻所有Cell数组中的value和base累加作为返回值
2、此返回值不是绝对准确的,因为调用这个方法时,可能还有其他线程正在进行计数累加。
3、方法的返回时刻和调用时刻不是同一个点,在有并发的情况下,这个值只是近似准确的计数值。
4、高并发时,除非全局加锁,否则得不到程序运行中某个时刻绝对准确的值。
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;
}

7、AtomicLong与LongAdder对比

1、AtomicLong:
  • 线程安全,可允许一些性能损耗,对结果要求精确时可使用。
  • 是多个线程针对单个热点值value进行原子操作。
2、LongAdder:
  • 当需要在高并发下有较好的性能表现,且对值的精确度要求不高时可以使用。
  • 是每个线程拥有自己的槽,各个线程一般只对自己槽中的那个值进行CAS操作。

8、伪共享原理

1、@sun.misc.Contended:对某字段加上该注解则表示该字段会单独占用一个缓存行(Cache Line)。这里的缓存行是指CPU缓存(L1、L2、L3)的存储单元,常见的缓存行大小为64字节。
2、单独使用一个缓存行有什么作用 — 避免伪共享
  • 当一个CPU要修改某共享变量A时会先锁定自己缓存里A所在的缓存行,并且把其他CPU缓存上相关的缓存行设置为无效。但如果被锁定或失效的缓存行里,还存储了其他不相干的变量B,其他线程此时就访问不了B,或者由于缓存行失效需要重新从内存中读取加载到缓存里,这里造成了开销,所以让共享变量A单独使用一个缓存行就不会影响到其他线程的访问。

在这里插入图片描述

3、缓存和内存的速度比较:
从CPU到大约需要的时间周期
寄存器1 cycle(4GHz 的 CPU 约为0.25ns)
L13-4 cycle
L210-20 cycle
L340-45 cycle
内存120-240 cycle
4、因为CPU与内存的速度差异很大,需要靠预读数据至缓存来提升效率。而缓存以缓存行为单位,每个缓存行对应着一块内存,一般是64 byte(8个long),缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中,CPU要保证数据的一致性,如果某个CPU核心更改了数据,其他CPU核心对应的整个缓存行必须失效。

在这里插入图片描述

  • 因为Cell是数组形式,在内存中是连续存储的,一个Cell为24字节(16字节的对象头和8字节的value),因为此缓存行可以存下2个的Cell对象,这样就会出现问题:Core-0要修改Cell[0],Core-1要修改Cell[1]
  • 无论谁修改成功,都会导致对方Core的缓存行失败,比如Core-0中Cell[0] = 6000,Cell[1]=8000要累加Cell[0] = 6001,Cell[1]=8000,这时会让Core-1的缓存行失效。
  • @sun.misc.Contended用来解决这个问题,它的原理是在使用此注解的对象或字段的前后各增加128字节大小的padding,从而让CPU将对象预读至缓存时占用不同的缓存行,这样,不会造成对方缓存行的失效。

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值