閱讀507 返回首頁    go 阿裏雲 go 技術社區[雲棲]


測不準原理?記一次Guava隊列問題的排查

引子

測不準原理中有一個現象:人們對光子的觀測行為本身會影響觀測的結果。近期在排查問題中也遇到了類似的“詭異”問題,初期百思不得其解,真相大白之後搖頭苦笑,記在這裏貽笑大方。

現象

先看一段經過簡化的代碼。

// 對象結構體
public class Foo {
    private int id;

    public Foo(int i) {
        this.id = i;
    }

    public Foo() {

    }

    public void setId(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }
}

// 處理邏輯
List<Foo> fooList = new ArrayList<>();
fooList.add(new Foo(1));

List<Foo> newFooList=Lists.transform(fooList,new Function<Foo, Foo>(){
    @Override
    public Foo apply(Foo input){
        Foo output=new Foo();
        // 一些處理邏輯
        output.setId(input.getId()*10);
        return output;
        }
});

for(Foo foo:newFooList){
    // 後處理邏輯
    int id=foo.getId()*10;
    foo.setId(id);
}

問題:最後newFooList裏麵元素的id是多少?

上麵的代碼不複雜,JAVA又有強大趁手的調試工具,連上debugger單步走一遭就是。可是結果卻令人驚訝:雖然每一行代碼都走到了,最後的結果卻是明白無誤的10——中間的那次乘10操作被吃了?更詭異的事情在後麵:如果在for循環裏加一個斷點,明白無誤的看到新的id被賦值進去,但是把鼠標移到newFooList上,裏麵的指向的object對象裏的值還是10,且每次看這個對象的地址都在不斷遞增,好像後台在不斷的new對象,有內存泄露?

分析

對象的地址遞增可以解釋為何後處理邏輯的值設不進list裏:看到的對象已經不是當初的保存對象了,當然看不到設置的值了。apply函數中有new更是高度懷疑對象。

為何list裏保存對象會變呢?摟了一遍Guava list的源碼,裏麵自己實現了一個list以及相應的listIterator,隻要有對數組元素的查詢遍曆操作,就會調用apply函數,執行apply函數裏的邏輯——也就是每次new一個新對象,且設id的值為10。

為何用idea查看時,斷點沒動,對象的地址在遞增?這裏估計是idea做了特殊處理:它會調用list的get方法,重新new對象並設初值,但是不會觸發裏麵的斷點。證據就是如果在裏麵加上print函數,雖然沒有觸發斷點,但是有日誌打印出來。

總結

這個問題的產生的根源就是對list.transform的行為邏輯想當然了,以為apply函數的用處是一次性的。雖然java8中已經提供了替代的stream,但是在一些老係統中仍然存在著guava的代碼,如果對實現原理理解不清楚就會踩坑。在上述例子中,要注意List.transform後不要再對元素進行處理,如果一定要處理,需要將結果固定到一個數組中(使用Lists.newArrayList()或者ImmutableList.copyOf)。

最後更新:2017-06-05 15:32:30

  上一篇:go  真的嗎?算法謀取暴利,讓你多花錢
  下一篇:go  實戰json、html、jsx的互轉