优雅的避坑-从验证码功能代码优化到JVM栈和堆

2020-12-02 15:10:12 浏览数 (1)

验证码功能

实际业务中用验证码进行登录、注册等场景非常普遍,基本上现在的应用都会有这个功能,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位,样本量设置大一点验证一下:

代码语言:javascript复制
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回收。

小结

  • 能用纯数据运算解决问题的尽量不要用字符串
  • 因为基本数据类型存在于栈区,字符串常量池存在于堆区
  • 栈的存取速度比堆快
  • 平常工作中注意细节,你的一次优化有可能带来程序上成倍效率的提升

0 人点赞