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. 代码滑动panorama-即程序中设置SelectedIndex

    我们都知道panorama的SelectedIndex属性是只读的,所以通过修改它,在程序滑动panorama似乎不可能.那么是不是就没有办法了呢?其实我们可以通过设置SelectedItemProp ...

  2. 【福吧资源网整理】老男孩-python运维6期 不加密

    老男孩-python运维6期 不加密,连夜整理出来分享给大家老男孩的python教程确实不错. 教程目录: 下载地址:http://www.fu83.cn/thread-204-1-1.html  

  3. Spring 依赖注入方式详解

    平常的Java开发中,程序员在某个类中需要依赖其它类的方法. 通常是new一个依赖类再调用类实例的方法,这种开发存在的问题是new的类实例不好统一管理. Spring提出了依赖注入的思想,即依赖类不由 ...

  4. .NET平台下的微信SDK(Rabbit.WeiXin)开源发布

    在上一篇文章<RabbitHub开源情况及计划>上有提及到了一个新的开源项目——微信SDK,经过几天的努力现在开源发布Beta1版本. 目录 前言 特点 功能 支持的消息类型 请求消息 事 ...

  5. Redis的五种数据结构

    Redis支持持久化只是它的一件武器,它提供了多达5种数据存储方式: 一  string(字符串) string是最简单的类型,你可以理解成与Memcached一模一样的类型,一个key对应一个val ...

  6. linux 权限

    ls -la 查看文件 drwxr-xr-x 2 root root 4096 ...... 第一个d:代表目录:-代表文件 后面三个一组:r:读:w:写:x:执行 第一个root 代表所属用户: 第 ...

  7. 消息队列写入内容后,读出来的自动包裹了<string>标签,自定义格式化器解决该issue

    /// <summary> /// 该格式化器使输入即输出 /// </summary> public class StringFormatter : IMessageForm ...

  8. 转 为什么文件存储要选用B+树这样的数据结构?

    为什么文件存储要选用B+树这样的数据结构? "文件存储要选用B+树这样的数据结构"--没记错的话,这是严蔚敏那本数据结构书上的一句结论.不知道是我没细看还是她没细讲,反正当时纯粹应 ...

  9. [转]session 持久化问题(重启服务器session 仍然存在)

    转:http://xiaolongfeixiang.iteye.com/blog/560800 关于在线人数统计,大都使用SessionListener监听器实现. SessionListener 触 ...

  10. 【UOJ #20】【NOIP 2014】解方程

    http://uoj.ac/problem/20 并不会做...然后看题解....... 对a取模,避免了高精度带来的复杂度,然后再枚举x判断是否满足模意义下等于0. 取5个模数,我直接抄的别人的_( ...