圖解JDK7的Comparison method violates its general contract異常

Axb的自我修養html

首頁java

圖解JDK7的Comparison method violates its general contract異常

目錄算法

[顯示]數組

1.摘要

前一陣遇到了一個使用Collections.sort()時報異常的問題,發現問題的緣由是JDK7的排序實現改成了TimSort,以後咱們又進一步研究了一下這個神奇的算法。服務器

2.背景

先說一下爲何要研究這個異常,前幾天線上服務器發現日誌裏有偶發的異常:cors

 

 

1ide

2測試

3優化

4google

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中的歸併。歸併過程相對普通的歸併排序作了必定的優化,假若有以下的一段數組:

normal1

  1. 首先把數組拆成兩個RunTask,這裏稱爲A段和B段,注意,A段和B段在物理地址上是連續的:
    normal1

  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]值的段做爲結果的結束部分。

    更形象的說,這裏把待歸併的數據「掐頭去尾」,只須要合併中間的數據就能夠了:
    normal1

  3. 以後須要建立一個tmp數組,大小爲B段截取後的大小,並把B段剩餘的數據拷貝過去,由於合併過程當中這些數據會被覆蓋掉。

    程序會記錄corsor1和corsor2,這是待歸併數據的指針,初始位置在A段和tmp段的末尾。同時會記錄合併後數組的dest指針,位置在原B段的末尾。

    這裏還有一個小優化:生成dest指針時會直接把A段cursor1指向的數據拷貝到B段末尾,同時cursor–,dest–。由於以前(2)步的時候已經保證了arr[cursor1]>arr[dest]
    normal1

  4. 進行歸併排序,這裏每次歸併比較時會記錄A和tmp段比較「勝利(大於對方)」的次數,比較失敗(小於對方)時會把勝利數清零。當有一個段的數據連續N次勝利時會激活另外一個優化策略,在這裏假設N爲4,下圖已是A段連續勝利了4次的狀況:
    normal1

  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指針左移兩個位置。
    normal1

  6. 若是cursor1>=0,以後會再用curosr1指向的數據在tmp數組中查找,因爲這裏cursor1已是-1了,循環結束。

  7. 最後把tmp裏剩餘的數據拷貝到A數組的剩餘位置中,結束。
    normal1

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兩段:
    normal1

  2. 在「掐頭去尾」的時候,這時會有一些變化,程序執行到compare(B[base2],A[base1])時返回-1,A的左側留下了兩個應該被切走的「5」。
    normal1

  3. 接下來是正常的歸併過程。
    normal1

  4. 這裏一樣會觸發「勝利」>N次邏輯
    normal1

  5. 在A[base1,cursor1]中查找小於tmp[cursor2]的元素,複製,cursor1和dest左移兩位。
    normal1

  6. 此時再用A[cursor1]在tmp中查找,tmp中全部的數據都被移入A數組,cursor二、dest左移4位。tmp2剩餘元素的數量(len2)爲0。
    normal1

注意!

在第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


  • 解決方案:
  • 1.JVM加入以下參數-Djava.util.Arrays.useLegacyMergeSort=true,表示使用JDK6的排序算法

    2.按照規定的比較規則進行值的返回,a==b 返回 0,ab 返回 1

相關文章
相關標籤/搜索