Random可以说是每个开发都知道,而且都用的很6的类,如果你说,你没有用过Random,也不知道Random是什么鬼,那么你也不会来到这个技术类型的社区,也看不到我的博客了。但并不是每个人都知道Random的原理,知道Random在高并发下的缺陷的人应该更少。这篇博客,我就来分析下Random类在并发下的缺陷以及JUC对其的优化。

Random的原理及缺陷

    public static void main(String[] args) {
Random random = new Random();
System.out.println(random.nextInt(100));
}

在学习编程的时候,我一直对JDK开发人员很不解:为什么产生随机数的方法名是:“”nextXXX”?虽然我英语只停留“点头yes,摇头no,来是come,去是go” 的水平,但是我知道next是“下一个”的意思,如果我来命名,会命名为“create”,“generate”,这样不是更“贴切”吗?为什么JDK开发人员会命名为“nextXXX”呢?难道是他们突然“词穷”了,想不出什么单词了,所以干脆随便来一个?后来才知道,原来通过Random生成的随机数,并不是真正的随机,它有一个种子的概念,是根据种子值来计算【下一个】值的,如果种子值相同,那么它生成出来的随机数也必定相等,也就是“确定的输入产生确定的输出”。

如果你不信的话,我们可以来做一个实验:

    public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Random random = new Random(15);
System.out.println(random.nextInt(100));
}
}

Random类除了提供无参的构造方法以外,还提供了有参的构造方法,我们可以传入int类型的参数,这个参数就被称为“种子”,这样“种子”就固定了,生成的随机数也都是一样了:

41
41
41
41
41
41
41
41
41
41

让我们简单的看下nextInt的源码把,源码涉及到算法,当然算法不是本篇博客讨论的重点,我们可以把源码简化成如下的样子:

    public int nextInt(int bound) {
if (bound <= 0)
throw new IllegalArgumentException(BadBound);
//1.根据老的种子生成新的种子
int r = next(31);
//2.根据新的种子计算随机数
...
return r;
}

首先是根据老的种子生成新的种子,然后是根据新的种子计算出随机数,nextXXX的核心代码可以被简化这两步。

现在让我们想一个问题,如果在高并发的情况下,有N个线程,同时执行到第一步:根据老的种子生成新的种子,获得的种子不就一样了吗?由于第二步是根据新的种子来计算随机数,这个算法又是固定的,会产生什么情况?N个线程最终获得的随机数不都一样了吗?显然这不是我们想要的,所以JDK开发人员想到了这点,让我们看看next方法里面做了什么:

    protected int next(int bits) {
long oldseed, nextseed;//定义旧种子,下一个种子
AtomicLong seed = this.seed;
do {
oldseed = seed.get();//获得旧的种子值,赋值给oldseed
nextseed = (oldseed * multiplier + addend) & mask;//一个神秘的算法
} while (!seed.compareAndSet(oldseed, nextseed));//CAS,如果seed的值还是为oldseed,就用nextseed替换掉,并且返回true,退出while循环,如果已经不为oldseed了,就返回false,继续循环
return (int)(nextseed >>> (48 - bits));//一个神秘的算法
}
  1. 定义了旧种子oldseed,下一个种子(新种子)nextseed。
  2. 获得旧的种子的值,赋值给oldseed 。
  3. 一个神秘的算法,计算出下一个种子(新种子)赋值给nextseed。
  4. 使用CAS操作,如果seed的值还是oldseed,就用nextseed替换掉,并且返回true,!true为false,退出while循环;如果seed的值已经不为oldseed了,就说明seed的值已经被替换过了,返回false,!false为true,继续下一次while循环。
  5. 一个神秘的算法,根据nextseed计算出随机数,并且返回。

我们可以看到核心就在第四步,我再来更详细的的描述下,首先要知道seed的类型:

 private final AtomicLong seed;

seed的类型是AtomicLong,是一个原子操作类,可以保证原子性,seed.get就是获得seed具体的值,seed就是我们所谓的种子,也就是种子值保存在了原子变量里面。

