程序员开发实例大全宝库

网站首页 > 编程文章 正文

这个坑我踩过,告诉你:别踩(我踩过的坑)

zazugpt 2024-09-09 10:31:14 编程文章 21 ℃ 0 评论


最近一源发现一个运行正常的订单服务系统后台时不时出现一个奇怪的异常日志,日志如下:

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.sortComparableTimSort.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.utiljava.lang包下的API,清楚对应API规则变化所带来写法的影响,并满足对应API的规则,实现我们想要的排序。对于这个问题的定位,需要通过模拟足够比例的数据才会出现,在线上如果出现null情况下的比较字段,不一定会出现上述异常,但是当满足了上述规则条件时就会发生异常,这就是问题排查的难点所在,该排序算法在JDK6之前是没有的,但在后面的版本都优化过,通过TimSort以对“并归排序”进行优化的原理来实现的排序,具体实现可以查看源码java.util.ComparableTimSortjava.util.TimSort的具体实现。所以这个坑早发现早治疗,在使用Java API自带的工具类时一定要理解使用规则以及用法,不然后果很严重。见JDK1.8文档

引用链接

[1] 在线API: https://tool.oschina.net/apidocs/apidoc?api=jdk-zh

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表