測不準原理?記一次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