1.摘要

前一阵遇到了一个使用Collections.sort()时报异常的问题,跟小伙伴@zhuidawugui 一起排查了一下,发现问题的原因是JDK7的排序实现改为了TimSort,之后我们又进一步研究了一下这个神奇的算法。

2.背景

先说一下为什么要研究这个异常,前几天线上服务器发现日志里有偶发的异常:

 
1
2
3
4
5
6
7
8
9
java.lang.IllegalArgumentException: Comparison method violates its general contract!
at java.util.TimSort.mergeHi(TimSort.java:868)
  at java.util.TimSort.mergeAt(TimSort.java:485)
  at java.util.TimSort.mergeCollapse(TimSort.java:408)
at java.util.TimSort.sort(TimSort.java:214)
  at java.util.TimSort.sort(TimSort.java:173)
  at java.util.Arrays.sort(Arrays.java:659)
  at java.util.Collections.sort(Collections.java:217)
...

出错部分的代码如下:

 
1
2
3
4
5
6
7
List<Integer> list = getUserIds();
Collections.sort(list, new Comparator<Integer>() {
    @Override
    public int compare(Integer o1, Integer o2) {
        return o1>o2?1:-1;
    }
});

google了一下:JDK7中的Collections.Sort方法实现中,如果两个值是相等的,那么compare方法需要返回0,否则可能会在排序时抛错,而JDK6是没有这个限制的。

这个问题在测试时并没有出现,线上也只是小概率复现,如何稳定的复现这个问题?看了一下源代码,抛出异常的那段源代码让人根本摸不着头脑:

 
1
2
3
if (len2 == 0) {
    throw new IllegalArgumentException("Comparison method violates its general contract!");
}

为了解开这个困惑,我们对java实现的Timsort代码做了一些分析。

3.Timsort概述

TimSort排序是一种优化的归并排序,它将归并排序(merge sort) 与插入排序(insertion sort) 结合,并进行了一些优化。对于已经部分排序的数组,时间复杂度远低于 O(n log(n)),最好可达 O(n),对于随机排序的数组,时间复杂度为 O(nlog(n)),平均时间复杂度 O(nlog(n))。

它的整体思路是这样的:

  1. 遍历数组,将数组分为若干个升序或降序的片段,(如果是降序片段,反转降序的片段使其变为升序),每个片段称为一个Runtask
  2. 从数组中取一个RunTask,将这个RunTask压栈。
  3. 取出栈中相邻两个的RunTask,做归并排序,并将结果重新压栈。
  4. 重复(2),(3)过程,直到所有数据处理完毕。

这篇文章就不再过多的阐述Timsort整体思路了,有兴趣可以参考[译]理解timsort, 第一部分:适应性归并排序(Adaptive Mergesort)

4.Timsort的归并

重点说一下Timsort中的归并。归并过程相对普通的归并排序做了一定的优化,假如有如下的一段数组:

  1. 首先把数组拆成两个RunTask,这里称为A段和B段,注意,A段和B段在物理地址上是连续的:

  2. A段的起点为base1,剩余元素数量为len1;B段起点为base2,剩余元素数量为len2。取B点的起点值B[base2],在A段中进行二分查找,将A段中小于等于B[base2]的段作为merge结果的起始部分;再取A段的终点值a[base1 + len1 – 1],在B段中二分查找,将B段中大于等于a[base1 + len1 – 1]值的段作为结果的结束部分。

    更形象的说,这里把待归并的数据“掐头去尾”,只需要合并中间的数据就可以了:

  3. 之后需要创建一个tmp数组,大小为B段截取后的大小,并把B段剩余的数据拷贝过去,因为合并过程中这些数据会被覆盖掉。

    程序会记录corsor1和corsor2,这是待归并数据的指针,初始位置在A段和tmp段的末尾。同时会记录合并后数组的dest指针,位置在原B段的末尾。

    这里还有一个小优化:生成dest指针时会直接把A段cursor1指向的数据拷贝到B段末尾,同时cursor–,dest–。因为之前(2)步的时候已经保证了arr[cursor1]>arr[dest]

  4. 进行归并排序,这里每次归并比较时会记录A和tmp段比较“胜利(大于对方)”的次数,比较失败(小于对方)时会把胜利数清零。当有一个段的数据连续N次胜利时会激活另一个优化策略,在这里假设N为4,下图已经是A段连续胜利了4次的情况:

  5. 如果连续胜利N次,那么可以假设A段的数据平均大于B段,此时会用tmp[cursor2]的值在A[base0]至A[cursor1]中查找第一个小于tmp[cursor2]的索引k,并把A[k+1]到A[cursor1]的数据直接搬移到A[dest-len,dest]。

    对于例子中的数据,tmp[cursor2]=8,在A数组中查找到小于8的第一个索引(-1),之后把A[0,1]填充到A[dest-1,dest],cursor1和dest指针左移两个位置。

  6. 如果cursor1>=0,之后会再用curosr1指向的数据在tmp数组中查找,由于这里cursor1已经是-1了,循环结束。

  7. 最后把tmp里剩余的数据拷贝到A数组的剩余位置中,结束。

5.异常情况下Timsort的归并

假设这里实现的compare(obj o1,obj o2)如下:

 
1
2
3
public int compare(Integer o1, Integer o2) {
    return o1>o2?1:-1;
}
  1. 仍然是分成A,B两段:

  2. 在“掐头去尾”的时候,这时会有一些变化,程序执行到compare(B[base2],A[base1])时返回-1,A的左侧留下了两个应该被切走的“5”。

  3. 接下来是正常的归并过程。

  4. 这里同样会触发“胜利”>N次逻辑

  5. 在A[base1,cursor1]中查找小于tmp[cursor2]的元素,复制,cursor1和dest左移两位。

  6. 此时再用A[cursor1]在tmp中查找,tmp中所有的数据都被移入A数组,cursor2、dest左移4位。tmp2剩余元素的数量(len2)为0。

