当前位置: 首页 > 科技观察

并发情况下,你还在用Random生成随机数吗?

时间:2023-03-16 22:51:33 科技观察

前言在代码中生成随机数是一个很常见的功能,JDK已经提供了一个现成的Random类来实现它,并且Random类是线程安全的。下面是Random.next()生成随机整数的实现:)&面具;//CAS竞争效率低}while(!seed.compareAndSet(oldseed,nextseed));return(int)(nextseed>>>(48-bits));}不难看出,上面的方法使用了CAS运算更新种子,在大量线程竞争的场景下,CAS操作很可能会失败。如果失败,会重试,而这种重试会消耗CPU运算,会大大降低性能。因此,Random虽然是线程安全的,但并不是“高并发”的。为了改善这个问题,增强随机数生成器在高并发环境下的性能,出现了ThreadLocalRandom——一个强大的高并发随机数生成器。ThreadLocalRandom继承自Random。根据里氏代换原则,这说明ThreadLocalRandom提供了与Random相同的随机数生成功能,只是实现算法略有不同。Thread中的变量为了处理线程竞争,Java中有一个ThreadLocal类,它为每个线程分配独立独立的存储空间。ThreadLocal的实现依赖于Thread对象中的ThreadLocal.ThreadLocalMapthreadLocals成员字段。同样,为了让随机数生成器只能访问本地线程数据,避免竞争,在Thread中多增加三个成员:/**ThecurrentseedforaThreadLocalRandom*/@sun.misc.Contended("tlr")longthreadLocalRandomSeed;/**Probehashvalue;nonzeroifthreadLocalRandomSeedinitialized*/@sun.misc.Contended("tlr")intthreadLocalRandomProbe;/**从publicThreadLocalRandomsequence中分离出来的Secondaryseed*/@sun.misc.Contended("tlr")intthreadLocalRandomSeedinitialSeed;这3个成员字段作为Thread类,自然而然地与各个Thread对象紧密绑定,因此成为名副其实的ThreadLocal变量,而依赖这些变量实现的随机数生成器就成为ThreadLocalRandom。消除虚假共享。不知道你有没有注意到。在这些变量上,有一个注解@sun.misc.Contended。这个注解有什么用?要理解这一点,首先要知道并发编程的一个重要问题——伪共享:我们知道CPU不直接访问内存,数据是从缓存加载到寄存器的,缓存有L1,L2、L3等级别。这里,我们先简化这些负责的层级关系,假设只有一级缓存和主存。CPU读取和更新缓存时,是以行为为单位执行的,也称为缓存行。一行一般为64字节,也就是8个long的长度。于是,问题来了,一个cacheline可以存储多个变量,如果多个线程同时访问不同的变量,而这些不同的变量恰好位于同一个cacheline中,会发生什么情况呢?如上图,X,Y是相邻的两个变量,位于同一个cacheline,CPUcore1core2都加载了它们,core1更新X,core2同时更新Y,因为数据的读取和更新是基于cachelineunit,也就是说当这两个事情同时发生的时候,就会产生竞争,导致core1和core2可能需要刷新自己的数据(cacheline是对方更新的),这会大大降低系统的性能。这是一个伪共享问题。那么如何改进呢?如下图所示:上图中,我们为X占用了一个cacheline,为Y占用了一个cacheline,这样更新读取各自不会有任何影响。上面代码中的@sun.misc.Contended("tlr")会帮我们在虚拟机级别的变量前后生成一些padding,使得标记的变量位于同一个cacheline中,不会与其他变量。在Thread对象中,将成员变量threadLocalRandomSeed、threadLocalRandomProbe、threadLocalRandomSecondarySeed标记为同一组tlr,这样这三个变量就放在了单独的缓存行中,不会与其他变量发生冲突,从而提高并发环境下的访问速度。反射的高效替代方法随机数的生成需要访问Thread的threadLocalRandomSeed等成员,但考虑到类的封装性,这些成员在包中是可见的。不幸的是,ThreadLocalRandom位于java.util.concurrent包中,而Thread位于java.lang包中。因此ThreadLocalRandom是没有办法访问到Thread的threadLocalRandomSeed等变量的。这时候Java老手可能会跳出来说:这是什么东西,看我的反射方法,不管什么都可以挖出来访问。诚然,反射是一种可以绕过封装,直接访问对象内部数据的方法。但是反射的性能不是很好,不适合作为高性能方案。有没有什么办法可以让ThreadLocalRandom访问Thread的内部成员,同时拥有一个远超反射又无限接近于直接变量访问的方法呢?答案是肯定的,这就是使用Unsafe类。这里简单介绍一下使用到的两个Unsafe方法:publicnativelonggetLong(Objecto,longoffset);publicnativevoidputLong(对象,longoffset,longx);其中getLong()方法将读取对象o长数据的偏移字节偏移量;putLong()会将x写入对象o的第offset-th个字节的偏移量。这种类C的操作方式带来了极大的性能提升。更重要的是,由于避开了字段名,直接使用偏移量,可以轻松绕过成员的可见性限制。性能问题解决了,那么接下来的问题是,如何知道threadLocalRandomSeed成员在Thread中的偏移位置呢?这就需要使用到不安全的objectFieldOffset()方法,请看下面的代码:上面的静态代码,在初始化ThreadLocalRandom类的时候,获取了Thread成员变量threadLocalRandomSeed、threadLocalRandomProbe、threadLocalRandomSecondarySeed在objectoffset中的位置.因此,只要ThreadLocalRandom需要使用这些变量,就可以通过不安全的getLong()和putLong()(可能还有getInt()和putInt())来访问它们。例如生成随机数时:protectedintnext(intbits){return(int)(mix64(nextSeed())>>>(64-bits));}finallongnextSeed(){Threadt;longr;//readandupdateper-threadseed//在ThreadLocalRandom中,获取Thread的threadLocalRandomSeed变量UNSAFE.putLong(t=Thread.currentThread(),SEED,r=UNSAFE.getLong(t,SEED)+GAMMA);returnr;}这种Unsafe方法可以落地有多快?下面我们一起来做个实验:这里我们自己写一个ThreadTest类,使用反射和unsafe两种方法连续读写threadLocalRandomSeed成员变量,比较它们的性能差异。代码如下:上面代码中使用了反射方法byReflection()和Unsafe方法byUnsafe()对threadLocalRandomSeed变量进行了1亿次读写,测试结果如下:byUnsafespend:171msbyReflectionspend:645ms不难看出,使用Unsafe的方法远优于反射的方法,这也是JDK内部广泛使用Unsafe而不是反射的原因之一。随机数种子我们知道伪随机数的产生需要一个种子,threadLocalRandomSeed和threadLocalRandomSecondarySeed就是这里的种子。其中,threadLocalRandomSeed为long型,threadLocalRandomSecondarySeed为int型。threadLocalRandomSeed是应用最广泛的大量随机数,实际上都是基于threadLocalRandomSeed。而threadLocalRandomSecondarySeed只是在一些特定的JDK内部实现中使用,并没有被广泛使用。初始种子默认使用系统时间:以上代码完成种子的初始化,通过UNSAFE(即threadLocalRandomSeed)将初始化的种子存放在SEED的位置。然后可以使用nextInt()方法获取随机整数:publicintnextInt(){returnmix32(nextSeed());}finallongnextSeed(){Threadt;longr;//readandupdateper-threadseedUNSAFE.putLong(t=Thread.currentThread(),SEED,r=UNSAFE.getLong(t,SEED)+GAMMA);returnr;}每次调用nextInt()都会使用nextSeed()来更新threadLocalRandomSeed。由于这是一个线程专有的变量,完全不会有竞争,也不会有CAS重试,性能会有很大的提升。除了种子,探针Probe还有一个threadLocalRandomProbe探针变量。这个变量是做什么用的?我们可以把threadLocalRandomProbe理解为每个Thread的一个Hash值(不为0),可以用来作为线程的特征值,根据这个值可以找到线程在数组中的具体位置。staticfinalintgetProbe(){returnUNSAFE.getInt(Thread.currentThread(),PROBE);}看一段代码:CounterCell[]as;longb,s;if((as=counterCells)!=null||!U.compareAndSwapLong(this,BASECOUNT,b=baseCount,s=b+x)){CounterCella;longv;intm;booleanuncontended=true;if(as==null||(m=as.length-1)<0||//使用probe,asforeachthread在数组中找一个位置//因为每个线程的probe值不同,所以很有可能每个线程对应的数组中的元素也不同//每个线程对应不同的元素,可以不冲突的进行完整的并发操作//所以probe这里的probe起到了防止冲突的作用(a=as[ThreadLocalRandom.getProbe()&m])==null||!(uncontended=U.compareAndSwapLong(a,CELLVALUE,v=a.value,v+x))){在具体实现中,如果上述代码冲突,也可以使用ThreadLocalRandom.advanceProbe()方法修改一个线程Probe值,可以进一步避免可能冲突未来,从而减少竞争并提高并发性能。staticfinalintadvanceProbe(intprobe){//根据当前probe值,计算一个更新的probe值probe^=probe<<13;//xorshiftprobe^=probe>>>17;probe^=probe<<5;//updateProbevalueintothread对象就是修改threadLocalRandomProbe变量UNSAFE.putInt(Thread.currentThread(),PROBE,probe);returnprobe;}小结今天介绍ThreadLocalRandom对象,它是一个高并发环境下的高性能随机数发电机。我们不仅介绍了ThreadLocalRandom的功能和内部实现原理,还介绍了ThreadLocalRandom对象是如何实现高性能的(比如通过falsesharing、Unsafe等),希望大家能够灵活地将这些技术应用到自己的项目中。傻逼们对这个冷门品类有更深的理解吗?懂的可以在评论区一波:变强,我是敖丙,知道的越多,不知道的越多,下期见。