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()的描述

 写道
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包。每次遇到多线程的问题,由于不能固定重现,大多数时间都是在脑海中画时序图,猜哪条路径会出错,太坑爹了!

相关推荐