并发编程进阶类学习--java并发编程之美(二)

文章目录

      • Random类原理及其局限性
        • 上述代码的执行流程
      • 应运而生的ThreadLocalRandom
      • AtomicLong的介绍
        • 函数列表
        • 缺点
      • 使用LongAdder(jdk1.8新增)相比于AtomicLong的好处
        • 具体LongAdder实现原理
        • LongAdder的设计结构简略图
        • 简单聊一下LongAdder的add方法
      • LongAccumulator类(jdk1.8新增)
        • 相比于LongAdder

Random类原理及其局限性

先来看这个

// 生成一个随机数0-9的整数
(int)(Math.random()*10);

其实Math.random底层调用的还是Random类的方法。
在这里插入图片描述
再往下看,相信下述代码一定不陌生,一起来了解一下执行流程

public class demo11 {
    public static void main(String[] args) {
        Random random = new Random();
        for (int i = 0; i < 10; i++) {
            //生成一个随机数0-9的整数
            System.out.println(random.nextInt(10));
        }
    }
}
上述代码的执行流程
  1. 新建一个Random类,这个无参构造类其实执行的是一个有参的构造类,会生成一个初始的种子。而且其内部使用的是原子类AtomicLong
    public Random() {
        this(seedUniquifier() ^ System.nanoTime());
    }

------------------------------------

    public Random(long seed) {
        if (getClass() == Random.class)
            this.seed = new AtomicLong(initialScramble(seed));
        else {
            // subclass might have overriden setSeed
            this.seed = new AtomicLong();
            setSeed(seed);
        }
    }
  1. 然后调用random.nextInt(10),这个方法里面做的事情就是,根据old种子计算new种子,再根据新的种子来计算随机数,以此来保证随机数的随机
public int nextInt(int bound) {
        if (bound <= 0)
            throw new IllegalArgumentException(BadBound);
		//这一步的操作就是为了获取到下一个种子
        int r = next(31);
        //下面就是根据新的种子计算新的随机数
		//计算随机数的方法一样,所以随机数新不新决定了随机数会不会变
		......
        return r;
    }

所以问题也就出在这,如果多个线程操作的话,那么就可能存在多个线程使用的种子都是同一个种子,而导致随机数不均匀
Random自然也考虑到了这一点,所以它将我们的种子改成了原子类,并且增加了一个CAS判断,自旋判断

    protected int next(int bits) {
        long oldseed, nextseed;
        //原子类AtomicLong
        AtomicLong seed = this.seed;
        do {
            oldseed = seed.get();
            nextseed = (oldseed * multiplier + addend) & mask;
        } while (!seed.compareAndSet(oldseed, nextseed));   //CAS自旋判断
        return (int)(nextseed >>> (48 - bits));
    }

Debug后的上述代码执行效果,可以看到nextseed被作为oldseed,去计算新的nextseed了。
并发编程进阶类学习--java并发编程之美(二)_第1张图片

并发编程进阶类学习--java并发编程之美(二)_第2张图片
是的,虽然原子类和CAS保证了其多线程下的安全性,但同时也降低了其性能。因为CAS是保证只有一个线程会去操作老的种子,其它线程就是自旋,循环等待,然后获取更新后的种子作为老种子去计算新的种子。所以,在大量线程竞争原子类的情况下,还要执行CAS的更新操作,就会造成大量的线程自旋重试,降低并发性能。
为此,ThreadLocalRandom就出现了!

应运而生的ThreadLocalRandom

为了弥补多线程高并发情况下Random的缺陷,在JUC包下新增了ThreadLocalRandom类
使用方法如下

public class demo11 {
    public static void main(String[] args) {
    //ThreadLocalRandom利用current()方法,进行实例化
        ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
        for (int i = 0; i < 10; i++) {
            System.out.println(threadLocalRandom.nextInt(10));
        }
    }
}

来了解一下ThreadLocalRandom.current(),其实它的创建过程返回的都是同一个静态实例

    public static ThreadLocalRandom current() {
        if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
            localInit();
        return instance;
    }
//instance就是下面这个定义好的静态对象
//所以多个线程操作时,其实返回的是一个工具类ThreadLocalRandom的实例
    static final ThreadLocalRandom instance = new ThreadLocalRandom();

