JUC进阶05——CAS

JUC进阶05------CAS

CAS是什么

概述

CAS(Compare And Swap) ,中文翻译为比较并交换 。用于实现并发算法时常用到的一种技术,保证并发编程中共享变量的原子操作,实现多线程环境下的同步和共享。

CAS主要通过java.util.concurrent.atomic包下的原子类来实现。常见的原子类有AtomicIntegerAtomicLongAtomicBoolean等。这些原子类提供了一系列的原子操作方法,保证了操作的原子性,避免了线程安全问题

java.util.concurrent.atomic 包下的CAS原子类:

CAS操作过程

CAS操作包含三个操作数:

  • 内存位置V
  • 旧的预期值A
  • 新值B

CAS执行过程如下:

把内存位置V的值和预期值A进行比较

  • 判断是否相等
    • 相等:则将该位置的值更新为新值B
    • 不想等:不做任何操作

过程如下图所示:

CAS操作的结果是通过返回值来判断是否成功。如果成功,说明内存位置的值在操作前后没有被其他线程修改过;如果失败,说明有其他线程在操作期间修改了内存位置的值。

CAS操作是原子性的,多个线程操作只有一个线程成功。也就是说,它不会因为其他线程的干扰而失败,从而保证了多线程同步的正确性。

CAS 操作时乐观锁的一种实现方式,它总是认为自己可以成功完成操作。当多个线程同时使用CAS操作一个共享变量时,只有一个线程会获取到锁并成功更新,其余线程均会失败。失败的线程不会被挂起,仅是被告知失败,并且允许再次尝试或放弃操作。基于这样的原理,CAS操作即使没有锁也可以发现其他线程对当前线程的干扰并进行恰当的处理

CAS 使用Demo

java 复制代码
public class CasDemo {
    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(3);
        System.out.println("第1次操作:"+atomicInteger.compareAndSet(3,5)+ "\t" + atomicInteger.get());
        System.out.println("第2次操作:"+atomicInteger.compareAndSet(3,5)+ "\t" + atomicInteger.get());
    }
}

执行结果:

arduino 复制代码
第1次操作:true	5
第2次操作:false	5

CAS 原理

从AtomicInteger getAndIncrement() 方法源码说起

AtomicInteger.class

java 复制代码
public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    //使用 volatile修饰变量 保证了多线程间的可见性
    private volatile int value;

/**
 * Atomically decrements by one the current value.
 *
 * @return the previous value
 */
public final int getAndDecrement() {
    //通过valueOffset,直接通过内存地址,获取到值,然后进行加1的操作
    return unsafe.getAndAddInt(this, valueOffset, -1);
}

从源码中可以看到 AtomicInteger getAndIncrement()方法底层调用的是 unsafe类getAndInt()方法

Unsafe类是什么?

