1. 程式人生 > >多線程編程中的"坑"--近期遇到的多線程bug總結

多線程編程中的"坑"--近期遇到的多線程bug總結

bean imp eat lse 場景 多線程編程 必須 net ade

最近工作中連續碰到幾個涉及多線程方面的bug,在這總結梳理一下,就當提醒自己別犯同樣的錯誤。

Bug 1 - 狂轉的CPU

同事的一個項目上線的時候,發現CPU占用率奇高,達到700%,而平常的時候,也就100%左右。用jstack查看線程棧,發現很多線程都卡在一個名為waitUntilInited()的方法裏面。查看代碼,發現這個方法是這樣的:

private boolean inited = false;
...
void waitUntilInited() {
    while(!inited) {
        ;
    }
}

有一個線程會執行一些初始化操作,初始化完成會將inited變量賦值為true;而業務線程調用waitUntilInited()

方法等待初始化完成才能執行操作。說到這裏,bug已經很明顯了。這是典型的沒使用volatile導致的線程可見性bug。這個bug的情況比較簡單,由於一直在做循環,比較容易定位到問題所在。但有時候由於可見性問題造成的bug會比這個詭異得多,因此在寫多線程程序的時候要特別留心共享變量的可見性。

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總結