行百里er 行百里er
首页
  • 分类
  • 标签
  • 归档
设计模式
  • JVM
  • Java基础
MySQL
Elastic Stack
Redis
  • Kafka
  • RocketMQ
分布式
Spring Cloud Alibaba
云原生
数据结构与算法
关于
GitHub (opens new window)

行百里er

Java程序员一枚
首页
  • 分类
  • 标签
  • 归档
设计模式
  • JVM
  • Java基础
MySQL
Elastic Stack
Redis
  • Kafka
  • RocketMQ
分布式
Spring Cloud Alibaba
云原生
数据结构与算法
关于
GitHub (opens new window)
  • JVM

  • Java基础

    • 【优雅的避坑】从验证码生成代码的优化到 JVM 栈和堆
      • 验证码功能
      • 优化验证码的生成
      • JVM栈和堆
      • 小结
    • 【优雅的避坑】HashMap正确的避免扩容带来的额外开销
    • 【优雅的避坑】不要轻易使用==比较两个Integer的值
    • 【优雅的避坑】为什么0.1+0.2不等于0.3了!?
    • 【优雅的避坑】不安全!别再共享SimpleDateFormat了
    • new Object在内存中占多少字节?
    • Java 8之Lambda表达式的写法套路
    • Java 8 Stream API可以怎么玩?
    • Java最强大的技术之一:反射
    • 从一道面试题进入Java并发新机制---JUC
  • Java
  • Java基础
行百里er
2020-09-23
目录

【优雅的避坑】从验证码生成代码的优化到 JVM 栈和堆

作者:行百里er

博客:https://chendapeng.cn (opens new window)

提示

这里是 行百里er 的博客:行百里者半九十,凡事善始善终,吾将上下而求索!

# 验证码功能

实际业务中用验证码进行登录、注册等场景非常普遍,基本上现在的应用都会有这个功能,Java中已为我们提供了Math.random()以及Random类。

Math.random():

public static double random()
1

返回大于或等于0.0且小于1.0的double类型的整数。返回值的选择是伪随机的,在这个范围内(近似)均匀分布。

Random类:

public class Random
extends Object
implements Serializable

//创建一个新的随机数生成器
Random() 
//使用一个long类型的种子数创建一个新的随机数生成器
Random(long seed) 

//返回从这个随机数生成器的序列中提取的在0(含)和指定值(不含)之间均匀分布的伪随机int值。
int nextInt(int bound) 
1
2
3
4
5
6
7
8
9
10
11

假如业务上要求我们生成一个6位数字的验证码,相信大家都能搞出来,用随机数函数,加上一些手段很容易就能构造出一个验证码。

方式1:

String code = String.valueOf(new Random().nextInt(1000000));
System.out.println("random code---------" + code);
1
2
> Task :RandomCodeTest.main()
random code---------950499
1
2

想一下这种方法有什么问题没有?

思考中

乍一看好像没什么问题,但是看我们的要求,是生成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);
}
1
2
3
4
5

random结果错误示例

100个样本量就出现了多个错误,要是大型高并发项目,肯定会有验证码不是6位的情况,因此这种生成验证码的方式首先排除掉!

方式2:

我们已经知道Math.random()可以生成0 ~ 1之间的double类型的随机数,因此可以通过截取字符串的方式,获取验证码。

先来看一下Math.random()的结果:

> Task :RandomCodeTest.main()
Math.random()-------0.8806639430958753
1
2

从2 ~ 8 位置上截取 0.8806639430958753 字符串就能得到6位数字的验证码:

System.out.println("Math.random()-------" + (Math.random() + "").substring(2, 8));

//运行结果
Math.random()-------304719
1
2
3
4

样本量设置为100万,也能正确生成:

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
1
2
3
4
5
6
7
8
9
10
11
12
13

一般情况下这种方式就够了,但是在分布式、高并发场景下,这样做的效率并不是最高的。

# 优化验证码的生成

为什么说上面的方式2不是最好的呢?

图都模糊了

我们分析一下就知道,这种方式是通过先通过 + "" 变成字符串,然后截取字符串的操作完成的;而我们生成验证码只要满足6位数字就行,我要是把生成验证码的方式变成纯数字运算是不是就快一点呢?

验证一下:

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));
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

运行结果:

效率提升

count样本量1000万,可以看到,运算结果提升了好几倍!

为什么用纯数字运算优化后能提升效率?

前文说过,我是用数字运算代替字符串操作而达到优化目的的,这是因为这些数字都是在JVM栈上进行操作,而String类对象在堆里。

# JVM栈和堆

运行Java程序时,JVM自己管理着一块内存区域-运行时数据区,运行时数据区根据用途可分为:

  • JVM栈(栈区)
  • 本地方法栈
  • Java堆(堆区)
  • 方法区
  • 程序计数器

其中JVM栈,栈区或者栈内存,主要是存储Java方法执行时的局部变量-以栈帧的形式存储,包括基本数据类型、对象的引用都在栈区,方法执行结束后释放。

栈帧是每个线程锁私有的,线程执行完了,占内存就释放了。一个Java方法被调用了,就会有栈帧压入虚拟机栈,当方法执行完毕,出栈。

而堆内存,是垃圾收集器管理的主要区域,该内存区域主要存放Java的对象实例,JVM只有一个堆区,它是线程中共享的。堆中不存放基本数据类型和对象引用,只存放对象本身和数组本身。

基于以上分析,可以得出结论:处于栈区的数据操作比在堆区中的快,因为栈区的东西用完了栈空间立刻就被回收了,而堆空间则需要等待GC回收。

# 小结

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


首发公众号 行百里er ,欢迎老铁们关注阅读指正。

#Java
上次更新: 2022/10/04, 18:14:30
【GC系列】JVM的常用GC参数及GC日志解析
【优雅的避坑】HashMap正确的避免扩容带来的额外开销

← 【GC系列】JVM的常用GC参数及GC日志解析 【优雅的避坑】HashMap正确的避免扩容带来的额外开销→

最近更新
01
重要数据不能丢!MySQL数据库定期备份保驾护航!
05-22
02
分布式事务解决方案之 Seata(二):Seata AT 模式
09-09
03
Seata 番外篇:使用 docker-compose 部署 Seata Server(TC)及 K8S 部署 Seata 高可用
09-05
更多文章>
Theme by Vdoing | Copyright © 2020-2023 行百里er | MIT License | 豫ICP备2022020385号-1
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式