Java生成随机数的5种方式,你知道几种?(建议收藏)
1. Math.random() 静态方法
产生的随机数是 0 – 1 之间的一个 double ,即 0 <= random <= 1 。
使用:结果:实现原理:
当第一次调用 Math.random() 方法时,自动创建了一个伪随机数生成器 ,实际上用的是 new java.util.Random() 。当接下来继续调用 Math.random() 方法时,就会使用这个新的伪随机数生成器 。
源码如下:
initRNG() 方法是 synchronized 的,因此在多线程情况下,只有一个线程会负责创建伪随机数生成器 (使用当前时间作为种子),其他线程则利用该伪随机数生成器 产生随机数。
因此 Math.random() 方法是线程安全的。什么情况下随机数的生成线程不安全:
- 线程1在第一次调用 random() 时产生一个生成器 generator1 ,使用当前时间作为种子。
- 线程2在第一次调用 random() 时产生一个生成器 generator2 ,使用当前时间作为种子。
- 碰巧 generator1 和 generator2 使用相同的种子,导致 generator1 以后产生的随机数每次都和 generator2 以后产生的随机数相同。
什么情况下随机数的生成线程安全: Math.random() 静态方法使用
- 线程1在第一次调用 random() 时产生一个生成器 generator1 ,使用当前时间作为种子。
- random() generator1 generator1
结果:2. java.util.Random 工具类
基本算法:linear congruential pseudorandom number generator (LGC) 线性同余法伪随机数生成器 缺点:可预测
An attacker will simply compute the seed from the output values observed. This takes significantly less time than 2^48 in the case of java.util.Random. 从输出中可以很容易计算出种子值。It is shown that you can predict future Random outputs observing only two(!) output values in time roughly 2^16. 因此可以预测出下一个输出的随机数。 You should never use an LCG for security-critical purposes. 在注重信息安全的应用中,不要使用 LCG 算法生成随机数,请使用 SecureRandom。
使用:结果:Random类默认使用当前系统时钟作为种子:
Random类提供的方法:API
- nextBoolean() true false
- nextBytes(byte[] bytes)
- nextDouble() – 返回 0.0 到 1.0 之间的均匀分布的 double
- nextFloat() – 返回 0.0 到 1.0 之间的均匀分布的 float
- nextGaussian() – 返回 0.0 到 1.0 之间的高斯分布(即正态分布)的 double
- nextInt() – 返回均匀分布的 int
- nextInt(int n) – 返回 0 到 n 之间的均匀分布的 int (包括 0,不包括 n)
- nextLong() – 返回均匀分布的 long
- setSeed(long seed) – 设置种子
只要种子一样,产生的随机数也一样:因为种子确定,随机数算法也确定,因此输出是确定的!
结果:3. java.util.concurrent.ThreadLocalRandom 工具类
ThreadLocalRandom 是 JDK 7 之后提供, 也是继承至 java.util.Random。
每一个线程有一个独立的 随机数生成器 ,用于并发产生随机数,能够解决多个线程发生的竞争争夺。 效率更高!
ThreadLocalRandom 不是直接用 new 实例化,而是第一次使用其静态方法 current() 得到 ThreadLocal<ThreadLocalRandom> 实例,然后调用 java.util.Random 类提供的方法获得各种随机数。
使用:结果:4. java.Security.SecureRandom也是继承至 java.util.Random。
Instances of java.util.Random are not cryptographically secure.Consider instead using SecureRandom to get a cryptographically secure pseudo-random number generator for use by security-sensitive applications. SecureRandom takes Random Data from your os (they can be interval between keystrokes etc – most os collect these data store them in files – /dev/random and /dev/urandom in case of linux/solaris) and uses that as the seed. 操作系统收集了一些随机事件,比如鼠标点击,键盘点击等等,SecureRandom 使用这些随机事件作为种子。
SecureRandom 提供加密的强随机数生成器 (RNG),要求种子必须是不可预知 的,产生非确定性 输出。 SecureRandom 也提供了与实现无关的算法,因此,调用方(应用程序代码)会请求特定的 RNG 算法并将它传回到该算法的 SecureRandom 对象中。
- 如果仅指定算法名称,如下所示: SecureRandom random = SecureRandom.getInstance(\”SHA1PRNG\”);
- 如果既指定了算法名称又指定了包提供程序,如下所示: SecureRandom random = SecureRandom.getInstance(\”SHA1PRNG\”, \”SUN\”);
使用:结果:5. 随机字符串
可以使用 Apache Commons-Lang 包中的 RandomStringUtils 类。Maven 依赖如下:
API 参考:https://commons.apache.org/proper/commons-lang/javadocs/api-2.6/org/apache/commons/lang/RandomStringUtils.html
示例:
RandomStringUtils 类的实现上也是依赖了 java.util.Random 工具类:
RandomStringUtils 类的定义
忘掉 Snowflake,感受一下性能高出587倍的全局唯一ID生成算法
今天我们来拆解 Snowflake 算法,同时领略百度、美团、腾讯等大厂在全局唯一 ID 服务方面做的设计,接着根据具体需求设计一款全新的全局唯一 ID 生成算法。这还不够,我们会讨论到全局唯一 ID 服务的分布式 CAP 选择与性能瓶颈。
已经熟悉 Snowflake 的朋友可以先去看大厂的设计和权衡。
百度 UIDGenertor:github.com/baidu/uid-g…
美团 Leaf:tech.meituan.com/2017/04/21/…
腾讯 Seqsvr: www.infoq.cn/article/wec…
全局唯一 ID 是分布式系统和订单类业务系统中重要的基础设施。这里引用美团的描述:
在复杂分布式系统中,往往需要对大量的数据和消息进行唯一标识。如在美团点评的金融、支付、餐饮、酒店、猫眼电影等产品的系统中,数据日渐增长,对数据分库分表后需要有一个唯一 ID 来标识一条数据或消息,数据库的自增 ID 显然不能满足需求;特别一点的如订单、骑手、优惠券也都需要有唯一 ID 做标识。
这时候你可能会问:我还是不懂,为什么一定要全局唯一 ID?
我再列举一个场景,在 MySQL 分库分表的条件下,MySQL 无法做到依次、顺序、交替地生成 ID,这时候要保证数据的顺序,全局唯一 ID 就是一个很好的选择。
在爬虫场景中,这条数据在进入数据库之前会进行数据清洗、校验、矫正、分析等多个流程,这期间有一定概率发生重试或设为异常等操作,也就是说在进入数据库之前它就需要有一个 ID 来标识它。
美团技术团队列出的 4 点属性我觉得很准确,它们是:
- 全局唯一性:不能出现重复的 ID 号,既然是唯一标识,这是最基本的要求;
- 趋势递增:在 MySQL InnoDB 引擎中使用的是聚集索引,由于多数 RDBMS 使用 B-tree 的数据结构来存储索引数据,在主键的选择上面我们应该尽量使用有序的主键保证写入性能;
- 单调递增:保证下一个 ID 一定大于上一个 ID,例如事务版本号、IM 增量消息、排序等特殊需求;
- 信息安全:如果 ID 是连续的,恶意用户的爬取工作就非常容易做了,直接按照顺序下载指定 URL 即可;如果是订单号就更危险了,竞争对手可以直接知道我们一天的单量。所以在一些应用场景下,会需要 ID 无规则、不规则。
看上去第 3 点和第 4 点似乎还存在些许冲突,这个后面再说。除了以上列举的 ID 属性外,基于这个生成算法构建的服务还需要买足高 QPS、高可用性和低延迟的几个要求。
大家在念书的时候肯定都学过 UUID 和 GUID,它们生成的值看上去像这样:
由于不是纯数字组成,这就无法满足趋势递增和单调递增这两个属性,同时在写入时也会降低写入性能。上面提到了数据库自增 ID 无法满足入库前使用和分布式场景下的需求,遂排除。
有人提出了借助 Redis 来实现,例如订单号=日期+当日自增长号,自增长通过 INCR 实现。但这样操作的话又无法满足编号不可猜测需求。
这时候有人提出了 MongoDB 的 ObjectID,不要忘了它生成的 ID 是这样的: 5b6b3171599d6215a8007se0,和 UUID 一样无法满足递增属性,且和 MySQL 一样要入库后才能生成。
难道就没有能打的了吗?
Twitter 于 2010 年开源了内部团队在用的一款全局唯一 ID 生成算法 Snowflake,翻译过来叫做雪花算法。Snowflake 不借助数据库,可直接由编程语言生成,它通过巧妙的位设计使得 ID 能够满足递增属性,且生成的 ID 并不是依次连续的,能够满足上面提到的全局唯一 ID 的 4 个属性。它连续生成的 3 个 ID 看起来像这样:
Snowflake 以 64 bit 来存储组成 ID 的4 个部分:
1、最高位占1 bit,值固定为 0,以保证生成的 ID 为正数;
2、中位占 41 bit,值为毫秒级时间戳;
3、中下位占 10 bit,值为工作机器的 ID,值的上限为 1024;
4、末位占 12 bit,值为当前毫秒内生成的不同 ID,值的上限为 4096;
Snowflake 的代码实现网上有很多款,基本上各大语言都能找到实现参考。我之前在做实验的时候在网上找到一份 Golang 的代码实现:
代码可在我的 Gist 查看和下载。
snowflake 不依赖数据库,也不依赖内存存储,随时可生成 ID,这也是它如此受欢迎的原因。但因为它在设计时通过时间戳来避免对内存和数据库的依赖,所以它依赖于服务器的时间。上面我们提到了 Snowflake 的 4 段结构,实际上影响 ID 大小的是较高位的值,由于最高位固定为 0,遂影响 ID 大小的是中位的值,也就是时间戳。
试想,服务器的时间发生了错乱或者回拨,这就直接影响到生成的 ID,有很大概率生成重复的 ID 且一定会打破递增属性。这是一个致命缺点,你想想,支付订单和购买订单的编号重复,这是多么严重的问题!
另外,由于它的中下位和末位 bit 数限制,它每毫秒生成 ID 的上限严重受到限制。由于中位是 41 bit 的毫秒级时间戳,所以从当前起始到 41 bit 耗尽,也只能坚持 70 年。
再有,程序获取操作系统时间会耗费较多时间,相比于随机数和常数来说,性能相差太远,这是制约它生成性能的最大因素。
长话短说,我们来看看百度、美团、腾讯(微信)是如何做的。
百度团队开源了 UIDGenerator 算法.
它通过借用未来时间和双 Buffer 来解决时间回拨与生成性能等问题,同时结合 MySQL 进行 ID 分配。这是一种基于 Snowflake 的优化操作,是一个好的选择,你认为这是不是优选呢?
美团团队根据业务场景提出了基于号段思想的 Leaf-Segment 方案和基于 Snowflake 的 Leaf-Snowflake 方案.
出现两种方案的原因是 Leaf-Segment 并没有满足安全属性要求,容易被猜测,无法用在对外开放的场景(如订单)。Leaf-Snowflake 通过文件系统缓存降低了对 ZooKeeper 的依赖,同时通过对时间的比对和警报来应对 Snowflake 的时间回拨问题。这两种都是一个好的选择,你认为这是不是优选呢?
微信团队业务特殊,它有一个用 ID 来标记消息的顺序的场景,用来确保我们收到的消息就是有序的。在这里不是全局唯一 ID,而是单个用户全局唯一 ID,只需要保证这个用户发送的消息的 ID 是递增即可。
这个项目叫做 Seqsvr,它并没有依赖时间,而是通过自增数和号段来解决生成问题的。这是一个好的选择,你认为这是不是优选呢?
在了解 Snowflake 的优缺点、阅读了百度 UIDGenertor、美团 Leaf 和腾讯微信 Seqsvr 的设计后,我希望设计出一款能够满足全局唯一 ID 4 个属性且性能更高、使用期限更长、不受单位时间限制、不依赖时间的全局唯一 ID 生成算法。
这看起来很简单,但吸收所学知识、设计、实践和性能优化占用了我 4 个周末的时间。在我看来,这个算法的设计过程就像是液态的水转换为气状的雾一样,遂我给这个算法取名为薄雾(Mist)算法。接下来我们来看看薄雾算法是如何设计和实现的。
位数是影响 ID 数值上限的主要因素,Snowflake 中下位和末位的 bit 数限制了单位时间内生成 ID 的上限,要解决这个两个问题,就必须重新设计 ID 的组成。
抛开中位,我们先看看中下位和末位的设计。中下位的 10 bit 的值其实是机器编号,末位 12 bit 的值其实是单位时间(同一毫秒)内生成的 ID 序列号,表达的是这毫秒生成的第 5 个或第 150 个 数值,同时二者的组合使得 ID 的值变幻莫测,满足了安全属性。实际上并不需要记录机器编号,也可以不用管它到底是单位时间内生成的第几个数值,安全属性我们可以通过多组随机数组合的方式实现,随着数字的递增和随机数的变幻,通过 ID 猜顺序的难度是很高的。
最高位固定是 0,不需要对它进行改动。我们来看看至关重要的中位,Snowflake 的中位是毫秒级时间戳,既然不打算依赖时间,那么肯定也不会用时间戳,用什么呢?我选择自增数 1,2,3,4,5,…。中位决定了生成 ID 的上限和使用期限,如果沿用 41 bit,那么上限跟用时间戳的上限相差无几,经过计算后我选择采用与 Snowflake 的不同的分段:
缩减中下位和末位的 bit 数,增加中位的 bit 数,这样就可以拥有更高的上限和使用年限,那上限和年限现在是多久呢?中位数值的上限计算公式为 int64(1<<47 – 1),计算结果为 140737488355327 。百万亿级的数值,假设每天消耗 10 亿 ID,薄雾算法能用 385+ 年,几辈子都用不完。
中下位和末位都是 8 bit,数值上限是 255,即开闭区间是 [0, 255]。这两段如果用随机数进行填充,对应的组合方式有 256 * 256 种,且每次都会变化,猜测难度相当高。由于不像 Snowflake 那样需要计算末位的序列号,遂薄雾算法的代码并不长,具体代码可在我的 GitHub 仓库找到:
聊聊性能问题,获取时间戳是比较耗费性能的,不获取时间戳速度当然快了,那 500+ 倍是如何得来的呢?以 Golang 为例(我用 Golang 做过实验),Golang 随机数有三种生成方式:
- 基于固定数值种子的随机数;
- 将会变换的时间戳作为种子的随机数;
- 大数真随机;
基于固定数值种子的随机数每次生成的值都是一样的,是伪随机,不可用在此处。将时间戳作为种子以生成随机数是目前 Golang 开发者的主流做法,实测性能约为 8800 ns/op。大数真随机知道的人比较少,实测性能 335ns/op,由此可见性能相差近 30 倍。
大数真随机也有一定的损耗,如果想要将性能提升到顶点,只需要将中下位和末位的随机数换成常数即可,常数实测性能 15ns/op,是时间戳种子随机数的 587 倍。
要注意的是,将常数放到中下位和末位的性能是很高,但是猜测难度也相应下降。
薄雾算法为了避开时间依赖,不得不依赖存储,中位自增的数值只能在内存中存活,遂需要依赖存储将自增数值存储起来,避免因为宕机或程序异常造成重复 ID 的事故。
看起来是这样,但它真的是依赖存储吗?
你想想,这么重要的服务必定要求高可用,无论你用 Twitter 还是百度或者美团、腾讯微信的解决方案,在架构上一定都是高可用的,高可用一定需要存储。在这样的背景下,薄雾算法的依赖其实并不是额外的依赖,而是可以与架构完全融合到一起的设计。
既然提出了薄雾算法,怎么能不提供真实可用的工程实践呢?在编写完薄雾算法之后,我就开始了工程实践的工作,将薄雾算法与 KV 存储结合到一起,提供全局唯一 ID 生成服务。这里我选择了较为熟悉的 Redis,Mist 与 Redis 的结合,我为这个项目取的名字为 Medis。
性能高并不是编造出来的,我们看看它 Jemeter 压测参数和结果:
以上是 Medis README 中给出的性能测试截图,在大基数条件下的性能约为 2.5w/sec。这么高的性能除了薄雾算法本身高性能之外,Medis 的设计也作出了很大贡献:
- 使用 Channel 作为数据缓存,这个操作使得发号服务性能提升了 7 倍;
- 采用预存预取的策略保证 Channel 在大多数情况下都有值,从而能够迅速响应客户端发来的请求;
- 用 Gorouting 去执行耗费时间的预存预取操作,不会影响对客户端请求的响应;
- 采用 Lrange Ltrim 组合从 Redis 中批量取值,这比循环单次读取或者管道批量读取的效率更高;
- 写入 Redis 时采用管道批量写入,效率比循环单次写入更高;
- Seqence 值的计算在预存前进行,这样就不会耽误对客户端请求的响应,虽然薄雾算法的性能是纳秒级别,但并发高的时候也造成一些性能损耗,放在预存时计算显然更香;
- 得益于 Golang Echo 框架和 Golang 本身的高性能,整套流程下来我很满意,如果要追求极致性能,我推荐大家试试 Rust;
Medis 服务启动流程和接口访问流程图下所示:
感兴趣的朋友可以下载体验一下,启动 Medis 根目录的 server.go 后,访问 http://localhost:1558/sequence 便能拿到全局唯一 ID。
分布式 CAP (一致性、可用性、分区容错性)已成定局,这类服务通常追求的是可用性架构(AP)。由于设计中采用了预存预取,且要保持整体顺序递增,遂单机提供访问是优选,即分布式架构下的性能上限就是提供服务的那台主机的单机性能。
你想要实现分布式多机提供服务?
这样的需求要改动 Medis 的逻辑,同时也需要改动各应用之间的组合关系。如果要实现分布式多机同时提供服务,那么就要废弃 Redis 和 Channel 预存预取机制,接着放弃 Channel 而改用即时生成,这样便可以同时使用多个 Server,但性能的瓶颈就转移到了 KV 存储(这里是 Redis),性能等同于单机 Redis 的性能。你可以采用 ETCD 或者 Zookeeper 来实现多 KV,但这不是又回到了 CAP 原点了吗?
至于怎么选择,可根据实际业务场景和需求与架构进行讨论,选择一个适合的方案进行部署即可。
领略了 Mist 和 Medis 的风采后,相信你一定会有其他巧妙的想法,欢迎在评论区留言,我们一起交流进步!
夜幕团队成立于 2019 年,团队包括崔庆才(静觅)、周子淇(Loco)、陈祥安(CXA)、唐轶飞(大鱼|BruceDone)、冯威(妄为)、蔡晋(的老板)、戴煌金(咸鱼)、张冶青(MarvinZ)、韦世东(Asyncins|奎因)和文安哲(sml2h3)。
涉猎的编程语言包括但不限于 Python、Rust、C++、Go,领域涵盖爬虫、深度学习、服务研发、逆向工程、软件安全等。团队非正亦非邪,只做认为对的事情,请大家小心。
作者:夜幕镇岳剑丨韦世东链接:https://juejin.im/post/5efefaaa6fb9a07e644ee60f
js 实现随机3D,并记录
原生态JavaScript ,自己可以用JQuery来升级
Math.round()函数是JavaScript中用于四舍五入取整的内置函数,其作用是将一个数字四舍五入到最接近的整数。在数学计算和数据处理过程中,我们经常需要对小数进行取整操作,以便得到更为简洁、易于比较的数据结果。Math.round()函数正是为此目的而生的。
一、Math.round()函数的作用
数据简化:在数据分析和处理过程中,对于大量的小数数据,直接使用原始数据可能会增加计算复杂度和存储空间。通过使用Math.round()函数对数据进行四舍五入取整,可以有效地简化数据,提高计算效率和节省存储空间。
便于比较:在进行数值比较时,使用整数值比使用小数更为直观和便捷。通过将小数四舍五入为整数,可以直接使用等于(==)或不等于(!=)运算符进行比较,避免了浮点数精度问题带来的困扰。
可视化展示:在数据可视化中,坐标轴通常以刻度的形式显示整数值。如果将小数值直接映射到坐标轴上,可能会导致刻度线过于密集或不易读取。通过使用Math.round()函数将数据取整后再绘制图形,可以使坐标轴刻度更加清晰易读。
JavaScript的 Math.random() 函数用于生成一个0到1之间(包括0,但不包括1)的伪随机数。尽管结果看起来是无序且随机的,但它们实际上是通过一种算法确定性地生成的。这使得Math.random()生成的数值在统计分析中具有随机性,但从理论上讲,如果您知道算法的内部工作原理以及它使用的种子值,您就可以预测接下来会生成哪个数字。
Math.random()背后的算法因不同的JavaScript引擎而异,如V8、SpiderMonkey或JavaScriptCore等,每种引擎都有自己的实现方式,但所有实现都致力于确保生成的数值序列尽可能看起来是“随机”的。
本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com
文章为作者独立观点不代本网立场,未经允许不得转载。