Tomcat的Request不是线程安全的
这几天遇到一个和Request及Cookie相关的问题,再次验证了多线程是魔鬼的道理。这是从一个第三方jar中反编译出来的代码,程序间歇性的抛NullPointerException,看stack trace是第8行抛的。
public String getCookieVal(String key) { Cookie[] cookies = this.req.getCookies(); if (cookies == null) { return null; } for (int i = 0; i if (key.equals(cookie.getName())) { return cookie.getValue(); } } return null;
这行为什么会抛NullPointerException呢?看代码无非key是null或者cookie是null,于是想到加Log,捕捉到NPE时打出传入的key和所有的cookie,看看有没有null。
可是...从Log看,捕捉到NPE时,key是有值的,所有的cookie也都是有值的,难道见鬼了?
这个bug纠结了两天,才找到了问题所在:正如标题,Request.getCookies()不是线程安全的。
先来看一下Servelt2.4spec中关于Request.getCookies()的描述
publicCookie[]getCookies()
ReturnsanarraycontainingalloftheCookieobjectstheclientsentwiththis
request.Thismethodreturnsnullifnocookiesweresent.
Returns:anarrayofalltheCookiesincludedwiththisrequest,ornullif
the request has no cookies
从这个描述来,getCookies方法返回的数组中应该不会包含null才对,但是,按照Tomcat5.5的源码来看,并不是这样,
以下是org.apache.catalina.connector.Request中的实现/* * Return the set of Cookies received with this Request. */ public Cookie[] getCookies() { if (!cookiesParsed) parseCookies(); return cookies; }
parseCookies源码如下
/** * Parse cookies. */ protected void parseCookies() { cookiesParsed = true; Cookies serverCookies = coyoteRequest.getCookies(); int count = serverCookies.getCookieCount(); if (count <= 0) return; cookies = new Cookie[count]; int idx=0; for (int i = 0; i < count; i++) { ServerCookie scookie = serverCookies.getCookie(i); try { /* we must unescape the '\\' escape character */ Cookie cookie = new Cookie(scookie.getName().toString(),null); int version = scookie.getVersion(); cookie.setVersion(version); cookie.setValue(unescape(scookie.getValue().toString())); cookie.setPath(unescape(scookie.getPath().toString())); String domain = scookie.getDomain().toString(); if (domain!=null) cookie.setDomain(unescape(domain));//avoid NPE String comment = scookie.getComment().toString(); cookie.setComment(version==1?unescape(comment):null); cookies[idx++] = cookie; } catch(IllegalArgumentException e) { // Ignore bad cookie } } if( idx < count ) { Cookie [] ncookies = new Cookie[idx]; System.arraycopy(cookies, 0, ncookies, 0, idx); cookies = ncookies; } }
getCookies()的流程是先置cookiesParsed,再生成并初始化cookies[]数组,并且没有同步,所以,若两个线程在同一个Request上按照以下时间步骤调用getCookies方法,就会出错,
ThreadAThreadB
1getCookies();
2parseCookie();
3cookiesParsed=true;
4cookies=newCookie[count];
5getCookies();
6cookiesParsed==true;
7returncookies;
如果cookies数组刚被new出来,还没有完成初始化,ThreadA就让出了时间片,ThreadB接着调用getCookies()就可能读到一个没有初始化完成的cookies,这个cookies中包含null元素。
这同时也解释了为什么ThreadB的log中request.getCookies()都是有值的:当ThreadB的log再次调用request.getCookies()时,ThreadA早已再次获得时间片,并完成了cookies的初始化,所以log中getCookies()拿到的都是好的发现不了问题。
定位了问题后,解决方法是从源头上避免对于request的并发访问,相对就比较容易实现了。
这次debug的总结
1)Request并不是一个线程安全的对象
2) 多线程真心坑爹,要不java也不会推出个conccurent包。每次遇到多线程的问题,由于不能固定重现,大多数时间都是在脑海中画时序图,猜哪条路径会出错,太坑爹了!