Unsafe类是CAS的核心类 ,由于Java方法无法直接访问底层系统,需要通过本地(native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因此Java中CAS操作的执行依赖于Unsafe类的方法

注意:Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的所有方法都直接调用操作系统底层资源执行相应任务

所以Atomic修饰的包装类,能够保证原子性,依靠的就是底层的unsafe类

从上面的源码可以看到,AtomicInteger类主要利用CAS+volatile和native方法来保证原子操作,从而避免synchronized的高开销,执行效率大为提升

value变量使用volatile修饰,保证了多线程之间的内存可见性

var5:就是我们从主内存中拷贝到工作内存中的值(每次都要从主内存拿到最新的值到自己的本地内存,然后执行compareAndSwapInt()在再和主内存的值进行比较。因为线程不可以直接越过高速缓存,直接操作主内存,所以执行上述方法需要比较一次,在执行加1操作)

那么操作的时候,需要比较工作内存中的值,和主内存中的值进行比较

假设执行 compareAndSwapInt返回false,那么就一直执行 while方法,直到期望的值和真实值一样

  • val1:AtomicInteger对象本身

  • var2:该对象值得引用地址

  • var4:需要变动的数量

  • var5:用var1和var2找到的内存中的真实值

    • 用该对象当前的值与var5比较
    • 如果相同,更新var5 + var4 并返回true
    • 如果不同,继续取值然后再比较,直到更新完成

这里没有用synchronized,而用CAS,这样提高了并发性,也能够实现一致性,是因为每个线程进来后,进入的do while循环,然后不断的获取内存中的值,判断是否为最新,然后在进行更新操作。

假设线程A和线程B同时执行getAndInt操作(分别跑在不同的CPU上)

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

Unsafe类 + CAS思想: 也就是自旋,自我旋转

底层汇编

DK提供的CAS机制,在汇编层级会禁止变量两侧的指令优化,然后使用compxchg指令比较并更新变量值(原子性)

总结:

  • CAS是靠硬件实现的从而在硬件层面提升效率,最底层还是交给硬件来保证原子性和可见性
  • 实现方式是基于硬件平台的汇编指令,在inter的CPU中,使用的是汇编指令compxchg指令
  • 核心思想就是比较要更新变量V的值和预期值E,相等才会将V的值设为新值N,如果不相等自旋再来

CAS实现------原子操作类

原子操作类位于 java.util.concurrent.atomic 包下,atomic 翻译成中文是原子的意思。在化学上,我们知道原子是构成一般物质的最小单位,在化学反应中是不可分割的。在我们这里 atomic 是指一个操作是不可中断的。即使是在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰

常用原子操作类可分为 基本类型、数组类型、引用类型三种

基本类型

基本类型分为下面三种

  • AtomicInteger:整型原子类
  • AtomicBoolean:布尔型原子类
  • AtomicLong:长整型原子类

常用API如下

java 复制代码
public final int get() //获取当前的值
public final int getAndSet(int newValue)//获取当前的值,并设置新的值
public final int getAndIncrement()//获取当前的值,并自增
public final int getAndDecrement() //获取当前的值,并自减
public final int getAndAdd(int delta) //获取当前的值,并加上预期的值
boolean compareAndSet(int expect, int update) //如果输入的数值等于预期值,则以原子方式将该值设置为输入值(update)
public final void lazySet(int newValue)//最终设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。

基本类型使用案例(此处以 AtomicInteger 为例)

java 复制代码
class MyNumber {
    AtomicInteger () = new AtomicInteger();

    public void addPlusPlus() {
        atomicInteger.getAndIncrement();
    }

}

public class AtomicIntegerDemo {

    public static final int SIZE = 50;

    public static void main(String[] args) throws InterruptedException {
        MyNumber myNumber = new MyNumber();
        CountDownLatch countDownLatch = new CountDownLatch(SIZE);
        for (int i = 1; i <= SIZE; i++) {
            new Thread(() -> {
                try {
                    for (int j = 1; j <= 10; j++) {
                        myNumber.addPlusPlus();
                    }
                } finally {
                    countDownLatch.countDown();
                }
            }, String.valueOf(i)).start();

        }
        countDownLatch.await();

        System.out.println(Thread.currentThread().getName() + "\t" + "result: " + myNumber.atomicInteger.get());
    }
}
----------------------------------------------------------------------------------
执行结果:
main	result: 500

数组类型

数组类型分为下面三种;

  • AtomicIntegerArray:整型原子数组类
  • AtomicLongArray:长整型原子数组类
  • AtomicReferenceArray:引用类型原子数组类

常用API如下

java 复制代码
public final int get(int i) //获取 index=i 位置元素的值
public final int getAndSet(int i, int newValue)//返回 index=i 位置的当前的值,并将其设置为新值:newValue
public final int getAndIncrement(int i)//获取 index=i 位置元素的值,并让该位置的元素自增
public final int getAndDecrement(int i) //获取 index=i 位置元素的值,并让该位置的元素自减
public final int getAndAdd(int i, int delta) //获取 index=i 位置元素的值,并加上预期的值
boolean compareAndSet(int i, int expect, int update) //如果输入的数值等于预期值,则以原子方式将 index=i 位置的元素值设置为输入值(update)
public final void lazySet(int i, int newValue)//最终 将index=i 位置的元素设置为newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。

数组类型使用案例(此处以 AtomicIntegerArray 为例) AtomicIntegerArray 的构造方法有两个:

  • public AtomicIntegerArray(int length) {array = new int[length];}
  • public AtomicIntegerArray(int[] array) {this.array = array.clone();} 所以初始化的方式也有两种
java 复制代码
public class AtomicIntegerArrayDemo {

    public static void main(String[] args) {
        //构造方法传参直接传一个实例化后的数组
        AtomicIntegerArray atomicIntegerArray1 = new AtomicIntegerArray(new int[]{1,2,3,4,5});
        for (int i = 0; i < atomicIntegerArray1.length(); i++) {
            System.out.println("atomicIntegerArray1中的值是:"+atomicIntegerArray1.get(i));
        }
        System.out.println("---------------------------------------");
        //构造方法中传指定长度的数组
        AtomicIntegerArray atomicIntegerArray2 = new AtomicIntegerArray(new int[4]);
        for (int i = 0; i < atomicIntegerArray2.length(); i++) {
            System.out.println("atomicIntegerArray2中的值是:"+atomicIntegerArray2.get(i));
        }
        System.out.println("---------------------------------------");
        //构造方法中直接传一个数组的长度
        AtomicIntegerArray atomicIntegerArray3 = new AtomicIntegerArray(3);
        System.out.println("atomicIntegerArray3 CAS是否成功:"+atomicIntegerArray3.compareAndSet(0,0,1));
        System.out.println("atomicIntegerArray3 CAS是否成功:"+atomicIntegerArray3.compareAndSet(0,0,1));
        System.out.println("atomicIntegerArray3 执行getAndIncrement操作:"+atomicIntegerArray3.getAndIncrement(0)+"\t 操作完成后的值"+atomicIntegerArray3.get(0));
    }
}
--------------------------------------------------------------------------------
执行结果:
atomicIntegerArray1中的值是:1
atomicIntegerArray1中的值是:2
atomicIntegerArray1中的值是:3
atomicIntegerArray1中的值是:4
atomicIntegerArray1中的值是:5
---------------------------------------
atomicIntegerArray2中的值是:0
atomicIntegerArray2中的值是:0
atomicIntegerArray2中的值是:0
atomicIntegerArray2中的值是:0
---------------------------------------
atomicIntegerArray3 CAS是否成功:true
atomicIntegerArray3 CAS是否成功:false
atomicIntegerArray3 执行getAndIncrement操作:1	 操作完成后的值2

引用类型

  • AtomicReference:引用类型原子类
  • AtomicStampedReference:带有版本号的引用类型原子类。该类将整数值和引用关联起来,可解决原子的更新数据和数据的版本号,可以解决使用 CAS 操作进行原子更新时可能出现的ABA问题
    • 解决修改几次的问题
  • AtomicMarkableReference:带标记的引用类型原子类。该类将 boolean标记和引用关联起来
    • 解决是否被修改过的问题。它的定义就是将标记戳简化为 true/false

引用类型使用案例

1.AtomicReference

java 复制代码
@Data
@AllArgsConstructor
@NoArgsConstructor
class User{
   public String name;
   public int age;
}

public class AtomicReferenceDemo {
    public static void main(String[] args) {
        AtomicReference<User> atomicReference = new AtomicReference<>();
        User z3 = new User("z3", 22);
        User li4 = new User("li4", 25);

        atomicReference.set(z3);
        System.out.println(atomicReference.compareAndSet(z3, li4) + "\t" + atomicReference.get().toString());//true    User(userName=li4, age=25)
        System.out.println(atomicReference.compareAndSet(z3, li4) + "\t" + atomicReference.get().toString());//false   User(userName=li4, age=25)
    }
-----------------------------------------------------------------------------
执行结果:
true	User(name=li4, age=25)
false	User(name=li4, age=25)

2.AtomicStampedReference

先说说什么是ABA问题:

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

对于一个共享变量A,有两个线程使用原子类对A进行操作,线程1 从内存中读取A的值,线程2 也从内存中读取A的值,线程2在线程1还没有对A进行操作之前先把 A更新成B ,然后操作完之后又将 V 位置的值变回 A,当线程1 要对 A进行操作时发现内存的值仍然是A,线程1 操作成功。尽管线程1的CAS操作成功,但是不代表这个过程就是没有问题的

ABA问题案例演示:

csharp 复制代码
public static void main(String[] args) {
    AtomicInteger atomicInteger  = new AtomicInteger(0);
    new Thread(()->{
        try {
            TimeUnit.SECONDS.sleep(1);
            System.out.println("线程"+Thread.currentThread().getName()+":"+atomicInteger.compareAndSet(0,1));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    },"A").start();

    new Thread(()->{
        System.out.println("线程"+Thread.currentThread().getName()+":"+atomicInteger.compareAndSet(0,2));
        System.out.println("线程"+Thread.currentThread().getName()+":"+atomicInteger.compareAndSet(2,0));
    },"B").start();
}
----------------------------------------------------------------------------
执行结果:可以看到B线程把值从0改成2之后再改回0,等A操作的时候预期值仍然是0,CAS操作成功
线程B:true
线程B:true
线程A:true

ABA问题解决:

csharp 复制代码
public static void main(String[] args) {
    AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100,1);
    new Thread(()->{
        try {
            int stamp = atomicStampedReference.getStamp();
            System.out.println(Thread.currentThread().getName() + "\t" + "首次版本号: " + stamp);
            TimeUnit.SECONDS.sleep(1);
            boolean flag = atomicStampedReference.compareAndSet(100, 101, stamp, stamp+1);
            System.out.println("线程A执行CAS操作:"+flag);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    },"A").start();

    new Thread(()->{
        int stamp = atomicStampedReference.getStamp();
        System.out.println(Thread.currentThread().getName() + "\t" + "首次版本号: " + stamp);
        boolean flag1 = atomicStampedReference.compareAndSet(100, 102, stamp, stamp+1);
        System.out.println(Thread.currentThread().getName() + "\t" + "二次版本号: " + atomicStampedReference.getStamp());
        System.out.println("线程B执行CAS操作:"+flag1);
        boolean flag2 = atomicStampedReference.compareAndSet(102, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp()+1);
        System.out.println(Thread.currentThread().getName() + "\t" + "三次版本号: " + atomicStampedReference.getStamp());
        System.out.println("线程B执行CAS操作:"+flag2);
    },"B").start();
}
-----------------------------------------------------------------------------------------
执行结果:
A	首次版本号: 1
B	首次版本号: 1
B	二次版本号: 2
线程B执行CAS操作:true
B	三次版本号: 3
线程B执行CAS操作:true
线程A执行CAS操作:false

3.AtomicMarkableReference

java 复制代码
public static void main(String[] args) {
    AtomicMarkableReference markableReference = new AtomicMarkableReference(100, false);
    new Thread(()->{
        try {
            boolean marked = markableReference.isMarked();
            System.out.println(Thread.currentThread().getName() + "\t" + "首次的修改标志: " + marked);
            TimeUnit.SECONDS.sleep(1);
            boolean flag = markableReference.compareAndSet(100,101,marked,!marked);
            System.out.println("线程A执行CAS操作:"+flag);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    },"A").start();

    new Thread(()->{
        boolean marked = markableReference.isMarked();
        System.out.println(Thread.currentThread().getName() + "\t" + "首次修改标志: " + marked);
        boolean flag1 = markableReference.compareAndSet(100, 102, marked, !marked);
        System.out.println(Thread.currentThread().getName() + "\t" + "二次修改标志: " + markableReference.isMarked());
        System.out.println("线程B执行CAS操作:"+flag1);
        boolean flag2 = markableReference.compareAndSet(102, 100, marked, !marked);
        System.out.println(Thread.currentThread().getName() + "\t" + "二次修改标志: " + markableReference.isMarked());
        System.out.println("线程B执行CAS操作:"+flag2);
    },"B").start();
}
------------------------------------------------------------------------------------------
执行结果:
A	首次的修改标志: false
B	首次修改标志: false
B	二次修改标志: true
线程B执行CAS操作:true
B	二次修改标志: true
线程B执行CAS操作:false
线程A执行CAS操作:false

对象属性修改类型

  • AtomicIntegerFieldUpdater:原子更新对象中int类型字段的值
  • AtomicLongFieldUpdater:原子更新对象中Long类型字段的值
  • AtomicReferenceFieldUpdater:原子更新对象中引用类型字段的值

对象属性修改类型使用案例

1.AtomicIntegerFieldUpdater:

java 复制代码
class SaleCount{
    volatile int money = 0;
    AtomicIntegerFieldUpdater<SaleCount> atomicIntegerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(SaleCount.class,"money");
    public void incomeCount(SaleCount saleCount){
        atomicIntegerFieldUpdater.getAndIncrement(saleCount);
    }
}
public class AtomicIntegerFieldUpdaterDemo {

    public static void main(String[] args) throws InterruptedException {
        SaleCount saleCount = new SaleCount();
        CountDownLatch countDownLatch = new CountDownLatch(50);
        for (int i = 0; i <50 ; i++) {

            new Thread(()->{
                try {
                    for (int j = 0; j <1000 ; j++) {
                        saleCount.incomeCount(saleCount);
                    }

                }finally {
                    countDownLatch.countDown();
                }

            }).start();

        }
        countDownLatch.await();
        System.out.println(Thread.currentThread().getName() + '\t' + "result: " + saleCount.money); //main result: 50000

    }
}
--------------------------------------------------------------------------------------
执行结果:
main	result: 50000

2.AtomicReferenceFieldUpdater:

java 复制代码
class CheckInit{
    public volatile Boolean initFlag = Boolean.FALSE;
    AtomicReferenceFieldUpdater<CheckInit,Boolean> atomicReferenceFieldUpdater = AtomicReferenceFieldUpdater.newUpdater(CheckInit.class,Boolean.class,"initFlag");

    public void init(CheckInit flag){
        if(atomicReferenceFieldUpdater.compareAndSet(flag,Boolean.FALSE,Boolean.TRUE)){
            System.out.println(Thread.currentThread().getName()+" \t"+"start init");
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+"\t"+"init is complete");
        }else{
            System.out.println(Thread.currentThread().getName()+"\t"+"already been init");
        }
    }
}
public class AtomicReferenceFieldUpdaterDemo {
    public static void main(String[] args) {
        CheckInit checkInit = new CheckInit();
        for (int i = 0; i <6 ; i++) {
            new Thread(()->{
                checkInit.init(checkInit);
            }).start();
        }
    }
}

-------------------------------------------------------------------------------------------
执行结果:
Thread-1	already been init
Thread-4	already been init
Thread-0 	start init
Thread-2	already been init
Thread-3	already been init
Thread-5	already been init
Thread-0	init is complete

原子操作增强类

  • DoubleAccumulator:一个或多个变量,它们一起保持运行double使用所提供的功能更新值
  • DoubleAdder:一个或多个变量一起保持初始为零double总和
  • LongAccumulator:一个或多个变量,一起保持使用提供的功能更新运行的值long ,提供了自定义的函数操作
  • LongAdder:一个或多个变量一起维持初始为零long总和(重点),只能用来计算加法,且从0开始计算

上述四个原子操作增强类是在 java8 中引入的 ,都可以用于在多线程环境下进行原子性的累加操作。都可以用于对double或者long类型数字进行原子性的累加操作

原子操作增强类常用API

以操作long类型的的增强类为例:

LongAdder:

java 复制代码
    public void add(long x) {}     //将当前的 value 加x
    public void increment() {}     //将当前的 value 加1
    public void decrement() {}     //将当前的 value 减1
    public long sum() {}           //返回当前值。特别注意,在没有并发更新value的情况下,sum会返回一个精确值,在存在并发的情况下,sum不保证返回精确值。
    public void reset() {}         //将当前的value重置为 0 ,可用于替代重新new一个longAdder,此方法只能在没有并发更新的情况下使用
    public long sumThenReset() {}  //该方法等效于先调用sum()方法计算总和,然后调用reset()方法将计数器重置为初始值。该方法常用于多线程计算之间的静默点。如果在调用此方法时存在并发的更新操作,则不能保证返回值是reset()方法执行前的最终值
    public long longValue() {}     //等价于 sum()
    public int intValue() {}       //等价于 (int)sum();
    public float floatValue() {}   //等价于 (float)sum()
    public double doubleValue() {} //等价于 (double)sum()

LongAccumulator:

java 复制代码
    public void accumulate(long x) {}//对累加器进行原子性的累加操作,使用指定的二元操作符函数和给定的值
    public void get() {}     //获取当前的累加结果
    public void reset() {}         //将累加器重置为初始值
    public long getThenReset() {}  //获取当前的累加结果,并将累加器重置为初始值
    public long longValue() {}     //等价于 get()
    public int intValue() {}       //等价于 (int)get();
    public float floatValue() {}   //等价于 (float)get()
    public double doubleValue() {} //等价于 (double)get()

原子增强类使用案例

不加锁热点商品点赞计算器,点赞数加加统计,不要求实时精确

java 复制代码
/**
 * @Description 需求:50个线程,每个线程200w此,总点赞数出来
 * @Date 2024-01-25 14:30
 */
class UpvoteCount{
    int count;
    public synchronized void upvoteBySynchronized(){
        count++;
    }

    AtomicLong atomicLong = new AtomicLong(0);
    public void upvoteByAtomicLong(){
        atomicLong.getAndIncrement();
    }

    LongAdder longAdder = new LongAdder();
    public void upvoteByLongAdder(){
        longAdder.increment();
    }

    LongAccumulator longAccumulator = new LongAccumulator((left, right) -> left+right,0);
    public void upvoteByLongAccumulator(){
        longAccumulator.accumulate(1);
    }

}

public class LongAddrDemo {
    public static final int _100W = 2000000;
    public static final int THREAD_NUMER = 50;
    public static void main(String[] args) throws InterruptedException {
        UpvoteCount upvoteCount = new UpvoteCount();
        CountDownLatch countDownLatchSync =  new CountDownLatch(50);
        CountDownLatch countDownLatchAtomicLong =  new CountDownLatch(50);
        CountDownLatch countDownLatchLongAddr =  new CountDownLatch(50);
        CountDownLatch countDownLatchLongAccumulator =  new CountDownLatch(50);
        Instant start = Instant.now();
        for (int i = 0; i < THREAD_NUMER; i++) {
            new Thread(()->{
                try{
                    for (int j = 0; j < _100W; j++) {
                        upvoteCount.upvoteBySynchronized();
                    }

                }finally {
                    countDownLatchSync.countDown();
                }
            }).start();
        }
        countDownLatchSync.await();
        Instant end = Instant.now();
        System.out.println("upvoteBySynchronized cost time:"+ Duration.between(start,end).toMillis()+" \t "+"最终点赞数是:"+upvoteCount.count);

       start = Instant.now();
        for (int i = 0; i < THREAD_NUMER; i++) {
            new Thread(()->{
                try{
                    for (int j = 0; j < _100W; j++) {
                        upvoteCount.upvoteByAtomicLong();
                    }

                }finally {
                    countDownLatchAtomicLong.countDown();
                }
            }).start();
        }
        countDownLatchAtomicLong.await();
        end = Instant.now();
        System.out.println("upvoteByAtomicLong cost time:"+ Duration.between(start,end).toMillis()+" \t "+"最终点赞数是:"+upvoteCount.atomicLong.get());

        start = Instant.now();
        for (int i = 0; i < THREAD_NUMER; i++) {
            new Thread(()->{
                try{
                    for (int j = 0; j < _100W; j++) {
                        upvoteCount.upvoteByLongAdder();
                    }

                }finally {
                    countDownLatchLongAddr.countDown();
                }
            }).start();
        }
        countDownLatchLongAddr.await();
        end = Instant.now();
        System.out.println("upvoteByLongAdder cost time:"+ Duration.between(start,end).toMillis()+" \t "+"最终点赞数是:"+upvoteCount.longAdder.sum());

        start = Instant.now();
        for (int i = 0; i < THREAD_NUMER; i++) {
            new Thread(()->{
                try{
                    for (int j = 0; j < _100W; j++) {
                        upvoteCount.upvoteByLongAccumulator();
                    }

                }finally {
                    countDownLatchLongAccumulator.countDown();
                }
            }).start();
        }
        countDownLatchLongAccumulator.await();
        end = Instant.now();
        System.out.println("upvoteByLongAccumulator cost time:"+ Duration.between(start,end).toMillis()+" \t "+"最终点赞数是:"+upvoteCount.longAccumulator.get());
    }
}

-----------------------------------------------------------------------
执行结果:
upvoteBySynchronized cost time:6011 	 最终点赞数是:100000000
upvoteByAtomicLong cost time:3700 	 最终点赞数是:100000000
upvoteByLongAdder cost time:489 	 最终点赞数是:100000000
upvoteByLongAccumulator cost time:399 	 最终点赞数是:100000000

案例总结: 从上述的四种统计方式的执行结果看,可以看到使用 LongAdder 和LongAccumulator的执行效率更高

LongAdder 和 LongAccumulator 原子增强类效率高的原因

因为 LongAdder 和LongAccumulator能够减少乐观锁的重试次数。

如果是JDK8,推荐使用LongAdder或者LongAccumulator对象,比AtomicLong性能更好

原子增强类源码分析

架构图:

从架构图上可以看到 LongAdder 和 LongAccumulator 都是 Striped64 子类

Striped64 中一些变量和方法的定义

  • base:类似于AtomicLong中全局的value值,在没有竞争的情况下数据直接累加到base上,或者cells扩容时,也需要将数据写入到base上
  • collide:标识扩容一项,false 一定不会扩容,true 可能会扩容
  • cellsBusy:初始化cells或者扩容cells需要获取锁, 0:表示无所状态 1:表示其他线程已经持有了锁
  • casCellsBusy:通过CAS操作修改cellsBusy的值,CAS成功代表获取锁,返回true
  • NCPU:当前计算机CPU数量,Cell数组扩容时使用
  • getProbe():获取当前线程的hash值
  • advanceProbe():重置当前线程的hash

从源码角度分析LongAdder 和 LongAccumulator的实现

从源码看 LongAdder 和 LongAccumulator基本实现思路是分散热点,将value值分散到一个 Cell[] 数组中,多个线程需要同时对value进行操作的时候,可以对线程id进行hash得到hash值,再根据hash值映射到这个数组cells的某个下标,再对该下标所对应的值进行自增操作,当所有线程操作完毕,将数组cells的所有值和base都加起来作为最终结果

LongAdder分析:

LongAdder类内部维护了一个long类型的变量base,表示当前的累加结果。同时它还有一个Cell[]类型的数组cells,每个Cell对象都包含一个long类型的变量,用于处理高并发情况下的累加操作 在使用LongAdder进行累加操作时,通过调用add(long x)方法来对累加器进行更新。这个方法的内部实现是通过调用striped64类中的方法来完成的。

striped64类是LongAdder的内部类,负责管理多线程并发访问时的同步和竞争。它将累加器的内部状态分成若干个桶(cells数组),每个桶管理一定范围内的数据。

在执行add()方法时,首先会根据当前线程的hash值计算出对应的桶的索引,然后尝试原子性地更新该桶中的值。如果更新成功,则直接返回;否则,表示当前桶已经被其他线程占用,需要使用CAS操作找到另一个可用的桶进行更新。

LongAccumulator 分析

LongAccumulator类内部维护了一个long类型的变量value,表示当前的累加结果。同时它还有一个LongBinaryOperator类型的变量function,表示累加操作的二元函数。

在使用LongAccumulator进行累加操作时,通过调用accumulate(long x)方法或accumulate(LongBinaryOperator accumulatorFunction, long value)方法来对累加器进行更新。这两个方法的内部实现都是通过调用striped64类中的方法来完成的。

striped64类是LongAccumulator的内部类,负责管理多线程并发访问时的同步和竞争。它将累加器的内部状态分成若干个桶(cells数组),每个桶管理一定范围内的数据。

在执行accumulate()方法时,首先会根据当前线程的hash值计算出对应的桶的索引,然后尝试原子性地更新该桶中的值。如果更新成功,则直接返回;否则,表示当前桶已经被其他线程占用,需要使用CAS操作找到另一个可用的桶进行更新。


LongAdderLongAccumulator 的实现中,每个桶的状态都是独立的,不同桶之间的更新操作可以并发执行,从而提高了累加器的性能和吞吐量

注意获取的结果不能保证实时的准确性

在多线程并发更新时,LongAccumulatorLongAdder 的值不一定是最终的累加结果。因为在某个线程更新时,其他线程也可能同时进行更新,导致最终结果可能不是所有更新操作的总和。如果需要获得准确的结果,可以在更新完成后LongAccumulator调用get()或者 LongAdder调用sum()方法获取当前的累加结果

相关推荐
hanbarger12 分钟前
mybatis框架——缓存,分页
java·spring·mybatis
cdut_suye20 分钟前
Linux工具使用指南:从apt管理、gcc编译到makefile构建与gdb调试
java·linux·运维·服务器·c++·人工智能·python
苹果醋332 分钟前
2020重新出发,MySql基础,MySql表数据操作
java·运维·spring boot·mysql·nginx
小蜗牛慢慢爬行33 分钟前
如何在 Spring Boot 微服务中设置和管理多个数据库
java·数据库·spring boot·后端·微服务·架构·hibernate
azhou的代码园36 分钟前
基于JAVA+SpringBoot+Vue的制造装备物联及生产管理ERP系统
java·spring boot·制造
wm10431 小时前
java web springboot
java·spring boot·后端
smile-yan1 小时前
Provides transitive vulnerable dependency maven 提示依赖存在漏洞问题的解决方法
java·maven
老马啸西风1 小时前
NLP 中文拼写检测纠正论文-01-介绍了SIGHAN 2015 包括任务描述,数据准备, 绩效指标和评估结果
java
Earnest~1 小时前
Maven极简安装&配置-241223
java·maven
皮蛋很白1 小时前
Maven 环境变量 MAVEN_HOME 和 M2_HOME 区别以及 IDEA 修改 Maven repository 路径全局
java·maven·intellij-idea