java高并发情况下高效的随机数生成器

前言

在代码中生成随机数,是一个非常常用的功能,并且JDK已经提供了一个现成的Random类来实现它,并且Random类是线程安全的。

下面是Random.next()生成一个随机整数的实现:

protected int next(int bits) {
    long oldseed, nextseed;
    AtomicLong seed = this.seed;
    do {
        oldseed = seed.get();
        nextseed = (oldseed * multiplier + addend) & mask;
      //CAS 有竞争是效率低下
    } while (!seed.compareAndSet(oldseed, nextseed));
    return (int)(nextseed >>> (48 - bits));
}

不难看到,上面的方法中使用CAS操作更新seed,在大量线程竞争的场景下,这个CAS操作很可能失败,失败了就会重试,而这个重试又会消耗CPU运算,从而使得性能大大下降了。

因此,虽然Random是线程安全的,但是并不是“高并发”的。

为了改进这个问题,增强随机数生成器在高并发环境中的性能,于是乎,就有了ThreadLocalRandom——一个性能强悍的高并发随机数生成器。

ThreadLocalRandom继承自Random,根据里氏代换原则,这说明ThreadLocalRandom提供了和Random相同的随机数生成功能,只是实现算法略有不同。

在Thread中的变量

为了应对线程竞争,Java中有一个ThreadLocal类,为每一个线程分配了一个独立的,互不相干的存储空间。

ThreadLocal的实现依赖于Thread对象中的ThreadLocal.ThreadLocalMap threadLocals成员字段。

与之类似,为了让随机数生成器只访问本地线程数据,从而避免竞争,在Thread中,又增加了3个成员:

/** The current seed for a ThreadLocalRandom */
@sun.misc.Contended("tlr")
long threadLocalRandomSeed;
/** Probe hash value; nonzero if threadLocalRandomSeed initialized */
@sun.misc.Contended("tlr")
int threadLocalRandomProbe;
/** Secondary seed isolated from public ThreadLocalRandom sequence */
@sun.misc.Contended("tlr")
int threadLocalRandomSecondarySeed;

这3个字段作为Thread类的成员,便自然和每一个Thread对象牢牢得捆绑在一起,因此成为了名副其实的ThreadLocal变量,而依赖这几个变量实现的随机数生成器,也就成为了ThreadLocalRandom。

消除伪共享

不知道大家有没有注意到, 在这些变量上面,都带有一个注解@sun.misc.Contended,这个注解是干什么用的呢?要了解这个,大家得先知道一下并发编程中的一个重要问题——伪共享

我们知道,CPU是不直接访问内存的,数据都是从高速缓存中加载到寄存器的,高速缓存又有L1,L2,L3等层级。在这里,我们先简化这些负责的层级关系,假设只有一级缓存和一个主内存。

CPU读取和更新缓存的时候,是以行为单位进行的,也叫一个cache line,一行一般64字节,也就是8个long的长度。

因此,问题就来了,一个缓存行可以放多个变量,如果多个线程同时访问的不同的变量,而这些不同的变量又恰好位于同一个缓存行,那会发生什么呢?

如上图所示,X,Y为相邻2个变量,位于同一个缓存行,两个CPU core1 core2都加载了他们,core1更新X,同时,core2更新Y,由于数据的读取和更新是以缓存行为单位的,这就意味着当这2件事同时发生时,就产生了竞争,导致core1和core2有可能需要重新刷新自己的数据(缓存行被对方更新了),这就导致系统的性能大大折扣,这就是伪共享问题。

那怎么改进呢?如下图:

上图中,我们把X单独占用一个缓存行,Y单独占用一个缓存行,这样各自更新和读取,都不会有任何影响了。

而上述代码中的@sun.misc.Contended(“tlr”)就会在虚拟机层面,帮助我们在变量的前后生成一些padding,使得被标注的变量位于同一个缓存行,不与其它变量冲突。

在Thread对象中,成员变量threadLocalRandomSeed,threadLocalRandomProbe,threadLocalRandomSecondarySeed被标记为同一个组tlr,使得这3个变量放置于一个单独的缓存行,而不与其它变量发生冲突,从而提高在并发环境中的访问速度。