当有两个线程同时进入到next方法,会发生如下的事情:

  1. 线程A,线程B同时拿到了seed的值,赋值给oldseed变量。
  2. 根据一个神秘的算法,计算出nextseed为XXX。注意,既然这个算法是固定的,那么生成出来的nextseed也必定是固定的。
  3. 进入while循环:

    3.1 线程A,利用CAS算法,发现seed的值还是为oldseed,说明seed的值没有被替换过,就把seed的值替换成第二步生成出来的nextseed,替换成功,返回true,!true为false,退出while循环。

    3.2 线程B,利用CAS算法,发现seed的值已经不为oldseed了,因为线程A已经把seed的值替换成了nextseed了啊,所以CAS失败,只能再次循环。再次循环的时候, seed.get()就拿到了最新的种子值,再次根据一个神秘的算法获得了nextSeed,CAS成功,退出循环。

看起来一切很美好,其实不然,如果并发很高,会发生什么?大量的线程都在进行while循环,这是相当占用CPU的,所以JUC推出了ThreadLocalRandom来解决这个问题。

ThreadLocalRandom

首先,让我们来看看ThreadLocalRandom的使用方法:

    public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
ThreadLocalRandom threadLocalRandom = ThreadLocalRandom.current();
System.out.println(threadLocalRandom.nextInt(100));
}
}

可以看到使用方式发生了些许的改变,我们来看看ThreadLocalRandom核心代码的实现逻辑:

current

    public static ThreadLocalRandom current() {
if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
localInit();
return instance;
}

有一点需要注意,由于current是一个静态的方法,所以多次调用此方法,返回的ThreadLocalRandom对象是同一个。

如果当前线程的PROBE是0,说明是第一次调用current方法,那么需要调用localInit方法,否则直接返回已经产生的实例。

localInit
    static final void localInit() {
int p = probeGenerator.addAndGet(PROBE_INCREMENT);
int probe = (p == 0) ? 1 : p; // skip 0
long seed = mix64(seeder.getAndAdd(SEEDER_INCREMENT));
Thread t = Thread.currentThread();
UNSAFE.putLong(t, SEED, seed);
UNSAFE.putInt(t, PROBE, probe);
}

首先初始化probe和seed,随后调用UNSAFE类的方法,把probe和seed设置到当前的线程,这样其他线程就拿不到了。

nextInt

    public int nextInt(int bound) {
if (bound <= 0)
throw new IllegalArgumentException(BadBound);
int r = mix32(nextSeed());
int m = bound - 1;
if ((bound & m) == 0) // power of two
r &= m;
else { // reject over-represented candidates
for (int u = r >>> 1;
u + m - (r = u % bound) < 0;
u = mix32(nextSeed()) >>> 1)
;
}
return r;
}

和Random类下的nextXXX方法的原理一样,也是根据旧的种子生成新的种子,然后根据新的种子来生成随机数,我们来看下nextSeed方法做了什么:

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;
}

首先使用UNSAFE.getLong(t, SEED) 来获得当前线程的SEED,随后+上GAMMA来作为新的种子值,随后将新的种子值放到当前线程中。

总结

本文首先简单的分析了Random的实现原理,引出nextXXX方法在高并发下的缺陷:需要竞争种子原子变量。接着介绍了ThreadLocalRandom的使用方法以及原理,从类的命名,就可以看出实现原理类似于ThreadLocal,seed种子是保存在每个线程中的,也是根据每个线程中的seed来计算新的种子的,这样就避免了竞争的问题。

