《Java后端知识体系》系列之Atomic原子类

最近参加了公司的王者荣耀比赛,一直开黑都忘记整理知识了,肥宅肥宅,希望能拿到一部Iphone 11 pro max(虽然根本不可能)

Atomic原子类

JUC并发包提供一系列的原子性操作,这些类都是使用非阻塞算法CAS实现的,相比使用锁实现原子性操作这在性能上有很大 的提高。
JUC并发包中含有AtomicInteger、AtomicLong和AtomicBoolean等原子操作类。以AtomicLong为例。

public class AtomicLong extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 1927816293512124184L;

    // (1)获取Unsafe实例
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    //(2)存放变量value的偏移量
    private static final long valueOffset;

    //(3)判断JVM是否支持Long类型无所CAS
    static final boolean VM_SUPPORTS_LONG_CAS = VMSupportsCS8();

    /**
     * Returns whether underlying JVM supports lockless CompareAndSet
     * for longs. Called only once and cached in VM_SUPPORTS_LONG_CAS.
     */
    private static native boolean VMSupportsCS8();

    static {
        try {
        	//(4)获取value在AtomicLong中的偏移量
            valueOffset = unsafe.objectFieldOffset
                (AtomicLong.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
	//(5)实际变量值
    private volatile long value;

   
    public AtomicLong(long initialValue) {
        value = initialValue;
    }
	...
}
  • 代码(1)中通过Unsafe.getUnsafe()方法获取到Unsafe的实例,之所以能通过Unsafe.getUnsafe获取到Unsafe的实例,这是因为 AtomicLong类也是在rt.jar包下面的,AtomicLong类就是通过Bootstarp类加载器进行加载的。
  • 代码(5)中的value被声明为volatile的,这是为了多线程下保证内存可见性,value是具体存放计数的变量。
  • 代码(2)(4)获取value变量在AtomicLong类中的偏移量。
	//调用unsafe方法,原子性设置value值为原始值+1,返回值为递增后的值
    public final long incrementAndGet() {
        return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
    }
    //调用unsafe方法,原子性设置value值为原始值-1,返回值为递减之后的值
    public final long decrementAndGet() {
        return unsafe.getAndAddLong(this, valueOffset, -1L) - 1L;
    }
    //调用unsafe方法,原子性设置value原始值+1,返回原始值
    public final long getAndIncrement() {
        return unsafe.getAndAddLong(this, valueOffset, 1L);
    }
    //调用unsafe方法,原子性设置value为原始值-1,返回值为原始值
    public final long getAndDecrement() {
        return unsafe.getAndAddLong(this, valueOffset, -1L);
    }

  • 如上代码中都是通过调用Unsafe的getAndAddLong方法来实现操作,这个函数是原子性操作,这里第一个参数是AtomicLong实例的引用,第二个参数是value变量在AtomicLong中的偏移值,第三个参数是要设置的第二个变量的值。
    public final boolean compareAndSet(long expect, long update) {
        return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
    }
  • 在如上代码中在内部还是调用了unsafe.compareAndSwapLong方法,如果原子变量中的value值等于expect,则使用update值更新该值并返回true,否则返回false。
public class AtomicTest {

    private static AtomicLong atomicLong = new AtomicLong();

    private static Integer[] arrOne = new Integer[]{0,1,2,3,4,5,6};

    private static Integer[] arrTwo = new Integer[]{10,12,34,43,2,5,2};

    public static void main(String[] args) throws InterruptedException {
        Thread threadOne = new Thread(new Runnable() {
            @Override
            public void run() {
                int size = arrOne.length;
                for (int i = 0;i<size;i++){
                    if (arrOne[i].intValue() == 0){
                        atomicLong.incrementAndGet();
                    }
                }
            }
        });

        Thread threadTwo = new Thread(new Runnable() {
            @Override
            public void run() {
                int size = arrTwo.length;
                for (int i=0;i<size;i++){
                    if (arrTwo[i].intValue() == 0){
                        atomicLong.incrementAndGet();
                    }
                }
            }
        });


        //启动子线程
        threadOne.start();
        threadTwo.start();

        //等待线程执行完毕
        threadOne.join();
        threadTwo.join();

        System.out.println("count 0 "+atomicLong.get());
    }
}

  • 如上代码中两个线程各自统计自己所持数据中0的个数,每当找到一个0就会调用AtomicLong的原子性递增方法。
  • 在没有原子类的情况下,实现计数器需要使用一定的同步措施,比如使用synchronized关键字等,这些都是阻塞算法,对性能有一定的损耗,原子操作类使用非阻塞算法CAS,性能更好。但是在高并发情况下AtomicLong还会存在性能问题,因此JDK8提供了一个高并发下性能更好的LongAdder类。
  • AtomicLong通过CAS提供非阻塞的原子性操作,相比使用阻塞算法的同步器在性能方面有了提升,但是在高并发下大量线程会同时去竞争同一个原子变量,但是由于同时只有一个线程的CAS会操作成功,这就造成大量线程竞争失败,会通过无线循环不断进行自旋尝试CAS操作,这样会浪费CPU资源。

在JDK8中新增LongAdder类,实现结构如下:

在这里插入图片描述

  • 在使用LongAdder时,内部会维护多个Cell变量,每个Cell变量中有一个初始值为0的long型变量,这样在同等并发量的情况下,争夺单个变量更新操作的线程量会减少,这变相的减少了争夺共享资源的并发量。另外多个线程在争夺同一个Cell原子变量时如果失败了,它并不是一直在当前Cell变量上自旋CAS重试,而是尝试在其它Cell变量上进行尝试,这个改变增加了当前线程重试CAS成功的可能性。最后在获取LongAdder当前值时,把所有的Cell变量的value值累加后再加上base返回。
    LongAdder维护一个延迟初始化的原子性更新数组(默认情况下Cell数组是null)和一个基值变量base。由于Cell占用的内存相对比较大,所以一开始并不创建它,而是在需要时创建,也就是惰性加载。
  • 当一开始判断Cell数组是null并且并发线程较少时,所有的累加操作都会对base变量进行操作。保持Cell数组的大小为2的N次方,在初始化时Cell数组中的Cell元素个数为2,数组里面的变量实体是Cell类型,Cell类型是AtomicLong的一个改进,用来减少缓存的竞争,也就是解决伪共享问题。

LongAdder代码分析

为了解决高并发下多线程对一个变量CAS争夺失败后进行自旋而造成的降低并发性能问题,LongAdder内部维护多个Cell元素来分担对单个变量进行争夺的开销?

LongAdder的结构是怎样的?
当前线程应该访问Cell数组里面的哪个元素?
如何初始化Cell数组?
Cell数组如何扩容?
线程访问分配的Cell元素有冲突后如何处理?
如何保证线程操作被分配的Cell元素的原子性?

在这里插入图片描述

  • 由该构造图可知,LongAdder类继承自Striped64类,在Striped64内部维护三个变量,LongAdder的真实值其实是base的值与Cell数组里面所有Cell元素的value值的累加,base是个基础值,默认为0。cellBusy用来实现自旋锁,状态只有0和1,当创建Cell元素,扩容Cell数组或者初始化Cell数组时,使用CAS操作该变量来保证同时只有一个线程可以进行其中之一的操作。

接下来看Cell源码:

    @sun.misc.Contended static final class Cell {
        volatile long value;
        Cell(long x) { value = x; }
        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);
            }
        }
    }
  • 从源码中可以看到Cell中内部维护了一个被声明的volatile变量,这里声明为volatile是因为线程操作value变量时没有使用锁,为了保证变量的内存可见性,这里将其声明为volatile的。另外CAS函数通过CAS操作保证当前线程更新时被分配的Cell元素中value值的原子性,另外Cell类使用@sun.misc.Contended修饰是为了避免伪共享 。

