百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

惊艳面试官-Java中关于随机数生成8种方式的思考

toyiye 2024-09-12 21:01 9 浏览 0 评论

Java中生成随机数常用的有下面这8种写法:简而言之,名称带安全的未必安全,名字简洁的未必简单。

  • Math.random()
  • Random
  • ThreadLocalRandom
  • SecureRandom
  • UUID.randomUUID()
  • RandomStringUtils
  • RandomUtils
  • RandomUtil


Math.random()


Talk is cheap, show me the code. 先上源码:

public static double random() {
    return RandomNumberGeneratorHolder.randomNumberGenerator.nextDouble();
}
private static final class RandomNumberGeneratorHolder {
    static final Random randomNumberGenerator = new Random();
}


从源码可以看出:Math.random本质上用的就是new Random。而且人家给了一个最佳实践:使用的时候不用每次就new一个对象,直接做成一个静态类就可以了。


Random


Random是伪随机数类,采用线性同余算法产生的。伪随机数是1946年冯诺依曼提出来的。他在参加一个氢弹的制造项目,需要大量的随机数模拟核聚变、核裂变。因为当时设备没办法去存储大量的随机数,所以就想到了这个办法。伪随机数(或称伪乱数)这个称呼是因为真随机数都不是计算出来的。这里是使用一个确定性的算法计算出来的似乎是随机的数序,因此伪随机数实际上并不随机。从更宏大的视角看来,是非常有规律的,就像我在《大话高可用》里提到的炊烟。


下面来说说这个线性同余算法,大家不用担心。我比大家还对算法一窍不通。但是我有人类的常识,这个常识是什么呢?既然是计算得到的,并且人类看起来没有规律,那一定有一个外部因子(种子),种子是一个变量。顺着这个思路看源码:


/**
 * Creates a new random number generator. This constructor sets
 * the seed of the random number generator to a value very likely
 * to be distinct from any other invocation of this constructor.
 */
public Random() {
    this(seedUniquifier() ^ System.nanoTime());
}


没想到在构造函数里就给出了答案,这个因子就是系统时间,精确到纳秒。这里为了方便描述,我们举例想要一个int类型的随机数(实际它可以产生其他数值类型的随机数)。来看看源码:

public int nextInt() {
    return next(32);
}
protected int next(int bits) {
    long oldseed, nextseed;
    AtomicLong seed = this.seed;
    do {
        oldseed = seed.get();
        nextseed = (oldseed * multiplier + addend) & mask;
    } while (!seed.compareAndSet(oldseed, nextseed));
    return (int)(nextseed >>> (48 - bits));
}


解释一下:调用nextInt默认会生成32个比特位的数字。调用next方法,会有oldseed和nextseed。计算出nextseed会使CAS,将nextseed替换现有oldseed。nextseed的计算方法是oldseed做一些运算,运算的其他数值都是常量。最终返回nextseed值。


因为使用了AtomicLong、自旋+CAS(可以参考《系统梳理一下锁》),所以Random生成随机数是线程安全的。new一个全局变量,下次不用新建,Math.random的调用方法是合理的。


通过这个源码大胆猜测一下:如果两个不同的进程或者线程,在纳秒级相同的时间同时调用new Random,这时候nextInt的返回值是相同的!下次调用nextInt的值也相同!想要验证的话,可以下载java源码,在new Random的地方稍作修改,

System.nanoTime()改成固定值,这里我就不验证了。


ThreadLocalRandom


上面提到Random使用了线程安全的算法:AtomicLong、自旋+CAS。这在保证线程安全的同时造成了很大的性能开销。ThreadLocalRandom是JDK 7之后提供并发产生随机数,能够解决多个线程发生的竞争。


它不是直接用new实例化,而是第一次使用时初始化其静态方法current()。直接调用静态方法,可以每个线程实例化一个。有朋友测试过,100个线程的情况下,3分钟,Math.random可以跑几百次。而

ThreadLocalRandom.current().nextInt()可以跑十几万次!


来看一下它是怎么做到的:

public static ThreadLocalRandom current() {
    if (UNSAFE.getInt(Thread.currentThread(), PROBE) == 0)
        localInit();
    return instance;
}
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);
}


简而言之,Random是通过系统时间这个外部因子,而ThreadLocalRandom都是通过UNSAFE调用本地方法拿到线程本身的一些变量作为外部因子。所有的参数绑定在线程本身,和其他线程没有竞争,所以可以不加锁就保证线程安全。


