JDK不同版本的Collections.Sort方法实现

一句话总结:

JDK7中的Collections.Sort方法实现中,应用了比较运算的基本属性:若A大于B,则B小于A,若A等于B,则B等于A。所以要求传入compare方法在传入参数交换时,返回值正负也需要交换,或恒为0,否则可能会在排序时抛错。

 

现象:

昨晚偶然发现XX业务线上接口调用返回服务器内部异常。而调用仿真环境接口返回正常。查看日志发现报错如下:

2015-01-14 22:14:17 291 [WARN] ApiServletApiServlet process error! act:query_join_groups, PARAMS:{}

java.lang.IllegalArgumentException:Comparison method violates its general contract!

 at java.util.TimSort.mergeHi(TimSort.java:868)

  atjava.util.TimSort.mergeAt(TimSort.java:485)

  atjava.util.TimSort.mergeCollapse(TimSort.java:408)

at java.util.TimSort.sort(TimSort.java:214)

  atjava.util.TimSort.sort(TimSort.java:173)

  at java.util.Arrays.sort(Arrays.java:659)

  atjava.util.Collections.sort(Collections.java:217)

atcn.sina.groupchat.processor.QueryJoinGroupsProcessor.process(QueryJoinGroupsProcessor.java:54)

 

排查过程:

查看出错的业务代码如下

      Collections.sort(infos, new Comparator<UserGroupInfo>() {

           @Override

           public int compare(UserGroupInfo info1, UserGroupInfo info2) {

               return info2.joinTime > info1.joinTime ? 1 : -1;

           }

       });

查看了具体报错的位置,发现只有如下代码:

if (len2== 0) {

     throw new IllegalArgumentException("Comparison method violates its generalcontract!");

    }

 

Google了一下出错,发现JDK6和JDK7的sort实现不同,对于JDK7才会有上述问题。解决的办法是compare方法在传入对象相等时必须返回0,但是没有详细描述出错的原因。

 

查了一下JDK版本1.6到1.7的改动,Collections.Sort方法实现从普通归并排序改成了TimSort排序。不太理解为什么更换排序会导致相同输入报错,并且报错的地方判断逻辑很突兀。于是简单的了解的了一下java timsort的实现,和大家分享一下。

 

TimSort排序是一种优化的归并排序,对于降序和升降序片段混合的输入有很大的性能提升。OpenJDK关于TimSort的实现如下:

1.      遍历数组,将数组分为若干个升序或降序的片段,反转降序的片段使其变为升序,每个片段成为一个Runtask

2.      将切分好的RunTask压栈

3.      对栈中相邻的RunTask做归并,归并过程相对普通的归并排序做了一定的优化,主要有两步

a)        设做归并的两段分别为A,B,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]值的段作为merge结果的结束部分。

b)        之后进行普通归并,将两段终点标为cursor1和cursor2,倒序归并

 

这里有一个优化,如果连续N(图中假设为4)次某段的cursor指向的值都大于另一段,则可以预期该段的平均值大于另一段,仿照(a)中的方法,用cursor的值分别对另一段进行切割,提高归并速度。如下图中所示,cursor1指向的值(10,10,11,11)已经连续4次大于cursor2指向的值(8),触发切割逻辑,用cursor2指向的值去切割1段,使cursor1左移,然后用cursor1切割2段,由于cursor1指向值(10)大于2段中所有的值,没有进行实际切割。

最终,将2段的值arraycopy到1段0~3的位置,归并结束。

 

 

代码如下:

// 普通归并过程,count记录连续大于的次数,到达一个阈值时,进行二分法切割,提高归并速度

do {

                         // tmp为B段的复制

               if (c.compare(tmp[cursor2], a[cursor1]) < 0) {

                   a[dest--] = a[cursor1--];

                   count1++;

                   count2 = 0;

                   if (--len1 == 0)

                       break outer;

               } else {

                                              …

               }

            }while ((count1 | count2) < minGallop);

 

                           // 到达阈值后,用预期平均值较小的段的最大值去切割另一段,方法和(a)中类似

                  do {

               count1 = len1 - gallopRight(tmp[cursor2], a, base1, len1, len1 - 1, c);

               if (count1 != 0) {

                   dest -= count1;

                   cursor1 -= count1;

                   len1 -= count1;

                   System.arraycopy(a, cursor1 + 1, a, dest + 1, count1);

                  if (len1 == 0)

                       break outer;

               }

               a[dest--] = tmp[cursor2--];

               if (--len2 == 1)

                   break outer;

                                    

                                    // 出问题的地方,gallopLeft是在B段中查找A[Cursor1]的位置,如有相等的情况,取最左的位置,如果B段全部大于A[Cursor1],则返回0

               count2 = len2 - gallopLeft(a[cursor1], tmp, 0, len2, len2 - 1, c);

               if (count2 != 0) {

                                              …

                  len2-= count2;

                   …

                  }

                               …

            }while (count1 >= MIN_GALLOP | count2 >= MIN_GALLOP);

                           …

                           // 最终对len2进行合法检测

                  if (len2 == 0) {

                      throw newIllegalArgumentException("Comparison method violates its generalcontract!");

            }

 

 

关于代码中最后的len2值检测,是因为(a)中切割后,A中所有的值都大于B段的起点B[base2]。在之后的普通归并中,如果出现count2>=minGallop的情况,进行加速归并优化时,按照之前的推论,gallopLeft返回值大于等于1(cursor1必然大于B[base2]),从而推出len2 > 0。

 

当传入的比较方法返回有问题时,会破坏以上推论,以出现问题的代码为例,当传入两个相等值时,返回-1(交换参数后还是返回-1,违背了之前的要求)。过程如下所示:

1.      归并前的A段和B段

 

2.      用B段的起点和A段的终点互相切割之后,由于compare方法的问题,A段中的1和2位置的5被保留,破坏了A段中所有值都大于B[base2]的条件

 

3.      之后是普通的归并过程

----à

 

4.      之后由于连续的A段大于B段,触发了切割,B段的cursor2将A段切割到cursor1的位置;当用cursor1对B段进行切割时,由于compare方法的问题,gallopLeft会返回0,从而导致len2值等于0,引起报错。

 

 

后续

1.      测试环境的JDK版本需要和线上环境保持一致

2.      排查代码,修复此类问题

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值