在LongAdder中的源码:

long sum()

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;
    }
  • long
    sum是返回当前的值,内部操作是累加所有Cell内部的value值后在家base。在以上代码中并没有对Cell数组进行加锁,所以在累加过程中可能有其它线程对Cell中的值进行了修改,也有可能对数组进行了扩容,所以sum值返回的值并不是非常精确的,其返回值并不是一个调用sum方法时的原子快照。

void reset()

    public void reset() {
        Cell[] as = cells; Cell a;
        base = 0L;
        if (as != null) {
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null)
                    a.value = 0L;
            }
        }
    }
  • reset方法为重置操作,如上代码中会把base值为0,如果Cell数组有元素,则元素值会被重置为0.

long sumThenReset()

    public long sumThenReset() {
        Cell[] as = cells; Cell a;
        long sum = base;
        base = 0L;
        if (as != null) {
            for (int i = 0; i < as.length; ++i) {
                if ((a = as[i]) != null) {
                    sum += a.value;
                    a.value = 0L;
                }
            }
        }
        return sum;
    }
  • 该方法时sum的一个改造版本,如上代码在使用sum累加对应的Cell值后,把当前Cell的值重置为0,base重置为0,这样多线程调用该方法时会有问题。

void add(long x)

    public void add(long x) {
        Cell[] as; long b, v; int m; Cell a;
        if ((as = cells) != null || !casBase(b = base, b + x)) {//(1)
            boolean uncontended = true;
            if (as == null || (m = as.length - 1) < 0 ||//(2)
                (a = as[getProbe() & m]) == null ||//(3)
                !(uncontended = a.cas(v = a.value, v + x)))//(4)
                longAccumulate(x, null, uncontended);//(5)
        }
    }
  • 在以上方法中,(1)会首先判断Cell是否为null,如果为null则在当前基础变量base上进行累加
  • 如果Cell不为null或者执行代码(1)的CAS失败了,则会去执行代码(2)。代码(2)(3)决定当前线程应该访问Cell数组里面的哪个元素,如果当前线程映射的元素存在则执行代码(4),使用CAS操作去更新分配的Cell元素的value值,如果当前线程映射的元素不存在或者存在但是CAS操作失败则执行代码(5)。其实将代码(2)(3)(4)合起来看就是获取当前线程应该访问的Cell数组的Cell元素。
  • 另外当前线程访问Cell数组的哪个Cell元素是通过getProbe()&m进行计算的,其中m是当前Cell数组元素个数-1,getProbe()则用于获取当前线程中变量threadLocalRandomProbe的值,这个值一开始为0,在代码(5)中会进行初始化。并且当前线程通过分配的Cell元素的CAS函数来保证对Cell元素的value值更新的原子性。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值