2010-01-04 59 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