注意!

在第6步查找的时候,有A[base1+1]<tmp[0](tmp[0]的值等于没有合并之前的B[base2])。
而第2步时,有B[base2]<A[base1]
而最初生成RunTask的时候,有A[base1]<=A[base1+1]
连起来就是B[base2]<A[base1]<=A[base1+1]<B[base2],这显然是有问题的。

所以,当len2==0时,会抛出“Comparison method violates its general contract”异常。问题复现的条件是触发“胜利N次”的优化,并且存在类似(A[base1]==A[base1+x])&&(A[base1+x]==B[base2])的数据排列。这里应该还有几种另外的触发条件,精力有限,就不再深究了。

6.参考

TimSort in Java 7 OpenJDK 源代码阅读之 TimSort

解决方法:

Collections.sort(list1, new Comparator<Combo>(){
//重写排序规则
@Override
public int compare(Combo o1, Combo o2) {
if(o2.getCreateTime()!=null&&o1.getCreateTime()!=null){
if(o2.getCreateTime().getTime()>o1.getCreateTime().getTime()){
return 1;
}else if(o2.getCreateTime().getTime()<o1.getCreateTime().getTime()){
return -1;
}else{
return 0;
}
}
return 0;
};

JDK7的Comparison method violates its general contract异常的更多相关文章

  1. 排序遇到问题 JDK7的Comparison method violates its general contract

    图解JDK7的Comparison method violates its general contract异常 楼主分析的很详细,能力有限,我看得迷迷糊糊的,不过大致知道这个错误的起因了.学习了,谢 ...

  2. 关于jdk7中 使用Collections的排序方法时报Comparison method violates its general contract!异常

    参考: Comparison method violates its general contract Comparison method violates its general contract! ...

  3. Comparison method violates its general contract! 异常原因

    项目运行期间出现Comparison method violates its general contract!异常,网上查阅了一下,原因还是比较明确的: Collections.sort(list, ...

  4. java-collections.sort异常Comparison method violates its general contract!

    转载:http://www.tuicool.com/articles/MZreyuv 异常信息 java.lang.IllegalArgumentException: Comparison metho ...

  5. Comparison method violates its general contract

    生产环境出现的错误排查,错误log如下 java.lang.IllegalArgumentException: Comparison method violates its general contr ...

  6. Comparison method violates its general contract 解决

    java.lang.IllegalArgumentException: Comparison method violates its general contract! 原因 JDK7中的Collec ...

  7. 解决 Comparison method violates its general contract!

    问题:Comparison method violates its general contract!报错 Collections.sort(list, new Comparator<Integ ...

  8. 解决“Comparison method violates its general contract!”

    The ONE跑MaxProp.Prophet可能(取决于你JDK的版本)会报“java.lang.IllegalArgumentException: Comparison method violat ...

  9. [ Error 分析] Comparison method violates its general contract!

    public static void main(String[] args) { List<Long> ret = new ArrayList<>(); int n = 103 ...

随机推荐

  1. 【腾讯GAD暑期训练营游戏程序开发】游戏中的动画系统作业

    游戏中的动画系统作业说明文档   一.实现一个动画状态机:至少包含3组大的状态节点

  2. 似魔鬼的 『 document.write 』

    在平时的工作中,楼主很少用 document.write 方法,一直觉得 document.write 是个危险的方法.楼主不用,并不代表别人不用,最近给维护的项目添了一点代码,更加深了我对 &quo ...

  3. JavaScript中sort方法的一个坑(leetcode 179. Largest Number)

    在做 Largest Number 这道题之前,我对 sort 方法的用法是非常自信的.我很清楚不传比较因子的排序会根据元素字典序(字符串的UNICODE码位点)来排,如果要根据大小排序,需要传入一个 ...

  4. 虚拟机开机提示Operating System not found解决办法

    为了更好体验windows更多操作系统,有些用户会在VMware虚拟机中安装XP.win7或win8等等系统,有用户反映在虚拟机中安装XP开机后提示"Operating System not ...

  5. IIS7.5开启GZip压缩

    在IIS7.5选择要开启GZip压缩的网站,在功能视图中找到并双击"压缩"图标,在压缩界面中钩选"启用静态内容压缩"和"启用动态内容压缩", ...

  6. 个人知识管理系统Version1.0开发记录(01)

    架 构 描 述 01.数据层,数据源,有形资源入库,无形资源整理,对外搜索引擎,对内平台搜索,数据类型(文字.图片.声音.视频.组合),数据时空优化,数据安全方案(数据进站关卡,数据出战关卡),数据并 ...

  7. BroadcastReceive之ip拨号

    首先,新建一个类,继承于BroadcastReceive,然后去配置Manifest.xml <receiver android:name=".PhoneOnReceice" ...

  8. ios8调用相机报警告: Snapshotting a view that has not been rendered results in an empty snapshot. Ensure you(转)

    我这也报了这个警告,但按他的方法并没有起作用,把写到这个地方看是否其他人用的到 错误代码:Snapshotting a view that has not been rendered results ...

  9. eclipse中的常用插件

    常用插件整理: http://blog.sina.com.cn/s/blog_60b5e13e0102vz6q.html    插件带有链接 http://blog.csdn.net/kagoy/ar ...

  10. Nginx之负载均衡服务器揭秘

    Nginx代理服务器, 一次性代理多台后端机器, 利用负载算法, 决定将当前请求传递给某台服务器执行. 有哪些后台服务器?例如微软的IIS,Apache,Nginx 负载算法是什么? 加权轮询. ng ...