那么,ThreadLocalRandom是怎么保证高并发情况下的效率的呢。
其实你看这个类名,就可以发现其实它与ThreadLocal是有关系。
通过前面基础的学习,我们知道ThreadLocal是通过让每一个线程复制一个共享变量到自己的内存当中,操作自己的变量来避免了同步
ThreadLocalRandom其实也是这个原理,它通过让每一个线程复制一份种子到自己的工作内存当中,各个线程操作自己的种子进行更新,也就避免了Random更新种子多个线程竞争的情况。

AtomicLong的介绍

实现的基本思想,就是CAS,compareAndSet方法

  //expect 表示当前内存中你所预期望的值,update 是你想要更新的值
    public final boolean compareAndSet(long expect, long update) {
    	这个this表示了当前对象的地址,后面的valueOffset是在该对象开始的位置加上这个valueOffset的偏移量,就能拿到的是当前对象的值
        return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
    }

//把上面的compareAndSwapLong的入参和这个对应起来,然后其实就是判断当前位置的值,是不是和你预期的expect的值相等
//如果相等就把valueOffset处的值更新为update你想要更新的结果,
//如果失败的话,就不做改变(说明这个值跟你期望的不一致,就意味着这个值被别的线程修改过了,就什么也不操作)
public final native boolean compareAndSwapLong(Object var1, 
											long var2,  
											long var4, 
											long var6);
函数列表
// 构造函数
AtomicLong()
// 创建值为initialValue的AtomicLong对象
AtomicLong(long initialValue)
// 以原子方式设置当前值为newValue。
final void set(long newValue) 
// 获取当前值
final long get() 
// 以原子方式将当前值减 1,并返回减1后的值。等价于“--num”
final long decrementAndGet() 
// 以原子方式将当前值减 1,并返回减1前的值。等价于“num--”
final long getAndDecrement() 
// 以原子方式将当前值加 1,并返回加1后的值。等价于“++num”
final long incrementAndGet() 
// 以原子方式将当前值加 1,并返回加1前的值。等价于“num++”
final long getAndIncrement()    
// 以原子方式将delta与当前值相加,并返回相加后的值。
final long addAndGet(long delta) 
// 以原子方式将delta添加到当前值,并返回相加前的值。
final long getAndAdd(long delta) 
// 如果当前值 == expect,则以原子方式将该值设置为update。成功返回true,否则返回false,并且不修改原值。
final boolean compareAndSet(long expect, long update)
// 以原子方式设置当前值为newValue,并返回旧值。
final long getAndSet(long newValue)
// 返回当前值对应的int值
int intValue() 
// 获取当前值对应的long值
long longValue()    
// 以 float 形式返回当前值
float floatValue()    
// 以 double 形式返回当前值
double doubleValue()    
// 最后设置为给定值。延时设置变量值,这个等价于set()方法,但是由于字段是volatile类型的,因此次字段的修改会比普通字段(非volatile字段)有稍微的性能延时(尽管可以忽略),所以如果不是想立即读取设置的新值,允许在“后台”修改值,那么此方法就很有用。如果还是难以理解,这里就类似于启动一个后台线程如执行修改新值的任务,原线程就不等待修改结果立即返回(这种解释其实是不正确的,但是可以这么理解)。
final void lazySet(long newValue)
// 如果当前值 == 预期值,则以原子方式将该设置为给定的更新值。JSR规范中说:以原子方式读取和有条件地写入变量但不 创建任何 happen-before 排序,因此不提供与除 weakCompareAndSet 目标外任何变量以前或后续读取或写入操作有关的任何保证。大意就是说调用weakCompareAndSet时并不能保证不存在happen-before的发生(也就是可能存在指令重排序导致此操作失败)。但是从Java源码来看,其实此方法并没有实现JSR规范的要求,最后效果和compareAndSet是等效的,都调用了unsafe.compareAndSwapInt()完成操作。
final boolean weakCompareAndSet(long expect, long update)
缺点

这样带来的缺点就是,如果有大量的线程进行CAS的时候,那么就会导致大量的线程都在进行CAS,但是能够成功的又很少,那么就会出现有大量线程进行自旋,并且对CPU造成大量的资源消耗的缺点,所以这个时候就进行了优化,那么就出现了LongAdder

使用LongAdder(jdk1.8新增)相比于AtomicLong的好处

这些都是位于JUC.atomic包下的类,先来看下atomic类下的情况
并发编程进阶类学习--java并发编程之美(二)_第3张图片

