浅析Java源码之Math.random()
从零自学java消遣一下,看书有点脑阔疼,不如看看源码!(๑╹◡╹)ノ"""
JS中Math调用的都是本地方法,底层全是用C++写的,所以完全无法观察实现过程,Java的工具包虽然也有C/C++的介入,不过也有些是自己实现的。
本篇文章主要简单阐述Math.random()的实现过程。
Math隶属于java.lang包中,默认加载。本身是一个final类,方法都是静态方法,所以使用的时候不需要生成一个实例,直接调用Math.XX就行了。
一步一步观察该方法,首先是java.lang.Math:
public final class Math {
// 大量静态变量与方法
// ...
private static Random randomNumberGenerator;
private static synchronized void initRNG() {
if (randomNumberGenerator == null)
randomNumberGenerator = new Random();
}
public static double random() {
if (randomNumberGenerator == null) initRNG();
return randomNumberGenerator.nextDouble();
}
// ...other
}
这里面与random相关的操作有3个:
1、声明一个私有静态Random类randomNumberGenerator
2、若randomNumberGenerator未初始化,调用new Random()将其初始化
3、若randomNumberGenerator已经初始化,调用nextDouble方法并将其值返回
tips:synchronized关键字代表同步执行此方法,Java为多线程,所以为了保证randomNumberGenerator对象只被初始化一次,需要该关键字。比如两个线程同时调用了Math.random(),线程A发现rXX未被初始化,进入initRNG调用new Random()方法。此时线程B也发现了rXX未被初始化,但是initRNG是同步方法,所以挂起等待线程A执行完毕。当线程A执行完后把rXX初始化了,所以在initRNG中的if判断,线程B会直接返回。
所以简单来讲,random方法会在第一次调用时生成一个randomNumberGenerator对象,并调用其nextDouble方法生成随机数,之后的调用就只要持续调用此方法返回随机数就行了。
下面来看Random类是个什么鬼,来源于java.util.Random:
public class Random implements java.io.Serializable {
// 静态变量
/** use serialVersionUID from JDK 1.1 for interoperability */
static final long serialVersionUID = 3905348978240129619L;
private final AtomicLong seed;
private final static long multiplier = 0x5DEECE66DL;
private final static long addend = 0xBL;
private final static long mask = (1L << 48) - 1;
// constructor
public Random() { this(++seedUniquifier + System.nanoTime()); }
private static volatile long seedUniquifier = 8682522807148012L;
public Random(long seed) {
this.seed = new AtomicLong(0L);
setSeed(seed);
}
// 设置种子
synchronized public void setSeed(long seed) {
seed = (seed ^ multiplier) & mask;
this.seed.set(seed);
haveNextNextGaussian = false;
}
// 产生大数字
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));
}
// 生成随机数
public double nextDouble() {
return (((long)(next(26)) << 27) + next(27))
/ (double)(1L << 53);
}
// 其他不关心的方法
// nextBytes(bytes [])
// nextInt
// nextInt(int)
// nextLong
// nextBoolean
// nextFloat
// Serializable相关
}
上述代码剔除了大量的注释,还有一些不需要关心的方法,本文只关注Math.random()调用相关方法。
对于这个类,首先来看看它的构造函数,理论上new一个Random实例是需要一个long类型的整数作为参数,但是代码用了this使其默认调用new Random(long)这个构造函数。而在构造函数中又生成了一个新类并赋值给实例变量seed,关于这个AtomicLong类其实没啥好讲的,简单看一下就行:
public class AtomicLong extends Number implements java.io.Serializable {
private static final long serialVersionUID = 1927816293512124184L;
// valueOffset相关...
// 实例变量
private volatile long value;
// 构造函数
public AtomicLong(long initialValue) {
value = initialValue;
}
public AtomicLong() {}
// 方法
public final long get() {
return value;
}
public final void set(long newValue) {
value = newValue;
}
// 这个也会用到 但是不用关心具体实现
public final boolean compareAndSet(long expect, long update) {
return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
}
// 其余不需要关心(其实我也看不懂)的方法
}
如果思想简单一点,可以看出这个类也很简单,初始化传参赋值,set设置,get获取,多简单!
现在回到Random类的构造函数中,实例变量被赋值,类的value为初始化的0(后缀L代表这是一个long类型整数)。下一步调用setSeed,传入构造函数的long类型seed变量(不是seed类),其值为:
++seedUniquifier + System.nanoTime()
// private static volatile long seedUniquifier = 8682522807148012L(8.6825e+15);
// 2^52 ~ 2^53
// 写文章时测试 => System.nanoTime() => 13230650355964(1.323e+13);
其中第一个变量为一个固定值,每次加1,另外一个为System.nanoTime(),该方法返回一个与当前时间相关的数字,具体我不关心。
两个相加后,作为初始种子出传入setSeed方法中,方法第一步会对seed进行二次计算:
seed = (seed ^ multiplier) & mask;
// private final static long multiplier = 0x5DEECE66DL;(25214903917 => 2.5214e+10)
// 2^34 ~ 2^35
// private final static long mask = (1L << 48) - 1;(2^48-1 => 0111...1 => 2^48 = 2.8147+e14)
此处进行的是位运算,这里不用关心具体数值,只关注可能得到的最大最小值。
^ => 异或运算:3 ^ 4 => 011 ^ 100 = 111 => 7(不一样置1,否则置0)
可以看出,两个数字异或运算,假设其中较大的二进制位数为n,结果一定是小于等于2n-1,比如34,4为100三位,所以结果一定小于等于2^3-1,即7。
& => 与运算:3 & 4 => 011 & 100 = 000 => 0(都为1置1,否则置0)
可以看出,与运算的结果总是小于等于较小的那个数。
这样来再来看之前的位运算:
seed(2^52 ~ 2^53) ^ multiplier(2^34 ~ 2^35) => 0 ~ (2^53-1)
(seed ^ multiplier)(0 ~ 2^53-1) & mask(2^48-1) => 0 ~ 2^48-1
结论是种子的范围是在0 ~ 2^48-1之间。
测试代码:
public class test {
public static void main(String [] args){
pro b = new pro();
System.out.println(b.getValue());
// 256403749474577
// 256458702577093
// 256431328421593
}
}
class pro{
long seed = 8682522807148012L + System.nanoTime();
long multiplier = 0x5DEECE66DL;
long mask = (1L << 48) - 1;
long getValue(){
return (seed ^ multiplier) & mask;
}
}
构造函数调用完后,现在来看nextDouble,这个方法除去位运算,本质上就是调用了两次next方法:
public double nextDouble() {
return (((long)(next(26)) << 27) + next(27))
/ (double)(1L << 53);
}
所以直接看next方法:
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));
}
方法内部声明了2个long类型种子:oldseed、nextseed,通过get方法取得之前位运算得到的seed赋值给oldseed,然后再次通过运算得到一个nextseed的值,并传给seed.compareAndSet(oldseed, nextseed)方法中。
关于这个方法,源码里是这样的:
// java.util.concurrent.atomic.AtomicLong;
public class AtomicLong extends Number implements java.io.Serializable {
public final boolean compareAndSet(long expect, long update) {
return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
}
}
// sun.misc.Unsafe.java
public native boolean compareAndSwapLong(Object obj, long offset,long expect, long update);
这个方法是个内部方法,也就是用C/C++实现的,所以有兴趣的自己去看源码,这里贴一个blog:
http://www.cnblogs.com/Mainz/p/3546347.html
方法的用处简单讲也很简单,比较oldseed与内存中预期的值,如果符合,就将nextseed放进去。
这里的运算也不管具体数值,oldseed * multiplier按最大计算会出现溢位,截取成long类型后的大小不确定,所以按照与运算这里的范围依然是0 ~ mask,即0 ~ 2^48-1。
最后返回(int)(nextseed >>> (48 - bits)),这里对结果进行类型处理,贴一个类型范围图:
基本类型 | 最小值 | 最大值 |
---|---|---|
byte | -2^7 | 2^7 - 1 |
short | -2^15 | 2^15 - 1 |
int | -2^31 | 2^31 - 1 |
long | -2^63 | 2^63 - 1 |
若结果是大于int类型最大值,超出的部分会被直接截取砍掉。
最后看nextDouble的计算式:
(((long)(next(26)) << 27) + next(27)) / (double)(1L << 53)
传入的bits分别为26与27,这时返回的随机数为:
(int)(nextseed >>> 22) 与 (int)(nextseed >>> 21)
>>>为无符号右移,具体意思就不解释了。
得到的结果范围大概是 0 ~ 2^26(27)-1,理论上在这里是不会超过int的最大值。
当seed(测试代码中的tmp)为mask时,此时计算会达到最大值:
(((long)(1L << 53)-1 ) / (double)(1L << 53)
测试代码:
public class test {
public static void main(String [] args){
testb bb = new testb();
long a = (long)bb.getNext(26);
long b = bb.getNext(27);
double c = 1L << 53;
double d = ((a<<27) +b)/c;
// 0.99999999...
System.out.println(d);
}
}
class testb{
long tmp = (1L<<48)-1;
// long tmp = 0 => 0.0
int getNext(int num){
return (int)(tmp >>> (48 - num));
}
}
当测试代码中tmp为0时,计算结果为最小值0。
每一次调用nextDouble,会生成不一样的seed,也就会返回不一样的数字。
这样就是整个随机数生成过程。
完结,撒花ヽ(゚∀゚)メ(゚∀゚)ノ
浅析Java源码之Math.random()的更多相关文章
- 浅析Java源码之LinkedList
可以骂人吗???辛辛苦苦写了2个多小时搞到凌晨2点,点击保存草稿退回到了登录页面???登录成功草稿没了???喵喵喵???智障!!气! 很厉害,隔了30分钟,我的登录又失效了,草稿再次回滚,不客气了,* ...
- 浅析Java源码之ArrayList
面试题经常会问到LinkedList与ArrayList的区别,与其背网上的废话,不如直接撸源码! 文章源码来源于JRE1.8,java.util.ArrayList 既然是浅析,就主要针对该数据结构 ...
- 浅析Java源码之HttpServlet
纯粹是闲的,在慕课网看了几集的Servlet入门,刚写了1个小demo,就想看看源码,好在也不难 主要是介绍一下里面的主要方法,真的没什么内容啊~ 源码来源于apache-tomcat-7.0.52, ...
- 浅析Java源码之HashMap
写这篇文章还是下了一定决心的,因为这个源码看的头疼得很. 老规矩,源码来源于JRE1.8,java.util.HashMap,不讨论I/O及序列化相关内容. 该数据结构简介:使用了散列码来进行快速搜索 ...
- 浅析Java源码之HashMap外传-红黑树Treenode(已鸽)
(这篇文章暂时鸽了,有点理解不能,点进来的小伙伴可以撤了) 刚开始准备在HashMap中直接把红黑树也过了的,结果发现这个类不是一般的麻烦,所以单独开一篇. 由于红黑树之前完全没接触过,所以这篇博客相 ...
- 解密随机数生成器(二)——从java源码看线性同余算法
Random Java中的Random类生成的是伪随机数,使用的是48-bit的种子,然后调用一个linear congruential formula线性同余方程(Donald Knuth的编程艺术 ...
- 24点扑克牌游戏——(含java源码)(GUI实现)
给出四个数字,要求,在其间添加运算符和括号,使得计算结果等于24. 括号的放置即为决定哪几个数先进行计算.所以,我们先确定首先进行计算的两个相邻的数,计算完成后,就相当于剩下三个数字,仍需要在它们之间 ...
- 【java集合框架源码剖析系列】java源码剖析之HashSet
注:博主java集合框架源码剖析系列的源码全部基于JDK1.8.0版本.本博客将从源码角度带领大家学习关于HashSet的知识. 一HashSet的定义: public class HashSet&l ...
- 从Java源码到Java字节码
Java最主流的源码编译器,javac,基本上不对代码做优化,只会做少量由Java语言规范要求或推荐的优化:也不做任何混淆,包括名字混淆或控制流混淆这些都不做.这使得javac生成的代码能很好的维持与 ...
随机推荐
- cxGrid控件过滤排序和TClientDataSet同步
https://www.cnblogs.com/false/archive/2013/02/24/2924240.html procedure TReport10Form.cxGridViewData ...
- ECCV 2016 paper list
摘录ECCV2016部分文章,主要有Human pose esimation, Human activiity / actions, Face alignment, Face detection & ...
- .net framework 4.5 +steeltoe+ springcloud(三)实现Hystrix断路器
在基于.net framework的服务客户端实现断路器功能,基本项目创建步骤可以参照我的另一篇发现和调用服务的笔记,地址:http://www.cnblogs.com/troytian/p/8621 ...
- Kali Linux渗透测试实战 1.4 小试牛刀
目录 1.4 小试牛刀 1.4.1 信息搜集 whois查询 服务指纹识别 端口扫描 综合性扫描 1.4.2 发现漏洞 1.4.3 攻击与权限维持 小结 1.4 小试牛刀 本节作为第一章的最后一节,给 ...
- golang简单实现jwt验证(beego、xorm、jwt)
程序目录结构 简单实现,用户登录后返回一个jwt的token,下次请求带上token请求用户信息接口并返回信息. app.conf文件内容(可以用个beego直接读取里面的内容)写的是一个jwt的se ...
- [R]关于R语言的绘图函数
1. 首先就是plot(x,y,...) 参数: x: 所绘图形横坐标构成的对象 y: 所绘图形纵坐标构成的对象 type: 指定所绘图形类型 pch: 指定绘制点时使用的符号 cex: 指定符号的大 ...
- Python 爬虫(二十五) Cookie的处理--cookielib库的使用
Python中cookielib库(python3中为http.cookiejar)为存储和管理cookie提供客户端支持. 该模块主要功能是提供可存储cookie的对象.使用此模块捕获cookie并 ...
- MariaDB 连接查询与子查询(6)
MariaDB数据库管理系统是MySQL的一个分支,主要由开源社区在维护,采用GPL授权许可MariaDB的目的是完全兼容MySQL,包括API和命令行,MySQL由于现在闭源了,而能轻松成为MySQ ...
- 02_python_while循环/格式化输出/逻辑运算
一. while循环 1.基本形式 while 条件: 循环体 # 判断条件是否为真,如果真,执行代码块.然后再次判断条件是否为真.如果真继续执行代码块...直到条件变成了假.循环退出 ps:死循环 ...
- Android必学之AsyncTask
AsyncTask,即异步任务,是Android给我们提供的一个处理异步任务的类.通过此类,可以实现UI线程和后台线程进行通讯,后台线程执行异步任务,并把结果返回给UI线程. .为什么需要使用异步任务 ...