反射的高效替代方案

随机数的产生需要访问Thread的threadLocalRandomSeed等成员,但是考虑到类的封装性,这些成员却是包内可见的。

很不幸,ThreadLocalRandom位于java.util.concurrent包,而Thread则位于java.lang包,因此,ThreadLocalRandom并没有办法访问Thread的threadLocalRandomSeed等变量。

这时,Java老鸟们可能就会跳出来说:这算什么,看我的反射大法,不管啥都能抠出来访问一下。

说的不错,反射是一种可以绕过封装,直接访问对象内部数据的方法,但是,反射的性能不太好,并不适合作为一个高性能的解决方案。

有没有什么办法可以让ThreadLocalRandom访问Thread的内部成员,同时又具有远超于反射的,且无限接近于直接变量访问的方法呢?答案是肯定的,这就是使用Unsafe类。

这里,就简单介绍一下用的两个Unsafe的方法:

public native long    getLong(Object o, long offset);
public native void    putLong(Object o, long offset, long x);

其中getLong()方法,会读取对象o的第offset字节偏移量的一个long型数据;putLong()则会将x写入对象o的第offset个字节的偏移量中。

这类类似C的操作方法,带来了极大的性能提升,更重要的是,由于它避开了字段名,直接使用偏移量,就可以轻松绕过成员的可见性限制了。

性能问题解决了,那下一个问题是,我怎么知道threadLocalRandomSeed成员在Thread中的偏移位置呢,这就需要用unsafe的objectFieldOffset()方法了,请看下面的代码:

上述这段static代码,在ThreadLocalRandom类初始化的时候,就取得了Thread成员变量threadLocalRandomSeed,threadLocalRandomProbe,threadLocalRandomSecondarySeed在对象偏移中的位置。

因此,只要ThreadLocalRandom需要使用这些变量,都可以通过unsafe的getLong()和putLong()来进行访问(也可能是getInt()和putInt())。

比如在生成一个随机数的时候:

protected int next(int bits) {
    return (int)(mix64(nextSeed()) >>> (64 - bits));
}
final long nextSeed() {
    Thread t; long r; // read and update per-thread seed
    //在ThreadLocalRandom中,访问了Thread的threadLocalRandomSeed变量
    UNSAFE.putLong(t = Thread.currentThread(), SEED,
                   r = UNSAFE.getLong(t, SEED) + GAMMA);
    return r;
}

这种Unsafe的方法掉地能有多快呢,让我们一起看做个试验看看:

这里,我们自己写一个ThreadTest类,使用反射和unsafe两种方法,来不停读写threadLocalRandomSeed成员变量,比较它们的性能差异,代码如下:

上述代码中,分别使用反射方式byReflection() 和Unsafe的方式byUnsafe()来读写threadLocalRandomSeed变量1亿次,得到的测试结果如下:

byUnsafe spend :171ms
byReflection spend :645ms

不难看到,使用Unsafe的方法远远优于反射的方法,这也是JDK内部,大量使用Unsafe来替代反射的原因之一。

随机数种子

我们知道,伪随机数生成都需要一个种子,threadLocalRandomSeed和threadLocalRandomSecondarySeed就是这里的种子。其中threadLocalRandomSeed是long型的,threadLocalRandomSecondarySeed是int。

threadLocalRandomSeed是使用最广泛的大量的随机数其实都是基于threadLocalRandomSeed的。而threadLocalRandomSecondarySeed只是某些特定的JDK内部实现中有使用,使用并不广泛。

初始种子默认使用的是系统时间:

上述代码中完成了种子的初始化,并将初始化的种子通过UNSAFE存在SEED的位置(即threadLocalRandomSeed)。

接着就可以使用nextInt()方法获得随机整数了:

public int nextInt() {
    return mix32(nextSeed());
}
final long nextSeed() {
    Thread t; long r; // read and update per-thread seed
    UNSAFE.putLong(t = Thread.currentThread(), SEED,
                   r = UNSAFE.getLong(t, SEED) + GAMMA);
    return r;
}

