本文為作者原創,轉載請註明出處。
我們都知道Java是跨平臺的,一次編譯,到處執行,本質上依賴於不同作業系統下有不同的JVM。到處執行是做到了,但執行結果呢?一樣的程式,在不同的JVM上跑的結果是否一樣呢?很遺憾,程式的執行結果沒有百分百的確定性,本篇分享我遇到的一些case。
坑一 慎用Class.getMethods()方法
在Class類中,有一個方法是getMethods(),返回的是一個Method陣列,該陣列包含了Class所包含的方法。但是需要注意的是,其陣列元素的排序是不確定的,在不同的機器上會有不一樣的排序輸出。
public Method[] getMethods() throws SecurityException {
checkMemberAccess(Member.PUBLIC, Reflection.getCallerClass(), true);
return copyMethods(privateGetPublicMethods());
}
阿里的fastjson就曾經在這裡踩到坑了,fastjson是序列化框架,當要去獲取物件的某個屬性值時,往往需要通過反射呼叫getter方法。比如,有個屬性field,那麼通過遍歷Method陣列,判斷是否有getField方法,如果有的話,則呼叫取得相應的值。
但對於boolean型別的欄位,其getter方法有可能是isXXX,也有可能是getXXX,而fastjson在遍歷時,只要判斷有isXXX或者getXXX,就認定其為getter方法,然後立即執行該getter方法。
// 虛擬碼
for (Method method : someObject.class.getMethods()) {
// 判斷是否為getter方法
if(method.getName().equals("getField") || method.getName().equals("isField")){
// 通過getter取得屬性值
return method.invoke(xxx, xxxx);
}
}
但是如果一個物件同時存在isA和getA方法呢?
private A a;
private boolan isA(){
return false;
}
private A getA(){
return a;
}
這個時候fastjson到底執行的是isA()還是getA()呢?答案是不確定,因為isA和getA在返回的Method陣列中順序是不確定的,所以有的機器上可能是通過isA()來獲取屬性值,有的機器上可能是通過getA()來獲取屬性值,而這兩個方法返回的一個是boolean型別,一個是A型別,導致fastjson在不同機器執行的結果是不一樣的。
為什麼這個方法返回值不按照字母排序呢?每個類或者方法名字都會對應一個Symbol物件,在這個名字第一次使用的時候構建,Symbol物件是通過malloc來分配的,因此新分配的Symbol物件的地址就不一定比後分配的Symbol物件地址小,也不一定大,因為期間存在記憶體free的動作,那地址是不會一直線性變化的,之所以不按照字母排序,主要還是為了速度考慮,根據Symbol物件的地址排序是最快的。
坑二 慎用執行緒優先順序做併發處理
執行緒Thread中有priority屬性,表示執行緒的優先順序,預設值為5,取值區間為[1,10]。雖然在Thread的註釋中有說明優先順序高的執行緒將會被優先執行,但是測試結果,卻是隨機的。
如下,
static class Runner implements Runnable {
@Override
public void run() {
for (int i = 0; i < 100; i++) {
System.out.println(Thread.currentThread().getName()+"---"+i);
}
}
}
public static void main(String[] args) {
Thread t1 = new Thread(new Runner(), "thread-1");
Thread t2 = new Thread(new Runner(), "thread-2");
Thread t3 = new Thread(new Runner(), "thread-3");
t1.setPriority(10); // t1 執行緒優先順序設定為10
t2.setPriority(5); // t2 執行緒優先順序設定為5
t3.setPriority(1); // t3 執行緒優先順序設定為1
t1.start();
t2.start();
t3.start();
}
如果是嚴格按照執行緒優先順序來執行的,那麼應該是t1執行for迴圈,然後t2執行完for迴圈,最後t3執行for迴圈。但實際上測試結果顯示,每次執行的輸出順序都沒有遵循這個規則,並且每次執行的結果都是不一樣的。
---- console output ----
thread-2---0
thread-2---1
thread-3---0
thread-1---0
thread-1---1
thread-1---2
thread-3---1
......
......
執行緒排程具有很多不確定性,執行緒的優先順序只是對執行緒的一個標誌,但不代表著這是絕對的優先,具體的執行順序都是由作業系統本身的資源排程來決定的。不同作業系統本身的執行緒排程方式可能存在差異性,所以不能依靠執行緒優先順序來處理併發邏輯。
坑三 慎用系統時間做精確時間計算
Java API中,一般使用native方法System.currentTimeMillis() 來獲取系統的時間。從方法名上,可以看出,該方法用於獲取系統當前的時間,即從1970年1月1日8時到當前的毫秒值。
下面羅列出了官方對該方法的註釋:
public final class System {
/**
* Note that while the unit of time of the return value is a millisecond,
* the granularity of the value depends on the underlying
* operating system and may be larger. For example, many
* operating systems measure time in units of tens of
* milliseconds.
*/
public static native long currentTimeMillis();
}
方法註釋明確指出了這個毫秒值的精度在不同的作業系統中是存在差異的,有的系統1毫秒實際上等同於物理時間的幾十毫秒。也就是說,在一個效能測試中,因為精度不一致的問題,有的系統得出的結果是1毫秒,另外系統得出的效能結果卻是10毫秒。
那如何實現高精度的時間計算呢?先來看看System.nanoTime()方法,下面列出了官方的核心註釋:
public final class System {
/**
* This method can only be used to measure elapsed time and is
* not related to any other notion of system or wall-clock time.
*/
public static native long nanoTime();
}
這個方法只能用於檢測系統經過的時間,也就是說其返回的時間不是從1970年1月1日8時開始的納秒時間,是從系統啟動開始時開始計算的時間。
所以一般高精度的時間是採用System.nanoTime()方法來實現的,其單位為納秒(十億分之一秒),雖然不保證完全準確的納秒級精度。但用該方法來實現毫秒級精度的計算,是綽綽有餘的,如下。
long start = System.nanoTime();
// do something
long end = System.nanoTime();
// 程式執行的時間,精確到毫秒
long costTime = (end - start) / 1000000L
坑四 慎用執行時Runtime類
Runtime是JVM中執行時環境的抽象,包含了執行時環境的一些資訊,每個Java應用程式都有一個Runtime例項,用於應用程式和其所在的執行時環境進行互動。應用程式本身無法建立Runtime例項,只能通過Runtime.getRuntime()方法來獲取。
顯然,執行時環境是因作業系統而異的。其互動方式也存在差異,
例如,
// Windows下呼叫程式
Process proc =Runtime.getRuntime().exec("exefile");
// Linux下呼叫程式
Process proc =Runtime.getRuntime().exec("./exefile");
所以,如果應用程式中包含這類和執行時環境進行互動的方法,應確保應用的部署環境不變,如果不能保證的話,那麼至少需要提供兩套執行時互動邏輯。
以上是我遇到的不能跨平臺的一些case,其實本質上都和native實現有關。你有沒有遇到一些這樣的坑呢?歡迎留言~
參考連結:
JVM原始碼分析之不保證順序的Class.getMethods
公眾號簡介:作者是螞蟻金服的一線開發,分享自己的成長和思考之路。內容涉及資料、工程、演算法。