Random在高并发下的缺陷以及JUC对其的优化的更多相关文章

  1. 分布式高并发下Actor模型

    分布式高并发下Actor模型 写在开始 一般来说有两种策略用来在并发线程中进行通信:共享数据和消息传递.使用共享数据方式的并发编程面临的最大的一个问题就是数据条件竞争.处理各种锁的问题是让人十分头痛的 ...

  2. Java高并发下多线程编程

    1.创建线程 Java中创建线程主要有三种方式: 继承Thread类创建线程类: 定义Thread类的子类,并重写该类的run方法,该run方法的方法体就代表了线程要完成的任务.因此也把run方法称为 ...

  3. php结合redis实现高并发下的抢购、秒杀功能

    抢购.秒杀是如今很常见的一个应用场景,主要需要解决的问题有两个:1 高并发对数据库产生的压力2 竞争状态下如何解决库存的正确减少("超卖"问题)对于第一个问题,已经很容易想到用缓存 ...

  4. 高并发下MySQL出现checking permissions

    在某些数据访问层框架中,会使用show full tables from test like 'demo',来检查数据库的状态.当数据库中表的数量较少时,并没有出现严重的问题.但是当数据库中的表数量多 ...

  5. EF+MySQL乐观锁控制电商并发下单扣减库存,在高并发下的问题

    下订单减库存的方式 现在,连农村的大姐都会用手机上淘宝购物了,相信电商对大家已经非常熟悉了,如果熟悉电商开发的同学,就知道在买家下单购买商品的时候,是需要扣减库存的,当然有2种扣减库存的方式, 一种是 ...

  6. (高级篇)php结合redis实现高并发下的抢购、秒杀功能

    抢购.秒杀是如今很常见的一个应用场景,主要需要解决的问题有两个:1 高并发对数据库产生的压力2 竞争状态下如何解决库存的正确减少("超卖"问题)对于第一个问题,已经很容易想到用缓存 ...

  7. php结合redis实现高并发下的抢购、秒杀功能 (转载)

    抢购.秒杀是如今很常见的一个应用场景,主要需要解决的问题有两个: 1 高并发对数据库产生的压力 2 竞争状态下如何解决库存的正确减少("超卖"问题) 对于第一个问题,已经很容易想到 ...

  8. php 高并发下 秒杀处理思路

    1.用额外的单进程处理一个队列,下单请求放到队列里,一个个处理,就不会有并发的问题了,但是要额外的后台进程以及延迟问题,不予考虑. 2.数据库乐观锁,大致的意思是先查询库存,然后立马将库存+1,然后订 ...

  9. PHP开发中多种方案实现高并发下的抢购、秒杀功能

    抢购.秒杀是如今很常见的一个应用场景,主要需要解决的问题有两个: 1 高并发对数据库产生的压力 2 竞争状态下如何解决库存的正确减少("超卖"问题) 对于第一个问题,已经很容易想到 ...

随机推荐

  1. datetime的精度

    最近有需要将分钟线的数据进行内联拼接,但时间没有必要精确到秒,微秒. df['datetime'] = pd.to_datetime(df['datetime']) df = df.set_index ...

  2. Angular集成admin-lte框架

    其实上一篇里面提到的集成datatables.net就是admin-lte里面的一个子插件,不过这个子插件,他是自带types定义文件的,admin-lte这个东西在DefinitelyTyped里面 ...

  3. Android两级嵌套ListView滑动问题的解决

    Android下面两级嵌套ListView会出现滑动失效,解决方案,把两级Listview全换成NoScrollListView,代码如下: public class NoScrollListView ...

  4. Java数组的复制全解

    1.将一个基本数据类型数组的引用赋值给另一个数组 public class Array_copy { int[] array1=new int[]{1,2,3,4,5,6}; int[] array2 ...

  5. SSH项目的pom.xml文件

    <!-- 属性 --> <properties> <spring.version>4.2.4.RELEASE</spring.version> < ...

  6. 洛谷 P3177 树上染色 解题报告

    P3177 [HAOI2015]树上染色 题目描述 有一棵点数为\(N\)的树,树边有边权.给你一个在\(0\) ~ \(N\)之内的正整数\(K\),你要在这棵树中选择\(K\)个点,将其染成黑色, ...

  7. js基础进阶--图片上传时实现本地预览功能的原理

    欢迎访问我的个人博客:http://www.xiaolongwu.cn 前言 最近在项目上加一个图片裁剪上传的功能,用的是cropper插件,注意到选择本地图片后就会有预览效果,这里整理一下这种预览效 ...

  8. dubbo-admin和dubbo-monitor的安装

    一.安装dubbo-admin 去这里 http://download.csdn.net/download/u013081610/10044744 下载dubbo-admin.war 部署dubbo- ...

  9. .Net Core微服务系列--理论篇

    微服务的由来 微服务最早由Martin Fowler与James Lewis于2014年共同提出来的,但是微服务也不是一个全新的概念,它是由一系列在实践中获得成功并流行起来的概念中总结出来的一种模式, ...

  10. php对接微信小程序支付

    前言:这里我就假装你已经注册了微信小程序,并且基本的配置都已经好了.注: 个人注册小程序不支持微信支付,所以我还是假装你是企业或者个体工商户的微信小程序,其他的商户号注册,二者绑定,授权,支付开通,就 ...