2010-01-04 58 views
3

我在我的應用程序中有針腳指向瓶頸,在我看來,它歸結爲Thread::setContextClassLoader的呼叫。setContextClassLoader在同時調用時速度顯着減慢

基本上我被迫更動線程的上下文類加載由於與第三方庫(見this question明白爲什麼)問題。

我拿起了我的知識常見的一種,它的工作原理是這樣的解決方案:

Thread thread = Thread.currentThread(); 
ClassLoader old = thread.getContextClassLoader(); 
thread.setContextClassLoader(newClassLoader); 

try { 
    ... // problematic code that uses the thread context class loader 
} finally { 
    thread.setContextClassLoader(old); 
} 

原來調用setContextClassLoader不是一個問題,當只有1個線程運行,但當多個線程正在執行時,它會急劇減速。

我做了以下測試程序來隔離問題:

ArrayList<Thread> threads = new ArrayList<Thread>(); 
int thread_count = 1; 

long start = System.currentTimeMillis(); 

for (int i = 0; i < thread_count; i++) { 
    Thread thread = new Thread(new MyRunnable(100000000)); 

    thread.start(); 
    threads.add(thread); 
} 

for (Thread thread : threads) { 
    thread.join(); 
} 

long total = System.currentTimeMillis() - start; 
double seconds = (double)total/1000; 

System.out.println("time in seconds: " + seconds); 

這是MyRunnable類:

public class MyRunnable implements Runnable { 
    int _iterations; 

    public MyRunnable(int iterations) { 
     _iterations = iterations; 
    } 

    public void run() { 
     final Thread curr = Thread.currentThread(); 
     final ClassLoader loader = ClassLoader.getSystemClassLoader(); 

     for (int i = 0; i < _iterations; i++) { 
      curr.setContextClassLoader(loader); 
     } 
    } 
} 

基本上它打開了幾個線程,並設置當前線程上下文類加載器循環到系統類加載器。

在我的機器上更改了代碼後的結果:當thread_count爲1時,它在半秒鐘內完成。 2個線程佔用1.5〜3個線程2.7〜4個線程4〜 - 你得到的圖片。

我試過尋找線程的setContextClassLoader的實現,它似乎只是設置一個成員變量傳遞給它的類加載器。我發現在使用多個線程運行時沒有鎖定(或訪問需要的共享資源)來解釋這種開銷。

我在這裏錯過了什麼?

P.S.我正在使用JRE 1.5,但在1.6中發生了相同的事情。

編輯: @Tom Hawtin - 查看我所做的代碼更改以排除您提到的原因。即使系統類加載器被提取一次,當線程數大於1時結果也會變慢。

回答

3

來源中唯一真正明顯的事情與Thread.setContextClassLoader無關。 ClassLoader.getSystemClassLoader調用initSystemClassLoader即使系統類加載器已被初始化,也會鎖定ClassLoader.class

潛在的問題是讀取易失性變量可能會對某些多處理器機器產生性能影響。

請注意,我們只在這裏查看幾百個週期。

+0

只有幾百個週期,但大部分用戶代碼本身只有幾個週期,所以這很容易導致他看到的放緩。此外,鎖有效地排除了並行性。是的,setContextClassLoader()調用增加了時間,但在真實應用程序中,您不會在緊密循環中調用它1000萬次。這個用法將是你應用程序總使用量的微觀部分。不要擔心。 – 2010-01-04 17:51:13

+0

而不是調用getSystemClassLoader,創建一個空的URLClassLoader - 你會看到相同的結果。 – 2010-01-04 18:08:59

+0

在循環外部(並使用它)添加'final ClassLoader system = ClassLoader.getSystemClassLoader();主要爲我解決了這個問題。在所有情況下也大大提高性能。 – 2010-01-04 18:46:23

2

如果只有一個線程訪問給定的鎖,最近的JVM使用稱爲「偏置鎖定」的技術,這使得鎖定採集幾乎可用。當第二個線程第一次嘗試訪問鎖時,對原始訪問器的「偏見」被撤銷,並且鎖成爲正常/輕量級的鎖,它需要原子操作來獲取(有時一個釋放)。

偏置鎖定與正常鎖定之間的性能差異可能是一個數量級(例如5個週期對50個週期),這與您的測量結果一致。這裏提到的鎖可能是您提到的first reply中提到的鎖。如果您有興趣,可以更詳細地描述偏向鎖定here

即使忽略偏置鎖定,嘗試獲取相同鎖的兩個線程或更多線程的總吞吐量通常會比單個線程慢很多(因爲它們爭用包含鎖定字的高速緩存行)。

+0

查看更新的代碼 - 即使只有一次調用getSystemClassLoader(),結果也保持不變。 – 2010-01-05 08:44:36

+0

當然,但你爲什麼不預期結果會變慢呢?正如湯姆指出的那樣,正在完成的工作與線程數量成正比 - 使用4個線程循環多次4次。 你有多少個CPU?你有超線程嗎?您也可能會遇到抖動,因爲測試不會結束,直到所有CPU都完成,儘管大多數線程可能會提前完成。 – BeeOnRope 2010-01-05 22:33:27