每一次调用nextInt()都会使用nextSeed()更新threadLocalRandomSeed。由于这是一个线程独有的变量,因此完全不会有竞争,也不会有CAS的重试,性能也就大大提高了。

探针Probe的作用

除了种子外,还有一个threadLocalRandomProbe探针变量,这个变量是用来做什么的呢?

我们可以把threadLocalRandomProbe 理解为一个针对每个Thread的Hash值(不为0),它可以用来作为一个线程的特征值,基于这个值可以为线程在数组中找到一个特定的位置。

static final int getProbe() {
    return UNSAFE.getInt(Thread.currentThread(), PROBE);
}

来看一个代码片段:

CounterCell[] as; long b, s;
if ((as = counterCells) != null ||
    !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
    CounterCell a; long v; int m;
    boolean uncontended = true;
    if (as == null || (m = as.length - 1) < 0 ||
        // 使用probe,为每个线程找到一个在数组as中的位置
        // 由于每个线程的probe值不一样,因此大概率 每个线程对应的数组中的元素也是不一样的
        // 每个线程对应了不同的元素,就可以没有冲突的进行完全的并发操作
        // 因此探针probe在这里 就起到了防止冲突的作用
        (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
        !(uncontended =
          U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {

在具体的实现中,如果上述代码发生了冲突,那么,还可以使用ThreadLocalRandom.advanceProbe()方法来修改一个线程的探针值,这样可以进一步避免未来可能得冲突,从而减少竞争,提高并发性能。

static final int advanceProbe(int probe) {
    //根据当前探针值,计算一个更新的探针值
    probe ^= probe << 13;   // xorshift
    probe ^= probe >>> 17;
    probe ^= probe << 5;
    //更新探针值到线程对象中 即修改了threadLocalRandomProbe变量
    UNSAFE.putInt(Thread.currentThread(), PROBE, probe);
    return probe;
}

总结

今天,我们介绍了ThreadLocalRandom对象,这是一个高并发环境中的,高性能的随机数生成器。

我们不但介绍了ThreadLocalRandom的功能和内部实现原理,还介绍介绍了ThreadLocalRandom对象是如何达到高性能的(比如通过伪共享,Unsafe等手段),希望大家可以将这些技术灵活运用到自己的工程中。

小傻瓜们对这个冷门类是否有深一步的理解了?理解了可以在评论区来一波:变得更强

我是敖丙,你知道的越多,不知道的越多,我们下期见。

以上就是java高并发情况下高效的随机数生成器的详细内容,更多关于java高并发高效随机数的资料请关注我们其它相关文章!

(0)

相关推荐

  • java使用Abobe Acrobat DC生成模板

    一.利用word生成一个文档转成pdf 说明:转换成pdf格式 二.Abobe Acrobat DC图解 利用Abobe Acrobat DC打开pdf 步骤:文件 ---->创建------>创建表单----->选择文件(你转换成pdf文件) 这里是所要增加的文本域.图片域等功能按钮 在你需要的位置增加你说要的功能完成之后进行保存 三.java后台代码 环境maven <dependency> <groupId>com.itextpdf</groupId&

  • Eclipse+Java+Swing实现学生成绩管理系统的实例代码

    一.系统介绍 本系统实现了用户登录,实现了对学生成绩的增删改查,实现了用户修改密码功能,采用MD5加密算法,数据库使用Mysql8.0.13,界面良好. 二.系统展示 1.登录页 2.主页面 3.查询学生成绩 4.修改学生成绩 5.添加学生成绩 6.修改系统密码 三.系统实现 Student.java package com.sjsq.model; import java.util.ArrayList; import java.util.List; /** * 学生实体类 * @author s

  • 超简洁java实现双色球若干注随机号码生成(实例代码)

    Mavan pom文件引用依赖 <!-- hutool工具类--> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.3.6</version> </dependency> <!-- google java类库--> <dependency> <

  • Java 生成带Logo和文字的二维码

    ZXing 是一个开放源码的,用Java实现的多种格式的1D/2D条码图像处理库,它包含了联系到其他语言的端口.Zxing 可以实现使用手机的内置的摄像头完成条形码的扫描及解码.本章讲解用 ZXing 生成和扫码二维码. 依赖 在Java项目中pom.xml加入: <dependency> <groupId>com.google.zxing</groupId> <artifactId>core</artifactId> <version&g

  • 教你如何用Java根据日期生成流水号

    前言 生成流水号,在企业中可以说是比较常见的需求,尤其是订单类业务. 一般来说,需要保证流水号的唯一性. 如果没有长度和字符的限制,那么直接使用UUID生成一个唯一字符串即可,也可以直接使用数据库表中的主键,主键就是唯一的. 那么,如果限制了流水号必须多少位,这种怎么生成呢? 可以采用"前缀+日期+数字"的方式(ps:此方式是需要用到缓存的) 前缀:为了更好的标识这个流水号是属于哪种类型: 日期:为了防止重复: 数字:为了表示当前的流水所处序号. 需求:生成一个17位数的唯一流水号,&

  • 使用cmd根据WSDL网址生成java客户端代码的实现

    windows下使用cmd命令提示符生成java webservice客户端代码,可以使用命令提示符直接生成客户端代码,直接导入到项目中,只需配置jdk即可,在jdk的bin文件夹下,按Shift并点击右键,选中"在此处打开命令窗口" 输入命令如下: wsimport -keep -p com.demo.client -d D:\\(存放的地址) http://XX/Account?wsdl(wsdl地址) 命令参数说明: -d:生成客户端执行类的class文件的存放目录(默认存放在C

  • Java使用iTextPDF生成PDF文件的实现方法

    iText介绍和说明 因为项目需要生成PDF文件,所以去找了一下能够生成PDF的Java工具,看到了iText可以说好评如潮. 如果你想通过java操作PDF文件,那么 iText 绝对是你的首选. 引入依赖 这里使用的是iText5 <dependency> <groupId>com.itextpdf</groupId> <artifactId>itextpdf</artifactId> <version>5.5.10</ve

  • java自动生成编号的实现(格式:yyMM+四位流水号)

    本篇文章主要介绍了java自动生成编号的实现,分享给大家,具体如下 /** * 自动生成编号格式:yyMM+四位流水号 */ @RequestMapping(params = "createCode") @ResponseBody public AjaxJson createCode(HttpServletRequest request, String tableName, String fieldName) { AjaxJson j = new AjaxJson(); String

  • Java基于Javafaker生成测试数据

    1. 前言 老板说,明天甲方要来看产品,你得造点数据,而且数据必须是"真"的,演示效果要好看一些,这样他才会买我们的产品,我好明年给你换个嫂子.一般开发接到这种过分要求都不会很乐意去做,这完全是体力劳动,而且很棘手.今天胖哥教你一招,让你做出逼真的"假"数据. 2. javafaker 我们Java是有可以生成仿真数据的框架的,这里我安利一个Javafaker的框架,你只需要在项目中引入: <dependency> <groupId>com.

  • java高并发情况下高效的随机数生成器

    前言 在代码中生成随机数,是一个非常常用的功能,并且JDK已经提供了一个现成的Random类来实现它,并且Random类是线程安全的. 下面是Random.next()生成一个随机整数的实现: protected int next(int bits) { long oldseed, nextseed; AtomicLong seed = this.seed; do { oldseed = seed.get(); nextseed = (oldseed * multiplier + addend)

  • J2ee 高并发情况下监听器实例详解

    J2ee 高并发情况下监听器实例详解 引言:在高并发下限制最大并发次数,在web.xml中用过滤器设置参数(最大并发数),并设置其他相关参数.详细见代码. 第一步:配置web.xml配置,不懂的地方解释一下:参数50通过参数名maxConcurrent用在filter的实现类中获取,filter-class就是写的实现类, url-pattern就是限制并发时间的url,结束! <filter> <filter-name>ConcurrentCountFilter</filt

  • Redis高并发情况下并发扣减库存项目实战

    目录 第一种方案:纯MySQL扣减实现 MySQL架构升级 第二种方案:缓存实现扣减 第三种方案:数据库+缓存 顺序写的性能更好 顺序写的架构 扣减流程 相信大家从网上学习项目大部分人第一个项目都是电商,生活中时时刻刻也会用到电商APP,例如淘宝,京东等.做技术的人都知道,电商的业务逻辑简单,但是大部分电商都会涉及到高并发高可用,对并发和对数据的处理要求是很高的.这里我今天就讲一下高并发情况下是如何扣减库存的? 我们对扣减库存所需要关注的技术点如下: 当前剩余的数量大于等于当前需要扣减的数量,不

  • 数据库高并发情况下重复值写入的避免 字段组合约束

    10线程同时操作,频繁出现插入同样数据的问题.虽然在插入数据的时候使用了: insert inti tablename(fields....) select @t1,@t2,@t3 from tablename where not exists (select id from tablename where t1=@t1,t2=@t2,t3=@t3) 当时还是在高并发的情况下无效.此语句也包含在存储过程中.(之前也尝试线判断有无记录再看是否写入,无效). 因此,对于此类情况还是需要从数据库的根本

  • java并发高的情况下用ThreadLocalRandom来生成随机数

    目录 一:简述 二:Random的性能差在哪里 三:ThreadLocalRandom的简单使用 四:为什么ThreadLocalRandom能在保证线程安全的情况下还能有不错的性能 一:简述 如果我们想要生成一个随机数,通常会使用Random类.但是在并发情况下Random生成随机数的性能并不是很理想,今天给大家介绍一下JUC包中的用于生成随机数的类--ThreadLocalRandom.(本文基于JDK1.8) 二:Random的性能差在哪里 Random随机数生成是和种子seed有关,而为

  • Java Semaphore实现高并发场景下的流量控制

    目录 前言 Semaphore介绍 代码演示 补充 独占锁与共享锁 公平锁与非公平锁 可重入锁 前言 在java开发的工作中是否会出现这样的场景,你需要实现一些异步运行的任务,该任务可能存在消耗大量内存的情况,所以需要对任务进行并发控制.如何优雅的实现并发控制呢?下面我会给大家介绍一个类--Semaphore,能很优雅的实现并发控制,继续往下看吧. Semaphore介绍 首先我们看一下Semaphore类的构造函数是如何实现的. public Semaphore(int permits, bo

  • Java 高并发七:并发设计模型详解

    1. 什么是设计模式 在软件工程中,设计模式(design pattern)是对软件设计中普遍存在(反复出现)的各种问题 ,所提出的解决方案.这个术语是由埃里希·伽玛(Erich Gamma)等人在1990年代从建筑设计领 域引入到计算机科学的. 著名的4人帮: Erich Gamma,Richard Helm, Ralph Johnson ,John Vlissides (Gof) <设计模式:可复用面向对象软件的基础>收录23种模式 2. 单例模式 单例对象的类必须保证只有一个实例存在.许

  • Java高并发BlockingQueue重要的实现类详解

    ArrayBlockingQueue 有界的阻塞队列,内部是一个数组,有边界的意思是:容量是有限的,必须进行初始化,指定它的容量大小,以先进先出的方式存储数据,最新插入的在对尾,最先移除的对象在头部. public class ArrayBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable { /** 队列元素 */ final Object

  • Java 高并发编程之最实用的任务执行架构设计建议收藏

    目录 前言 1.业务架构 2.技术架构 3.物理架构 高并发任务执行架构 需求场景 业务架构设计 技术架构设计 初始设计 演化阶段一 演化阶段二 演化阶段三 代码设计 总结 前言 随着互联网与软件的发展,除了程序员,架构师也是越来越火的职业.他们伴随着项目的整个生命过程,他们更像是传统工业的设计师,将项目当做生命一般细心雕琢. 目前对于项目架构而言,基本都会需要设计的几个架构. 1.业务架构 项目或者产品的市场定位.需求范围.作用场景都是需要在项目启动初期进行系统性分析的.在设计业务架构中,架构

  • java 高并发中volatile的实现原理

    java 高并发中volatile的实现原理 摘要: 在多线程并发编程中synchronized和Volatile都扮演着重要的角色,Volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的"可见性".可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值.它在某些情况下比synchronized的开销更小 1. 定义: java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量.

随机推荐