网站首页 > 编程文章 正文
最近一源发现一个运行正常的订单服务系统后台时不时出现一个奇怪的异常日志,日志如下:
java.lang.IllegalArgumentException: Comparison method violates its general contract!
at java.util.ComparableTimSort.mergeHi(ComparableTimSort.java:866)
at java.util.ComparableTimSort.mergeAt(ComparableTimSort.java:483)
at java.util.ComparableTimSort.mergeForceCollapse(ComparableTimSort.java:422)
at java.util.ComparableTimSort.sort(ComparableTimSort.java:222)
at java.util.Arrays.sort(Arrays.java:1312)
at java.util.Arrays.sort(Arrays.java:1506)
at java.util.ArrayList.sort(ArrayList.java:1462)
at java.util.Collections.sort(Collections.java:141)
一看到这种日志,就发现哪个开发的人又没有按规范使用JDK-API的常用方法,导致线上出现的bug问题,于是开始排查,最后发现是TimSort.sort 和 ComparableTimSort.sort 对List进行排序导致的异常,源代码如下:
线下问题复现
通过代码大致位置已经定位到了,出现问题的问题所在,于是写个demo复现一下,demo如下:
定义排序订单实体类 SortOrderBean.java
// 必须实现Comparable接口并重写compareTo方法
public class SortOrderBean implements Comparable<SortOrderBean> {
private Integer sort;
public Integer getSort() {
return sort;
}
public void setSort(Integer sort) {
this.sort = sort;
}
public int compareTo(SortOrderBean o) {
if (this.getSort() == null || o.getSort() == null) {
return -1;
} else {
return o.getSort().compareTo(this.getSort());
}
}
@Override
public String toString() {
return "SortOrderBean{" +
"sort=" + sort +
'}';
}
}
定义运行类SortMain.java
import java.util.*;
public class SortMain {
public static void main(String[] args) {
// 定义一个处理线程每500毫秒执行一次排序
Runnable runnable = new Runnable() {
public void run() {
while (true) {
try {
sortFun();
Thread.sleep(500);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
};
runnable.run();
}
public static void sortFun() {
List<SortOrderBean> beans = new ArrayList<>();
Random random = new Random();
for (int i = 0; i < 27; i++) {
SortOrderBean bean = new SortOrderBean();
bean.setSort(100 + random.nextInt(3));
beans.add(bean);
}
for (int i = 0; i < 20; i++) {
SortOrderBean bean = new SortOrderBean();
beans.add(bean);
}
String afterSort = "";
for (SortOrderBean bean : beans) {
afterSort += (bean.getSort() + ",");
}
System.out.println("=排序前:afterSort=>" + afterSort);
Collections.sort(beans);
String beforSort = "";
for (SortOrderBean bean : beans) {
beforSort += (bean.getSort() + ",");
}
System.out.println("=排序后:beforSort=>" + beforSort);
}
}
运行查看复现效果
了解API
根据开篇给的异常定位,从Collections.sort 开始,我看查看对应API描述如下:在线API[1]
对于上图描述的API只是说明了实现排序时必须元素实现Comparable接口,还是没有得到我们的异常错误的答案,那么我们继续往下看,进入Comparable接口的API,如下:
API说明, 1、 该重写的比较机制必须保证对于所有的x,y而言sgn(compare(x,y))==-sgn(compare(y,x)),也就是说当compare(x,y)异常时,compare(y,x)也必须抛出异常; 2、 该重写的比较机制必须保证对于((compare(x, y)>0) && (compare(y, z)>0))意味着compare(x, z)>0(可传递的); 3、 重写的比较机制必须保证对于compare(x, y)==0意味着sgn(compare(x, z))==sgn(compare(y, z))成立。
定位问题根源
返回问题的根源,SortOrderBean.java 和 SortMain.java
// SortOrderBean
public class SortOrderBean implements Comparable<SortOrderBean> {
private Integer sort;
public Integer getSort() {
return sort;
}
public void setSort(Integer sort) {
this.sort = sort;
}
// 当比较的两个元素中存在其中一个仅仅为null时
public int compareTo(SortOrderBean o) {
// 问题
if (this.getSort() == null || o.getSort() == null) {
return -1;
} else {
return o.getSort().compareTo(this.getSort());
}
}
}
// SortMain.java
public static void sortFun() {
List<SortOrderBean> beans = new ArrayList<>();
Random random = new Random();
for (int i = 0; i < 30; i++) {
SortOrderBean bean = new SortOrderBean();
bean.setSort(100 + random.nextInt(3));
beans.add(bean);
}
for (int i = 0; i < 20; i++) {
SortOrderBean bean = new SortOrderBean();
beans.add(bean);
}
// 定义了30个随机生成的sort有值bean,20个null的bean
String afterSort = "";
for (SortOrderBean bean : beans) {
afterSort += (bean.getSort() + ",");
}
System.out.println("=排序前:afterSort=>" + afterSort);
Collections.sort(beans);
String beforSort = "";
for (SortOrderBean bean : beans) {
beforSort += (bean.getSort() + ",");
}
System.out.println("=排序后:beforSort=>" + beforSort);
}
根据API规则得到,如果sgn(compare(101,null))=-1时,会重复的元素比较sgn(compare(null,101))=-1 此时违反了上述的第一条规则sgn(compare(x,y))==-sgn(compare(y,x))。由此我们知道,问题就出现在这里,那么需要怎么修改呢?其实这里只需要将getSort()的默认值写成非null即可(其中一种方法)也有其他方法,只要满足上述规则即可,修改代码如下:
测试修改后的运行结果如下:
总结回顾
通过需要排序的实体类实现Comparable 接口并重写compareTo接口,由于不规范的写法导致线上数据量多后出现的问题,对线上问题排查定位,需要查阅java.util和java.lang包下的API,清楚对应API规则变化所带来写法的影响,并满足对应API的规则,实现我们想要的排序。对于这个问题的定位,需要通过模拟足够比例的数据才会出现,在线上如果出现null情况下的比较字段,不一定会出现上述异常,但是当满足了上述规则条件时就会发生异常,这就是问题排查的难点所在,该排序算法在JDK6之前是没有的,但在后面的版本都优化过,通过TimSort以对“并归排序”进行优化的原理来实现的排序,具体实现可以查看源码java.util.ComparableTimSort 和 java.util.TimSort的具体实现。所以这个坑早发现早治疗,在使用Java API自带的工具类时一定要理解使用规则以及用法,不然后果很严重。见JDK1.8文档
引用链接
[1] 在线API: https://tool.oschina.net/apidocs/apidoc?api=jdk-zh
猜你喜欢
- 2024-09-09 Java JDK11 在Linux上的安装和配置
- 2024-09-09 一份详细介绍JVM的资料(对比JDK8和JDK7)
- 2024-09-09 应用服务器安装指南(应用服务器安装指南下载)
- 2024-09-09 [信创]SpringBoot3 JDK17 整合 MyBatis + 达梦DM8(一)
- 2024-09-09 浅谈 Java线程状态转换及控制(java线程状态转换图)
- 2024-09-09 jdk 1.8 stream基本用法(jdk8 stream map)
- 2024-09-09 jdk安装、配置文档(jdk安装配置教程)
- 2024-09-09 2021年官网下载各个版本JDK最全版与官网查阅方法
- 2024-09-09 11.2.JDK5~JDK8各个版本新特性(jdk8u5)
- 2024-09-09 jdk1.8就带有的Lambda表达式,现在1.9都发布了你不会还没用过吧
你 发表评论:
欢迎- 06-24一个老爸画了超级有爱的365幅画 | 父亲节献礼
- 06-24产品小白看魏则西事件——用产品思维审视百度推广
- 06-24某教程学习笔记(一):13、脚本木马原理
- 06-24十大常见web漏洞——命令执行漏洞
- 06-24初涉内网,提权那些事(内网渗透提权)
- 06-24黑客命令第16集:47种最常见的**网站方法2/2
- 06-24铭说 | 一句话木马的多种变形方式
- 06-24Java隐藏的10倍效率技巧!90%程序员不知道的魔法方法(附代码)
- 最近发表
- 标签列表
-
- spire.doc (70)
- instanceclient (62)
- solidworks (78)
- system.data.oracleclient (61)
- 按键小精灵源码提取 (66)
- pyqt5designer教程 (65)
- 联想刷bios工具 (66)
- c#源码 (64)
- graphics.h头文件 (62)
- mysqldump下载 (66)
- libmp3lame (60)
- maven3.3.9 (63)
- 二调符号库 (57)
- git.exe下载 (68)
- diskgenius_winpe (72)
- pythoncrc16 (57)
- solidworks宏文件下载 (59)
- qt帮助文档中文版 (73)
- satacontroller (66)
- hgcad (64)
- bootimg.exe (69)
- android-gif-drawable (62)
- axure9元件库免费下载 (57)
- libmysqlclient.so.18 (58)
- springbootdemo (64)
本文暂时没有评论,来添加一个吧(●'◡'●)