public int nextInt() {
    return mix32(nextSeed());
}private static int mix32(long z) {
    z = (z ^ (z >>> 33)) * 0xff51afd7ed558ccdL;
    return (int)(((z ^ (z >>> 33)) * 0xc4ceb9fe1a85ec53L) >>> 32);
}
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;
}


生成随机数计算的时候也都是调用线程内的变量,不加锁。大家可以举一反三一下,回忆一下ThreadLocal相关知识。


SecureRandom


在安全应用场景,随机数应该使用安全的随机数。密码学意义上的安全随机数,要求必须保证其不可预测性。


由于 Random 是采用时间作为随机数种子,如果黑客知道随机数产生的时间,那就能重现随机数。而 SecureRandom 属于强随机数,一般不单独采用时间作为随机数种子,除了系统时间,还会采用临时文件夹中大小、某个线程从休眠到被唤醒所耗的时间等等一系列无法重现的值作为随机数种子。


因为SecureRandom 采用了很多外部参数,会产生熵源不足时阻塞问题。在我做过的项目中,因为有的业务凌晨没有流量,这个问题实际发生过。建议有明显低谷的业务,低谷时低于1TPS时不要用。


像SecureRandom这种强随机在很多场景下效果不一定好。有一个小故事说itunes之前播歌的时候采用的是真随机。有些用户总是抱怨说你这是真随机吗?怎么来来回回给我播放那几首歌。苹果公司有苦说不出,于是把算法改成伪随机:洗牌算法,先把所有的歌打乱顺序,然后按顺序播放。改完之后用户纷纷点赞。


适合使用SecureRandom的场景,比如要给每个资源生成一个url,为了防止url规律被别人攻破,使用爬虫爬取到自己的资源可以用这个。


UUID.randomUUID()


看下面的代码就知道UUID.randomUUID()是基于SecureRandom

public static UUID randomUUID() {
    SecureRandom ng = Holder.numberGenerator;
    byte[] randomBytes = new byte[16];
    ng.nextBytes(randomBytes);
    randomBytes[6]  &= 0x0f;  /* clear version        */
    randomBytes[6]  |= 0x40;  /* set to version 4     */
    randomBytes[8]  &= 0x3f;  /* clear variant        */
    randomBytes[8]  |= 0x80;  /* set to IETF variant  */
    return new UUID(randomBytes);
}


有的朋友说不对呀,网上告诉我UUID是时间戳+时钟序列+MAC地址。注意上面代码注释有个set to version 4。

从UUID的类注释里就可以看到java 8提到4个版本:

The version field holds a value that describes the type of this {@code
* UUID}.  There are four different basic types of UUIDs: time-based, DCE
* security, name-based, and randomly generated UUIDs.  These types have a
* version value of 1, 2, 3 and 4, respectively.


翻译一下:

版本1 - 基于时间

版本2 - 基于DCE sercrity

版本3 - 基于名字(MD5)

版本4 - 基于随机数

记住啦:java默认UUID是基于随机数的!


证明一下:如果是基于时间戳的,生成的UUID前几位应该相同

@Test
public void test() {
    for (int i = 0; i < 3; i++) {
        System.out.println(UUID.randomUUID());
    }
}


结果:

a0858771-c903-4061-ba6a-53efc372d8a0

17ebdc58-db6c-452f-9b21-b4f54d3958a5

116d6a4f-e50a-4001-b94c-61596da3a75d


事实胜于记忆中的知识,它有可能是错的!


java UUID也支持版本3:


@Test
public void test() {
    for (int i = 0; i < 3; i++) {
        System.out.println(UUID.nameUUIDFromBytes("编程一生".getBytes()));
    }
}


运行结果:

743f24b2-0914-363b-ace1-f4da750dccad

743f24b2-0914-363b-ace1-f4da750dccad

743f24b2-0914-363b-ace1-f4da750dccad


这个在你的机器上执行也是这个结果,就是MD5了一下。


有人说版本1、版本2怎么用,好像还听说过版本5。可以用linux命令

uuid -n 3 -v1


运行结果:

5b01cea2-9561-11e9-965b-a3d050dd0f23

5b01cf60-9561-11e9-965c-1b66505f5845

5b01d118-9561-11e9-965d-97354eb9e914


linux命令版本可指定!


RandomStringUtils、RandomUtils和RandomUtil


这两个不是Java原生的方法,apache commons下有这两个工具类,本质上还是Random。跟进代码去看可以发现:RandomUtils用的是JVMRandom。这个只不过是封装了一个静态的Random,所有使用RandomUtils类的,全局只用一个,减少了新建对象成本。