AtomicLong这类的原子类,里面其实都是用Unsafe类来保证实现的,方法中都通过一个非阻塞算法CAS来进行比较,使得每次获取变量的线程只有一个。但问题也就来了,同上面的Random一样,在高并发情况下,导致多个线程不停自旋比较,白白浪费了CPU性能
于是出现了LongAdder。
既然上述的性能问题是由于多个线程竞争一个变量产生的,那么LongAdder就将这个变量分解成多个变量,让同样多的线程竞争多个变量,性能就会提高了

具体LongAdder实现原理
  • LongAdder其实是在其内部维护多个Cell变量和一个Base,每个Cell里面有一个初始值为0的Long类型变量。多个Cell变量存在一个Cell数组里。每个线程操作一个cell变量。类似于JDK1.7中ConcurrentHashMap的分段锁。

  • LongAdder在多个线程竞争多个变量失败的情况下,并不是让线程在当前Cell变量前进行自旋等待,而是会让线程尝试在别的Cell变量上进行CAS操作,这无疑增加了成功的可能性。

  • 最后,在获取LongAdder的值时,是把所有的Cell变量的value值累加后再加上base返回的。

  • LongAdder维护了一个延迟初始化的原子性更新数组和一个基值变量base,由于Cells占用的内存是比较大的,所以一开始并不创建它,而是在需要的时候创建,也就是惰性加载。

既然LongAdder内部Cell是存储在数组中,又是在高并发的情况下操作,那么应该要考虑到伪共享的问题吧?

当一开始Cell数组为null时并且线程数较少的时候,所有的累加操作都是对base进行。保持Cell的数组的大小为2的N次方,在初始化时Cell元素个数为2。

对于大多数孤立的原子操作进行字节填充是浪费的,因为原子操作都是无规律的分布在内存中,多个原子变量被放入缓存行的可能性很小。但是原子性数组的内存地址是连续的,所以多个Cell可能就容易被一个线程占用(毕竟一个缓存行只能由一个线程操作)。因此这里使用[email protected]注解==对Cell类进行字节填充,防止数组中多个元素共享一个缓存行。

LongAdder的设计结构简略图

并发编程进阶类学习--java并发编程之美(二)_第4张图片
由上图可知,LongAdder类继承Striped64类,在Striped64类内部维护三个变量。LongAdder的真实值其实就是base的值和Cell数组里所有的Cell元素的值的和。

简单聊一下LongAdder的add方法
    public void add(long x) {
        Cell[] as; long b, v; int m; Cell a;
        //先判断Cell数组是否为空,为空则直接执行caseBase方法,对base进行累加操作
        if ((as = cells) != null || !casBase(b = base, b + x)) {
            boolean uncontended = true;
            //不为空,则进入
            //以下两行完成了对Cell数组中哪个Cell的筛选
            if (as == null || (m = as.length - 1) < 0 ||  //2
                (a = as[getProbe() & m]) == null ||       //3
                //进行cas累加操作
                !(uncontended = a.cas(v = a.value, v + x)))
                //映射元素失败或者cas失败,则执行下面这行,是Cell数组初始化和扩容的方法。
                longAccumulate(x, null, uncontended);
        }
    }
//----------------caseBase方法------------------
    final boolean casBase(long cmp, long val) {
        return UNSAFE.compareAndSwapLong(this, BASE, cmp, val);
    }

LongAccumulator类(jdk1.8新增)

上面讲的LongAdder其实属于该类的一个特例,LongAccumulator的功能其实比LongAdder更加强大。
下面是其在jdk1.8帮助文档中的一段描述:
在这里插入图片描述

相比于LongAdder
  • LongAccumulator可以为累加器提供非0的初始值,LongAdder只能提供默认的0值
  • LongAccumulator可以指定累加规则,比如不进行累加而进行累乘,只需要在构造LongAccumulator时传入自定义的双目运算器即可。LongAdder则是内置的累加规则
  • 累加规则的不同其实也就体现在add方法中,对于LongAdder方法,longAccumulate方法传的是个null,则使用v+x的算法。而对于LongAccumulator,则是使用传入的function函数的算法。
  • 在并发处理上,AtomicLong和LongAdder均具有各自优势,需要怎么使用还是得看使用场景。其实并不意味着LongAdder就一定比AtomicLong好使,个人认为在QPS统计等统计操作上,LongAdder会更加适合,而AtomicLong在自增控制方面是LongAdder无法代替的。
  • 在多数地并发和少数高并发情况下,AtomicLong和LongAdder性能上差异并不是很大,只有在并发极高的时候,才能真正体现LongAdder的优势。

你可能感兴趣的:(并发编程,java,多线程,并发编程)