多線程編程中的"坑"--近期遇到的多線程bug總結
最近工作中連續碰到幾個涉及多線程方面的bug,在這總結梳理一下,就當提醒自己別犯同樣的錯誤。
Bug 1 - 狂轉的CPU
同事的一個項目上線的時候,發現CPU占用率奇高,達到700%,而平常的時候,也就100%左右。用jstack查看線程棧,發現很多線程都卡在一個名為waitUntilInited()
的方法裏面。查看代碼,發現這個方法是這樣的:
private boolean inited = false;
...
void waitUntilInited() {
while(!inited) {
;
}
}
有一個線程會執行一些初始化操作,初始化完成會將inited變量賦值為true;而業務線程調用waitUntilInited()
Bug 2 - 忽隱忽現的地址已綁定異常
最近同事的項目在啟動的時候,Dubbo服務打開端口偶爾會出現地址已經被綁定異常(java.net.BindException
)。出現異常的代碼在com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
類裏面,其中創建server的方法是這樣的:
public <T> Exporter<T> export(Invoker<T> invoker) throws RpcException { URL url = invoker.getUrl(); ... openServer(url); return invoker; } private void openServer(URL url) { // find server. String key = url.getAddress(); //client 也可以暴露一個只有server可以調用的服務。 boolean isServer = url.getParameter(Constants.IS_SERVER_KEY,true); if (isServer) { ExchangeServer server = serverMap.get(key); if (server == null) { serverMap.put(key, createServer(url)); } else { //server支持reset,配合override功能使用 server.reset(url); } } }
由於應用為服務配置了延遲暴露,而延遲暴露實現方式是另起一個線程,sleep一段時間,然後再暴露方法,這就導致會並發調用上面的export()
方法,進而間接並發地調用createServer()
,最終導致多次綁定同一個地址的異常。解決的辦法很簡單,為openServer()
方法加上synchronized
關鍵字即可;或者使用synchronized
塊,將鎖的粒度減小。
這種bug比較隱蔽,因為serverMap
是一個ConcurrentHashMap
,很多人以為使用了ConcurrentHashMap
就是線程安全的,而且在創建server之前先在map中查詢了一次,如果沒有才會創建,所以應該沒有問題。但沒有意識到ConcurrentHashMap
保證的只是map內部的操作是同步的,不能一次get()操作和一次緊鄰的put操作也是同步的,所以必須在外部加上同步措施。
Bug 3 - 神出鬼沒的CompileError
也是一個同事的項目,在啟動的時候偶爾會出現下面的異常:
Caused by: java.lang.RuntimeException: [source error] no such class: com.alibaba.dubbo.common.bytecode.proxy2
at com.alibaba.dubbo.common.bytecode.ClassGenerator.toClass(ClassGenerator.java:354) ~[dubbo-yiji-2.5.13.jar:yiji-2.5.13]
at com.alibaba.dubbo.common.bytecode.ClassGenerator.toClass(ClassGenerator.java:293) ~[dubbo-yiji-2.5.13.jar:yiji-2.5.13]
at com.alibaba.dubbo.common.bytecode.Proxy.getProxy(Proxy.java:214) ~[dubbo-yiji-2.5.13.jar:yiji-2.5.13]
at com.alibaba.dubbo.common.bytecode.Proxy.getProxy(Proxy.java:67) ~[dubbo-yiji-2.5.13.jar:yiji-2.5.13]
at com.alibaba.dubbo.rpc.proxy.javassist.JavassistProxyFactory.getProxy(JavassistProxyFactory.java:35) ~[dubbo-yiji-2.5.13.jar:yiji-2.5.13]
at com.alibaba.dubbo.rpc.proxy.AbstractProxyFactory.getProxy(AbstractProxyFactory.java:49) ~[dubbo-yiji-2.5.13.jar:yiji-2.5.13]
at com.alibaba.dubbo.rpc.proxy.wrapper.StubProxyFactoryWrapper.getProxy(StubProxyFactoryWrapper.java:60) ~[dubbo-yiji-2.5.13.jar:yiji-2.5.13]
at com.alibaba.dubbo.rpc.ProxyFactory$Adpative.getProxy(ProxyFactory$Adpative.java) ~[dubbo-yiji-2.5.13.jar:yiji-2.5.13]
at com.alibaba.dubbo.config.ReferenceConfig.createProxy(ReferenceConfig.java:431) ~[dubbo-yiji-2.5.13.jar:yiji-2.5.13]
at com.alibaba.dubbo.config.ReferenceConfig.init(ReferenceConfig.java:305) ~[dubbo-yiji-2.5.13.jar:yiji-2.5.13]
at com.alibaba.dubbo.config.ReferenceConfig.get(ReferenceConfig.java:139) ~[dubbo-yiji-2.5.13.jar:yiji-2.5.13]
at com.alibaba.dubbo.config.spring.AnnotationBean$2.call(AnnotationBean.java:296) ~[dubbo-yiji-2.5.13.jar:yiji-2.5.13]
at com.alibaba.dubbo.config.spring.AnnotationBean$2.call(AnnotationBean.java:293) ~[dubbo-yiji-2.5.13.jar:yiji-2.5.13]
... 4 common frames omitted
Caused by: javassist.CannotCompileException: [source error] no such class: com.alibaba.dubbo.common.bytecode.proxy2
at javassist.CtNewMethod.make(CtNewMethod.java:79) ~[javassist-3.18.1-GA.jar:na]
at javassist.CtNewMethod.make(CtNewMethod.java:45) ~[javassist-3.18.1-GA.jar:na]
at com.alibaba.dubbo.common.bytecode.ClassGenerator.toClass(ClassGenerator.java:322) ~[dubbo-yiji-2.5.13.jar:yiji-2.5.13]
... 16 common frames omitted
Caused by: javassist.compiler.CompileError: no such class: com.alibaba.dubbo.common.bytecode.proxy2
at javassist.compiler.MemberResolver.searchImports(MemberResolver.java:468) ~[javassist-3.18.1-GA.jar:na]
at javassist.compiler.MemberResolver.lookupClass(MemberResolver.java:412) ~[javassist-3.18.1-GA.jar:na]
at javassist.compiler.MemberResolver.lookupClassByName(MemberResolver.java:315) ~[javassist-3.18.1-GA.jar:na]
at javassist.compiler.TypeChecker.atNewExpr(TypeChecker.java:146) ~[javassist-3.18.1-GA.jar:na]
at javassist.compiler.ast.NewExpr.accept(NewExpr.java:73) ~[javassist-3.18.1-GA.jar:na]
at javassist.compiler.CodeGen.doTypeCheck(CodeGen.java:242) ~[javassist-3.18.1-GA.jar:na]
at javassist.compiler.CodeGen.compileExpr(CodeGen.java:229) ~[javassist-3.18.1-GA.jar:na]
at javassist.compiler.CodeGen.atReturnStmnt2(CodeGen.java:598) ~[javassist-3.18.1-GA.jar:na]
at javassist.compiler.JvstCodeGen.atReturnStmnt(JvstCodeGen.java:425) ~[javassist-3.18.1-GA.jar:na]
at javassist.compiler.CodeGen.atStmnt(CodeGen.java:363) ~[javassist-3.18.1-GA.jar:na]
at javassist.compiler.ast.Stmnt.accept(Stmnt.java:50) ~[javassist-3.18.1-GA.jar:na]
at javassist.compiler.CodeGen.atStmnt(CodeGen.java:351) ~[javassist-3.18.1-GA.jar:na]
at javassist.compiler.ast.Stmnt.accept(Stmnt.java:50) ~[javassist-3.18.1-GA.jar:na]
at javassist.compiler.CodeGen.atMethodBody(CodeGen.java:292) ~[javassist-3.18.1-GA.jar:na]
at javassist.compiler.CodeGen.atMethodDecl(CodeGen.java:274) ~[javassist-3.18.1-GA.jar:na]
at javassist.compiler.ast.MethodDecl.accept(MethodDecl.java:44) ~[javassist-3.18.1-GA.jar:na]
at javassist.compiler.Javac.compileMethod(Javac.java:169) ~[javassist-3.18.1-GA.jar:na]
at javassist.compiler.Javac.compile(Javac.java:95) ~[javassist-3.18.1-GA.jar:na]
at javassist.CtNewMethod.make(CtNewMethod.java:74) ~[javassist-3.18.1-GA.jar:na]
... 18 common frames omitted
由於異常不是每次啟動都出現,所以推測可能和多線程有關。查看源碼,發現com.alibaba.dubbo.common.bytecode.ClassGenerator
類的getClassPool()
方法有問題。
private static final Map<ClassLoader, ClassPool> POOL_MAP = new ConcurrentHashMap<ClassLoader, ClassPool>();
public static ClassGenerator newInstance()
{
return new ClassGenerator(getClassPool(Thread.currentThread().getContextClassLoader()));
}
public static ClassPool getClassPool(ClassLoader loader)
{
if( loader == null )
return ClassPool.getDefault();
ClassPool pool = POOL_MAP.get(loader);
if( pool == null )
{
pool = new ClassPool(true);
pool.appendClassPath(new LoaderClassPath(loader));
POOL_MAP.put(loader, pool);
}
return pool;
}
其中getClassPool()
方法不是線程安全的。作者用一個ConcurrentHashMap
保存每個ClassLoader對應的ClassPool。和Bug2情況類似,作者也是先get()一下,如果沒有,就創建一個,然後再put()回去。這個過程沒有加鎖,如果第一個線程get()發現沒有,緊接著第二個線程用同樣的key也來get(),這時候還是沒有,然後第一個線程創建ClassPool放進map, 第二個線程也新建一個ClassPool放進map,就會把第一個線程的ClassPool覆蓋,造成第一個線程創建的proxy class找不到。
解決辦法有多種,可以使用鎖同步get()、put()操作,也可以在put的時候,使用putIfAbsent()
方法,這樣就不會覆蓋已經創建好的ClassPool,然後get()到最新的value返回。
為什麽之前沒有發現這個bug呢?其實用官方的dubbo版本,不會出現問題,因為ReferenceBean的初始化是單線程的。最近公司內部維護的版本優化使用多線程來初始化 ReferenceBean,才導致上述bug暴露出來。所以有時候程序運行正常不代表沒有bug,開發和測試的時候應該盡量覆蓋更多的使用場景,盡量減少隱藏bug的可能性。
Technorati Tags: 多線程多線程編程中的"坑"--近期遇到的多線程bug總結