如果你问我一般情况下建议用哪个,我选ThreadLocalRandom。那这么好的方法是不是应该也有对应的封装工具类呢?有的,hultool就搞了一个RandomUtil,

RandomUtil.getRandom()返回的就是ThreadLocalRandom。

RandomUtil.getSecureRandom()返回的是SecureRandom,提供多种选择。



总结


一张图表示他们之间的关系:

在《CURD系统怎么做出技术含量--怎样引导面试》里我提到可以利用工作中总结的技巧惊艳面试官,如果能把一个问题系统性讲明白,甚至让面试官意识到之前的知识竟然是错的,也一定能让面试官眼前一亮。

原文链接:https://mp.weixin.qq.com/s/9ReYiLCyvJKPXK07fXmrvg

相关推荐

为何越来越多的编程语言使用JSON(为什么编程)

JSON是JavascriptObjectNotation的缩写,意思是Javascript对象表示法,是一种易于人类阅读和对编程友好的文本数据传递方法,是JavaScript语言规范定义的一个子...

何时在数据库中使用 JSON(数据库用json格式存储)

在本文中,您将了解何时应考虑将JSON数据类型添加到表中以及何时应避免使用它们。每天?分享?最新?软件?开发?,Devops,敏捷?,测试?以及?项目?管理?最新?,最热门?的?文章?,每天?花?...

MySQL 从零开始:05 数据类型(mysql数据类型有哪些,并举例)

前面的讲解中已经接触到了表的创建,表的创建是对字段的声明,比如:上述语句声明了字段的名称、类型、所占空间、默认值和是否可以为空等信息。其中的int、varchar、char和decimal都...

JSON对象花样进阶(json格式对象)

一、引言在现代Web开发中,JSON(JavaScriptObjectNotation)已经成为数据交换的标准格式。无论是从前端向后端发送数据,还是从后端接收数据,JSON都是不可或缺的一部分。...

深入理解 JSON 和 Form-data(json和formdata提交区别)

在讨论现代网络开发与API设计的语境下,理解客户端和服务器间如何有效且可靠地交换数据变得尤为关键。这里,特别值得关注的是两种主流数据格式:...

JSON 语法(json 语法 priority)

JSON语法是JavaScript语法的子集。JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔花括号保存对象方括号保存数组JS...

JSON语法详解(json的语法规则)

JSON语法规则JSON语法是JavaScript对象表示法语法的子集。数据在名称/值对中数据由逗号分隔大括号保存对象中括号保存数组注意:json的key是字符串,且必须是双引号,不能是单引号...

MySQL JSON数据类型操作(mysql的json)

概述mysql自5.7.8版本开始,就支持了json结构的数据存储和查询,这表明了mysql也在不断的学习和增加nosql数据库的有点。但mysql毕竟是关系型数据库,在处理json这种非结构化的数据...

JSON的数据模式(json数据格式示例)

像XML模式一样,JSON数据格式也有Schema,这是一个基于JSON格式的规范。JSON模式也以JSON格式编写。它用于验证JSON数据。JSON模式示例以下代码显示了基本的JSON模式。{"...

前端学习——JSON格式详解(后端json格式)

JSON(JavaScriptObjectNotation)是一种轻量级的数据交换格式。易于人阅读和编写。同时也易于机器解析和生成。它基于JavaScriptProgrammingLa...

什么是 JSON:详解 JSON 及其优势(什么叫json)

现在程序员还有谁不知道JSON吗?无论对于前端还是后端,JSON都是一种常见的数据格式。那么JSON到底是什么呢?JSON的定义...

PostgreSQL JSON 类型:处理结构化数据

PostgreSQL提供JSON类型,以存储结构化数据。JSON是一种开放的数据格式,可用于存储各种类型的值。什么是JSON类型?JSON类型表示JSON(JavaScriptO...

JavaScript:JSON、三种包装类(javascript 包)

JOSN:我们希望可以将一个对象在不同的语言中进行传递,以达到通信的目的,最佳方式就是将一个对象转换为字符串的形式JSON(JavaScriptObjectNotation)-JS的对象表示法...

Python数据分析 只要1分钟 教你玩转JSON 全程干货

Json简介:Json,全名JavaScriptObjectNotation,JSON(JavaScriptObjectNotation(记号、标记))是一种轻量级的数据交换格式。它基于J...

比较一下JSON与XML两种数据格式?(json和xml哪个好)

JSON(JavaScriptObjectNotation)和XML(eXtensibleMarkupLanguage)是在日常开发中比较常用的两种数据格式,它们主要的作用就是用来进行数据的传...

取消回复欢迎 发表评论:

请填写验证码