这几天遇到一个和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()不是线程安全的。
先来看一下Servelt 2.4 spec 中关于Request.getCookies()的描述
public Cookie[] getCookies()
Returns an array containing all of the Cookie objects the client sent with this
request. This method returns null if no cookies were sent.
Returns: an array of all the Cookies included with this request, or null if
the request has no cookies
从这个描述来,getCookies方法返回的数组中应该不会包含null才对,但是,按照Tomcat 5.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方法,就会出错,
Thread A Thread B
1 getCookies();
2 parseCookie();
3 cookiesParsed = true;
4 cookies = new Cookie[count];
5 getCookies();
6 cookiesParsed==true;
7 return cookies;
如果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包。每次遇到多线程的问题,由于不能固定重现,大多数时间都是在脑海中画时序图,猜哪条路径会出错,太坑爹了!