验证码功能
实际业务中用验证码进行登录、注册等场景非常普遍,基本上现在的应用都会有这个功能,Java中已为我们提供了Math.random()以及Random类。
Math.random():
代码语言:javascript复制public static double random()
返回大于或等于0.0且小于1.0的double类型的整数。返回值的选择是伪随机的,在这个范围内(近似)均匀分布。
Random类:
代码语言:javascript复制public class Random
extends Object
implements Serializable
//创建一个新的随机数生成器
Random()
//使用一个long类型的种子数创建一个新的随机数生成器
Random(long seed)
//返回从这个随机数生成器的序列中提取的在0(含)和指定值(不含)之间均匀分布的伪随机int值。
int nextInt(int bound)
假如业务上要求我们生成一个6位数字的验证码,相信大家都能搞出来,用随机数函数,加上一些手段很容易就能构造出一个验证码。
方式1:
代码语言:javascript复制String code = String.valueOf(new Random().nextInt(1000000));
System.out.println("random code---------" code);
代码语言:javascript复制> Task :RandomCodeTest.main()
random code---------950499
想一下这种方法有什么问题没有?
乍一看好像没什么问题,但是看我们的要求,是生成6位验证码,而new Random().nextInt(1000000)
返回的是0 <= code < 1000000的随机数,也就是说有可能生成的数不够6位,样本量设置大一点验证一下:
int count = 100;
for (int i = 0; i < count; i ) {
String code = String.valueOf(new Random().nextInt(1000000));
System.out.println("random code---------" code);
}
random结果错误示例
100个样本量就出现了多个错误,要是大型高并发项目,肯定会有验证码不是6位的情况,因此这种生成验证码的方式首先排除掉!
方式2:
我们已经知道Math.random()可以生成0 ~ 1之间的double类型的随机数,因此可以通过截取字符串的方式,获取验证码。
先来看一下Math.random()的结果:
代码语言:javascript复制> Task :RandomCodeTest.main()
Math.random()-------0.8806639430958753
从2 ~ 8 位置上截取 0.8806639430958753 字符串就能得到6位数字的验证码:
代码语言:javascript复制System.out.println("Math.random()-------" (Math.random() "").substring(2, 8));
//运行结果
Math.random()-------304719
样本量设置为100万,也能正确生成:
代码语言:javascript复制int count = 1000000;
int wrongNum = 0;
for (int i = 0; i < count; i ) {
String code = (Math.random() "").substring(2, 8);
if (code.length() < 6) {
wrongNum ;
}
}
System.out.println("wrongNum-----" wrongNum);
//运行结果
> Task :RandomCodeTest.main()
wrongNum-----0
一般情况下这种方式就够了,但是在分布式、高并发场景下,这样做的效率并不是最高的。
优化验证码的生成
为什么说上面的方式2不是最好的呢?
图都模糊了
我们分析一下就知道,这种方式是通过先通过 "" 变成字符串,然后截取字符串的操作完成的;而我们生成验证码只要满足6位数字就行,我要是把生成验证码的方式变成纯数字运算是不是就快一点呢?
验证一下:
代码语言:javascript复制int count = 10000000;
long start = System.currentTimeMillis();
//int wrongNum = 0;
for (int i = 0; i < count; i ) {
String code = (Math.random() "").substring(2, 8);
}
long end = System.currentTimeMillis();
System.out.println("depends:" (end - start));
//System.out.println("wrongNum-----" wrongNum);
start = System.currentTimeMillis();
for (int i = 0; i < count; i ) {
String code = String.valueOf((int) (Math.random() * 9 1) * Math.pow(10, 5));
}
end = System.currentTimeMillis();
System.out.println("depends:" (end - start));
运行结果:
效率提升明显
count样本量1000万,可以看到,运算结果提升了好几倍!
为什么用纯数字运算优化后能提升效率?
前文说过,我是用数字运算代替字符串操作而达到优化目的的,这是因为这些数字都是在JVM栈上进行操作,而String类对象在堆里。
JVM栈和堆
运行Java程序时,JVM自己管理着一块内存区域-运行时数据区,运行时数据区根据用途可分为:
- JVM栈(栈区)
- 本地方法栈
- Java堆(堆区)
- 方法区
- 程序计数器
其中JVM栈,栈区或者栈内存,主要是存储Java方法执行时的局部变量-以栈帧的形式存储,包括基本数据类型、对象的引用都在栈区,方法执行结束后释放。
栈帧是每个线程锁私有的,线程执行完了,占内存就释放了。一个Java方法被调用了,就会有栈帧压入虚拟机栈,当方法执行完毕,出栈。
而堆内存,是垃圾收集器管理的主要区域,该内存区域主要存放Java的对象实例,JVM只有一个堆区,它是线程中共享的。堆中不存放基本数据类型和对象引用,只存放对象本身和数组本身。
基于以上分析,可以得出结论:处于栈区的数据操作比在堆区中的快,因为栈区的东西用完了栈空间立刻就被回收了,而堆空间则需要等待GC回收。
小结
- 能用纯数据运算解决问题的尽量不要用字符串
- 因为基本数据类型存在于栈区,字符串常量池存在于堆区
- 栈的存取速度比堆快
- 平常工作中注意细节,你的一次优化有可能带来程序上成倍效率的提升