JDK reflection的并发问题

在JDK6中,reflection的并发性能其实不是很好,我们就遇到过在高并发环境下反射性能急剧下降的情况。大规模web应用就是有这么大并发呀,而且因为框架或其他原因,广泛使用reflection,所以值得重视这种问题。

继续阅读前建议打开JDK源代码。(在Eclipse中只需在Preferences->Installed JREs里把JRE设为你安装的JDK,而不是默认那个,然后Ctrl+Shift+T就可以用名字查找JDK的任意一个类了)

1. ClassLoader

首先我们来看看Class.forName(String)这个方法,它在反射中使用之频繁,如大白菜一般。
Class.forName(String)只是调用了forName0(...)方法,这是个native方法,C++实现,看不到源代码。源代码

其实它会调用ClassLoader.loadClass(String),然后转到ClassLoader.loadClass(String, boolean)。源代码

在JDK6中,这是一个synchronized方法,这个方法做什么详见上面链接的源代码。它会对整个ClassLoader加锁,可想而知——整个系统都在这等待——扛不住高并发。

在JDK7中,这里得到了优化(感谢RednaxelaFX大大的提示)。源代码 注意getClassLoadingLock()

该方法不再是synchronized,其做法是把整个方法体包在“synchronized (getClassLoadingLock(name))”中。这个name,是类的全限定名。它维护了一个包含一组锁的map,key为类名, value为一个普通的Object(当做锁对象来用),其实就是给每个类分配一把锁。所以锁粒度一下子从ClassLoader类级降到了每个class的级别。听RednaxelaFX说JDK8还有进一步优化。

 

2. Proxy

Proxy技术在AOP中密切使用。广泛使用的有JDK Proxy和CgLib Proxy两种实现。

我们可以看一下JDK7的Proxy类:源代码
Proxy.getProxyClass(...)会转到Proxy.getProxyClass0(...)。然后一路往下看,出现了两次同步:

1) synchronized (loaderToCache) 源代码
这个loaderToCache存放了各个classLoader各自的class cache,key=classLoader,value=map<classLoader, map<className, class>>。
  中间做的操作挺轻量的,但是锁粒度这么大,整个系统在这等待,扛不住高并发。
我建议两个改法:
第一个改法是去掉synchronized,把loaderToCache改成ConcurrentHashMap,目的不是减小同步范围,而是减小锁粒度,不对整个cache加锁,而只对map的一部分加锁(由于ConcurrentHashMap的实现),高并发时的性能会明显改善。
第二个改法是loaderToCache里面不直接存放每一个cache,而是把cache包装在一个简单的单元素容器里,对单个容器加锁,代码示例:

cacheContainer = loaderToCache.get(loader);
synchronized (cacheContainer) {
  cache = cacheContainer.get()
  if (cache == null) {
    cache = new HashMap<>();
    cacheContainer.set(cache);
  }
}

 

2) synchronized (cache) 源代码
这个cache是class cache,key=className, value=class。
哎哟妈呀,也给整个cache加着锁哩!虽然锁范围很小,wait调用还会释放锁,但是你要让大家都为了一个类在这等待吗?
改法不多说了,与前面所述的类似。采用第二个改法稍微复杂点,因为当proxy generating失败时会remove相应的key,所以建议尽量不要remove吧,或者只对remove做同步。

另外还能看到Proxy.proxyClasses这个属性是个synchronized WeakHashMap,要是WeakHashMap也有个concurrent实现就好了。

 

总结下来就是,JDK6 reflection的并发性能不是很高,JDK7有明显改善(JDK8进一步改善),况且JDK6离XP的命运不远了,大家还没升级的都赶紧升级吧(没错,就是你们这些大公司!)。至于Proxy,可以用CgLib的(字节码生成慢一些,但生成之后用起来就快多了)。

 

最后建议大家以后不要使用synchronized Map了,用ConcurrentHashMap吧,由于锁粒度的细化,高并发时的性能好很多。Spring框架就在2013年上半年把所有关于map的synchronization统统换成了ConcurrentHashMap。那时我本来想捡个漏赚个pull request,晚了一步啊。

posted @ 2014-04-09 12:50  渐近的旅者  Views(1416)  Comments(4Edit  收藏  举报