关注潜在的整数越界问题 | 京东物流技术团队
在平时的开发过程中,整数越界是一个容易被忽视的问题,关注潜在的整数越界问题可使我们编写的代码更加健壮,规避因整数越界导致的 bug。
比较器
以下是在 Code Review 中发现的比较器实现:
乍一看该比较器实现不存在问题,但是如果 tag1 = Integer.MIN_VALUE = -2147483648, tag2 为大于 0 的数字如 1,则此时 tag1 - tag2 = 2147483647,但是按照 java.util.Comparator#compare 的定义,tag1 小于 tag2 时,应该返回一个负数,以上写法在遇到这样的示例数据时将导致排序结果错乱,引发相关 bug。
下面看看 Spring 中比较器的实现,在 Spring 中,提供了 @Order 注解用于指定 bean 的顺序,默认值为 Ordered.LOWEST_PRECEDENCE = Integer.MAX_VALUE,即在排序时排在最后,相关源码如下:
对应的比较器实现如下:
可知其采用的 Integer.compare 方法对两个整数进行比较操作,查看 Integer#compare 方法的源码:
/** * Compares two {@code int} values numerically. * The value returned is identical to what would be returned by: * <pre> * Integer.valueOf(x).compareTo(Integer.valueOf(y)) * </pre> * * @param x the first {@code int} to compare * @param y the second {@code int} to compare * @return the value {@code 0} if {@code x == y}; * a value less than {@code 0} if {@code x < y}; and * a value greater than {@code 0} if {@code x > y} * @since 1.7 */ public static int compare(int x, int y) { return (x < y) ? -1 : ((x == y) ? 0 : 1); }
可知 java.lang.Integer#compare 并未采取 x - y 的方式进行比较,而是使用小于及等于运算符直接进行比较,规避了潜在的整数越界问题。 那么文首代码正确的实现方式应为 return Integer.compare(tag1, tag2)。如果查看 JDK 中常见数值类的源码,可知均提供了静态的 compare 方法,如:java.lang.Long#compare,java.lang.Double#compare,此处不再赘述。
切量比例
以上代码是某段业务逻辑中初始切量比例实现,取余 100 的模式常用于按比例切量、按比例降级等业务场景。以上代码使用 userPin 的哈希值取余 100 判断是否小于切量比例以决定是否执行新业务逻辑,如果我们查看 java.lang.String#hashCode 的源码实现:
/** * Returns a hash code for this string. The hash code for a * {@code String} object is computed as * <blockquote><pre> * s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] * </pre></blockquote> * using {@code int} arithmetic, where {@code s[i]} is the * <i>i</i>th character of the string, {@code n} is the length of * the string, and {@code ^} indicates exponentiation. * (The hash value of the empty string is zero.) * * @return a hash code value for this object. */ public int hashCode() { int h = hash; if (h == 0 && value.length > 0) { char val[] = value; for (int i = 0; i < value.length; i++) { h = 31 * h + val[i]; } hash = h; } return h; }
可知 java.lang.String#hashCode 本质上是对字符串进行 s[0]*31^(n-1) + s[1]*31^(n-2) + ... + s[n-1] 多项式求值,此处潜在的风险在于计算出的 hash 值可能越界,导致 userPin.hashCode() 返回值为负数,如:"jd_xxxxxxxxxxxx".hashCode() = -1406647067,且在 Java 语言中,使用负数对正数取余,是可能得到负数的。以上代码的风险在于潜在的放大了期望的切量比例,如使用以上的代码进行上线,那么当我们设定 1% 的切量比例时,会导致远超 1%的用户执行新的业务逻辑(通过采样日志发现用户 pin 集合 hashCode 值负数占比并不低),导致非预期的切量结果。
基于以上的背景,容易想到的一种修复方案为在 userPin.hashCode 外层使用 Math.abs 保证取余前的数字为正数:
以上修复方案看似不再存在问题,但是并不能保证完全正确,我们查看 Math.abs 的源码实现:
/** * Returns the absolute value of an {@code int} value. * If the argument is not negative, the argument is returned. * If the argument is negative, the negation of the argument is returned. * * <p>Note that if the argument is equal to the value of * {@link Integer#MIN_VALUE}, the most negative representable * {@code int} value, the result is that same value, which is * negative. * * @param a the argument whose absolute value is to be determined * @return the absolute value of the argument. */ public static int abs(int a) { return (a < 0) ? -a : a; }
可知在注释中特意提到,如果入参是 Integer.MIN_VALUE,即 int 域中最小的值时,返回值依然为 Integer.MIN_VALUE,因为 int 域的范围为 [-2147483648, 2147483647]。如果按照 JLS 中的解释,-x equals (~x)+1。那么可知:
x = Integer.MIN_VALUE: 10000000_00000000_00000000_00000000 ~x: 01111111_11111111_11111111_11111111 (~x) + 1: 10000000_00000000_00000000_00000000
如果在神灯上搜索 Math.abs,可以发现有三篇文章与该函数有关,均与 Math.abs(Integer.MIN_VALUE) 依然为 Integer.MIN_VALUE 有关。而我们在 Code Review 阶段发现该问题即从根本上规避了该问题,不会使存在 bug 的代码上线。最后切量比例修改后的实现如下:
总结
- java.lang.String#hashCode 在计算过程中可能因为整数越界导致返回值为负数
- Java 语言中的 % 是取余而不是取模,如:(-21) % 4 = (-21) - (-21) / 4 *4 = -1
- Math.abs(int a) 当入参是 Integer.MIN_VALUE 时返回值依然是负数 Integer.MIN_VALUE
参考
15.15.4. Unary Minus Operator -
What's the difference between “mod” and “remainder”? - Stack Overflow
Best way to make Java's modulus behave like it should with negative numbers? - Stack Overflow
OrderComparator.java · spring-projects/spring-framework
作者:京东物流 刘建设 张九龙 田爽
来源:京东云开发者社区 自猿其说Tech 转载请注明来源
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
高效开发与设计:提效Spring应用的运行效率和生产力 | 京东云技术团队
引言 现状和背景 Spring框架是广泛使用的Java开发框架之一,它提供了强大的功能和灵活性,但在大型应用中,由于Spring框架的复杂性和依赖关系,应用的启动时间和性能可能会受到影响。这可能导致开发过程中的迟缓和开发效率低下。优化Spring应用程序的启动速度和性能是一个重要的任务,通过分析和优化应用的初始化过程、减少不必要的依赖和组件加载、并利用异步初始化、懒加载等技术,可以显著改善应用的启动性能。这将帮助开发者提高开发效率、减少调试时间,并提供更好的用户体验。 线上的业务 jar 包基本上普遍比较庞大,动不动一个 jar 包几百 M,启动时间在10分钟级,拖慢了我们在故障时快速扩容的响应、以及本地开发调试效率。于是做了一些分析,看看 Spring 程序启动慢到底慢在哪里,如何去优化,目前的效果是大部分大型应用启动时间可以缩短 70%~80%。 主要有下面这些内容 SpringBean 加载耗时 timeline 可视化分析(✅) SpringBean 的可视化依赖分析(✅) 应用未加载的jar包(Jar瘦身)(✅) 应用启动过程线程wall clock火焰图(✅) 重要性和影响...
- 下一篇
海内外大咖云集!deepin社区第13届DDUC大会成功召开
11月18日,第十三届深度开发者与用户大会(DDUC)在北京成功召开。作为deepin(深度)社区一年一度最盛大的社区活动,DDUC吸引了来自全球各地的开源爱好者、行业精英与用户们齐聚一堂,共同探讨深度操作系统、AI等相关技术领域的最新发展和应用前景。 deepin(深度)社区创始人刘闻欢在致辞中回顾了建立桌面操作系统开源社区的初心,他表示,过去一年里,在社区所有用户的共同努力下,deepin社区取得了行业瞩目的成绩。社区的繁荣发展,吸引了越来越多的开发者加入,也为开源技术的发展贡献了巨大的力量。“毫无疑问,我们需要通过开源社区做创新,需要通过开源社区更好地连接上游社区、国际开源社区和全球用户。这是我们过去19年的尝试和摸索形成的认知,未来也会坚定不移做下去。” 现场,阿里巴巴高级技术专家王云龙、智谱AI CodeGeeX负责人郑勤锴、Linux中国开源社区创始人硬核老王、英特尔公司开源软件研发总监杨继国、深度测评师冯世龙以及UbuntuDDE开发者Arun Pariyar等开源技术专家、行业伙伴和开源爱好者们分享了开源理念和成果、前沿技术进展,以及与deepin携手同行的精彩故事。特...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2全家桶,快速入门学习开发网站教程
- MySQL8.0.19开启GTID主从同步CentOS8
- SpringBoot2配置默认Tomcat设置,开启更多高级功能
- SpringBoot2整合Thymeleaf,官方推荐html解决方案
- 设置Eclipse缩进为4个空格,增强代码规范
- SpringBoot2整合Redis,开启缓存,提高访问速度
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- CentOS7设置SWAP分区,小内存服务器的救世主