被我们忽略的HttpSession线程安全问题

本文探讨了Java Web中Servlet、JSP及HttpSession对象的线程安全性问题,通过源码分析揭示了如何确保这些组件在线程并发环境下的正确行为,并提出了使JavaBean线程安全的建议。

1. 背景

最近在读《Java concurrency in practice》(Java并发实战),其中1.4节提到了Java web的线程安全问题时有如下一段话:

Servlets and JPSs, as well as servlet filters and objects stored in scoped containers like ServletContext and HttpSession, 
simply have to be thread-safe.

Servlet, JSP, Servlet filter 以及保存在 ServletContext、HttpSession 中的对象必须是线程安全的。含义有两点:

1)Servlet, JSP, Servlet filter 必须是线程安全的(JSP的本质其实就是servlet);

2)保存在ServletContext、HttpSession中的对象必须是线程安全的;

servlet和servelt filter必须是线程安全的,这个一般是不存在什么问题的,只要我们的servlet和servlet filter中没有实例属性或者实例属性是”不可变对象“就基本没有问题。但是保存在ServletContext和HttpSession中的对象必须是线程安全的,这一点似乎一直被我们忽略掉了。在Java web项目中,我们经常要将一个登录的用户保存在HttpSession中,而这个User对象就是像下面定义的一样的一个Java bean:

public class User {
    private int id;
    private String userName;
    private String password;
    // ... ...
    
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getUserName() {
        return userName;
    }
    public void setUserName(String userName) {
        this.userName = userName;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
}

2. 源码分析

下面分析一下为什么将一个这样的Java对象保存在HttpSession中是有问题的,至少在线程安全方面不严谨的,可能会出现并发问题。

Tomcat8.0中HttpSession的源码在org.apache.catalina.session.StandardSession.java文件中,源码如下(截取我们需要的部分):

public class StandardSession implements HttpSession, Session, Serializable {
    // ----------------------------------------------------- Instance Variables
    /**
     * The collection of user data attributes associated with this Session.
     */
    protected Map<String, Object> attributes = new ConcurrentHashMap<>();

   /**
     * Return the object bound with the specified name in this session, or
     * <code>null</code> if no object is bound with that name.
     *
     * @param name Name of the attribute to be returned
     *
     * @exception IllegalStateException if this method is called on an
     *  invalidated session
     */
    @Override
    public Object  getAttribute(String name) {

        if (!isValidInternal())
            throw new IllegalStateException
                (sm.getString("standardSession.getAttribute.ise"));

        if (name == null) return null;

        return (attributes.get(name));

    }

   /**
     * Bind an object to this session, using the specified name.  If an object
     * of the same name is already bound to this session, the object is
     * replaced.
     * <p>
     * After this method executes, and if the object implements
     * <code>HttpSessionBindingListener</code>, the container calls
     * <code>valueBound()</code> on the object.
     *
     * @param name Name to which the object is bound, cannot be null
     * @param value Object to be bound, cannot be null
     * @param notify whether to notify session listeners
     * @exception IllegalArgumentException if an attempt is made to add a
     *  non-serializable object in an environment marked distributable.
     * @exception IllegalStateException if this method is called on an
     *  invalidated session
     */

    public void setAttribute(String name, Object value, boolean notify) {

        // Name cannot be null
        if (name == null)
            throw new IllegalArgumentException
                (sm.getString("standardSession.setAttribute.namenull"));

        // Null value is the same as removeAttribute()
        if (value == null) {
            removeAttribute(name);
            return;
        }
        // ... ...
        // Replace or add this attribute
        Object unbound = attributes.put(name, value);
       // ... ...
   }
   /**
     * Release all object references, and initialize instance variables, in
     * preparation for reuse of this object.
     */
    @Override
    public void recycle() {
        // Reset the instance variables associated with this Session
        attributes.clear();
        // ... ...
    }
    /**
     * Write a serialized version of this session object to the specified
     * object output stream.
     * <p>
     * <b>IMPLEMENTATION NOTE</b>:  The owning Manager will not be stored
     * in the serialized representation of this Session.  After calling
     * <code>readObject()</code>, you must set the associated Manager
     * explicitly.
     * <p>
     * <b>IMPLEMENTATION NOTE</b>:  Any attribute that is not Serializable
     * will be unbound from the session, with appropriate actions if it
     * implements HttpSessionBindingListener.  If you do not want any such
     * attributes, be sure the <code>distributable</code> property of the
     * associated Manager is set to <code>true</code>.
     *
     * @param stream The output stream to write to
     *
     * @exception IOException if an input/output error occurs
     */
    protected void doWriteObject(ObjectOutputStream stream) throws IOException {
        // ... ...
        // Accumulate the names of serializable and non-serializable attributes
        String keys[] = keys();
        ArrayList<String> saveNames = new ArrayList<>();
        ArrayList<Object> saveValues = new ArrayList<>();
        for (int i = 0; i < keys.length; i++) {
            Object value = attributes.get(keys[i]);
            if (value == null)
                continue;
            else if ( (value instanceof Serializable)
                    && (!exclude(keys[i]) )) {
                saveNames.add(keys[i]);
                saveValues.add(value);
            } else {
                removeAttributeInternal(keys[i], true);
            }
        }

        // Serialize the attribute count and the Serializable attributes
        int n = saveNames.size();
        stream.writeObject(Integer.valueOf(n));
        for (int i = 0; i < n; i++) {
            stream.writeObject(saveNames.get(i));
            try {
                stream.writeObject(saveValues.get(i));  
                // ... ...            
            } catch (NotSerializableException e) {
                // ... ...               
            }
        }
    }
}

我们看到每一个独立的HttpSession中保存的所有属性,是存储在一个独立的ConcurrentHashMap中的:

protected Map<String, Object> attributes = new ConcurrentHashMap<>();

所以我可以看到 HttpSession.getAttribute(), HttpSession.setAttribute() 等等方法就都是线程安全的。

另外如果我们要将一个对象保存在HttpSession中时,那么该对象应该是可序列化的。不然在进行HttpSession的持久化时,就会被抛弃了,无法恢复了:

            else if ( (value instanceof Serializable)
                    && (!exclude(keys[i]) )) {
                saveNames.add(keys[i]);
                saveValues.add(value);
            } else {
                removeAttributeInternal(keys[i], true);
            }

所以从源码的分析,我们得出了下面的结论:

1)HttpSession.getAttribute(), HttpSession.setAttribute() 等等方法都是线程安全的;

2)要保存在HttpSession中对象应该是序列化的;

虽然getAttribute,setAttribute是线程安全的了,那么下面的代码就是线程安全的吗?

session.setAttribute("user", user);

User user = (User)session.getAttribute("user", user);

不是线程安全的!因为User对象不是线程安全的,假如有一个线程执行下面的操作:

User user = (User)session.getAttribute("user", user);

user.setName("xxx");

那么显然就会存在并发问题。因为会出现:有多个线程访问同一个对象 user, 并且至少有一个线程在修改该对象。但是在通常情况下,我们的Java web程序都是这么写的,为什么又没有出现问题呢?原因是:在web中 ”多个线程访问同一个对象 user, 并且至少有一个线程在修改该对象“ 这样的情况极少出现;因为我们使用HttpSession的目的是在内存中暂时保存信息,便于快速访问,所以我们一般不会进行下面的操作:

User user = (User)session.getAttribute("user", user);

user.setName("xxx");

我们一般是只使用对从HttpSession中的对象使用get方法来获得信息,一般不会对”从HttpSession中获得的对象“调用set方法来修改它;而是直接调用 setAttribute来进行设置或者替换成一个新的。

3. 结论

所以结论是:如果你能保证不会对”从HttpSession中获得的对象“调用set方法来修改它,那么保存在HttpSession中的对象可以不是线程安全的(因为他是”事实不可变对象“,并且ConcurrentHashMap保证了它是被”安全发布的“);但是如果你不能保证这一点,那么你必须要实现”保存在HttpSession中的对象必须是线程安全“。不然的话,就存在并发问题。

使Java bean线程安全的最简单方法,就是在所有的get/set方法都加上synchronized。

## crash date:2025-08-22 17:30:51 ## exe: MediaServer ## signal: 11 ## version: ZLMediaKit(git hash:/,branch:,build time:2025-08-14T16:01:23) ## stack: [0]: ./MediaServer() [0x596f94] sig_crash(int) System.cpp:? [1]: /usr/lib64/libc.so.6(+0x394f0) [0x7f143b81c4f0] ?? ??:0 [2]: ./MediaServer(_ZN8mediakit9RtpServer7getPortEv+0x1c) [0x70338c] mediakit::RtpServer::getPort() ??:? [3]: ./MediaServer(_Z13openRtpServertRKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEEiS6_bj+0x1f6) [0x5b3566] openRtpServer(unsigned short, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, int, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const&, bool, unsigned int) ??:? [4]: ./MediaServer() [0x5b38bf] installWebApi()::{lambda(toolkit::SockInfo&, mediakit::StrCaseMap&, HttpAllArgs<std::map<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, mediakit::StrCaseCompare, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > > > const&, Json::Value&)#29}::operator()(toolkit::SockInfo&, mediakit::StrCaseMap&, HttpAllArgs<std::map<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, mediakit::StrCaseCompare, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > > > const&, Json::Value&) const [clone .isra.1917] WebApi.cpp:? [5]: ./MediaServer() [0x59b516] std::_Function_handler<void (toolkit::SockInfo&, mediakit::StrCaseMap&, HttpAllArgs<std::map<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, mediakit::StrCaseCompare, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > > > const&, Json::Value&, mediakit::HttpResponseInvokerImp const&), toApi(std::function<void (toolkit::SockInfo&, mediakit::StrCaseMap&, HttpAllArgs<std::map<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, mediakit::StrCaseCompare, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > > > const&, Json::Value&)> const&)::{lambda(toolkit::SockInfo&, mediakit::StrCaseMap&, HttpAllArgs<std::map<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, mediakit::StrCaseCompare, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > > > const&, Json::Value&, mediakit::HttpResponseInvokerImp const&)#1}>::_M_invoke(std::_Any_data const&, toolkit::SockInfo&, mediakit::StrCaseMap&, HttpAllArgs<std::map<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, mediakit::StrCaseCompare, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > > > const&, Json::Value&, mediakit::HttpResponseInvokerImp const&) WebApi.cpp:? [6]: ./MediaServer() [0x5b756c] toApi(std::function<void (toolkit::SockInfo&, mediakit::StrCaseMap&, HttpAllArgs<std::map<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, mediakit::StrCaseCompare, std::allocator<std::pair<std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > const, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> > > > > > const&, Json::Value&, mediakit::HttpResponseInvokerImp const&)> const&)::{lambda(mediakit::Parser const&, mediakit::HttpResponseInvokerImp const&, toolkit::SockInfo&)#1}::operator()(mediakit::Parser const&, mediakit::HttpResponseInvokerImp const&, toolkit::SockInfo&) const WebApi.cpp:? [7]: ./MediaServer() [0x5b6cb7] addHttpListener()::{lambda(mediakit::Parser const&, mediakit::HttpResponseInvokerImp const&, bool&, toolkit::SockInfo&)#2}::operator()(mediakit::Parser const&, mediakit::HttpResponseInvokerImp const&, bool&, toolkit::SockInfo&) const [clone .isra.2033] WebApi.cpp:? [8]: ./MediaServer(_ZN8mediakit11HttpSession13emitHttpEventEb+0x2c1) [0x68cb71] mediakit::HttpSession::emitHttpEvent(bool) ??:? [9]: ./MediaServer() [0x68cea2] std::_Function_handler<bool (char const*, unsigned long), mediakit::HttpSession::Handle_Req_POST(long&)::{lambda(char const*, unsigned long)#2}>::_M_invoke(std::_Any_data const&, char const*&&, unsigned long&&) HttpSession.cpp:? [10]: ./MediaServer(_ZN8mediakit11HttpSession13onRecvContentEPKcm+0x34) [0x688494] mediakit::HttpSession::onRecvContent(char const*, unsigned long) ??:? [11]: ./MediaServer(_ZN8mediakit19HttpRequestSplitter5inputEPKcm+0x248) [0x687bd8] mediakit::HttpRequestSplitter::input(char const*, unsigned long) ??:? [12]: ./MediaServer() [0x60e2a8] std::_Function_handler<void (std::shared_ptr<toolkit::Buffer> const&, sockaddr*, int), toolkit::TcpServer::onAcceptConnection(std::shared_ptr<toolkit::Socket> const&)::{lambda(std::shared_ptr<toolkit::Buffer> const&, sockaddr*, int)#1}>::_M_invoke(std::_Any_data const&, std::shared_ptr<toolkit::Buffer> const&, sockaddr*&&, int&&) TcpServer.cpp:? [13]: ./MediaServer(_ZN7toolkit6Socket6onReadERKSt10shared_ptrINS_6SockFDEEb+0x1a8) [0x603298] toolkit::Socket::onRead(std::shared_ptr<toolkit::SockFD> const&, bool) ??:? [14]: ./MediaServer() [0x60533f] std::_Function_handler<void (int), toolkit::Socket::attachEvent(std::shared_ptr<toolkit::SockFD> const&)::{lambda(int)#1}>::_M_invoke(std::_Any_data const&, int&&) Socket.cpp:? [15]: ./MediaServer(_ZN7toolkit11EventPoller7runLoopEbb+0x2b7) [0x621b07] toolkit::EventPoller::runLoop(bool, bool) ??:? [16]: /usr/lib64/libstdc++.so.6(+0xbc5ff) [0x7f143bbf55ff] ?? ??:0 [17]: /usr/lib64/libpthread.so.0(+0x8f1b) [0x7f143bccaf1b] ?? ??:0 [18]: /usr/lib64/libc.so.6(clone+0x40) [0x7f143b8db1c0] ?? ??:0 2025-08-22 17:30:52.570 W MediaServer[1270120-MediaServer] System.cpp:137 startDaemon | 子进程退出 2025-08-22 17:30:55.570 D MediaServer[1270120-MediaServer] System.cpp:127 startDaemon | 启动子进程:1272485 2025-08-22 17:30:55.571 I MediaServer[1272485-MediaServer] System.cpp:159 systemSetup | core文件大小设置为:18446744073709551615 2025-08-22 17:30:55.571 I MediaServer[1272485-MediaServer] System.cpp:168 systemSetup | 文件最大描述符个数设置为:524288 2025-08-22 17:30:55.571 I MediaServer[1272485-MediaServer] main.cpp:230 start_main | ZLMediaKit(git hash:/,branch:,build time:2025-08-14T16:01:23) 2025-08-22 17:30:55.604 D MediaServer[1272485-MediaServer] SSLBox.cpp:174 setContext | Add certificate of: default.zlmediakit.com 2025-08-22 17:30:55.604 D MediaServer[1272485-stamp thread] util.cpp:366 operator() | Stamp thread started 2025-08-22 17:30:55.620 I MediaServer[1272485-MediaServer] EventPoller.cpp:466 EventPollerPool | EventPoller created size: 32 2025-08-22 17:30:55.621 I MediaServer[1272485-MediaServer] TcpServer.cpp:200 start_l | TCP server listening on [::]: 554 2025-08-22 17:30:55.621 I MediaServer[1272485-MediaServer] TcpServer.cpp:200 start_l | TCP server listening on [::]: 1935 2025-08-22 17:30:55.622 I MediaServer[1272485-MediaServer] TcpServer.cpp:200 start_l | TCP server listening on [::]: 19350 2025-08-22 17:30:55.622 I MediaServer[1272485-MediaServer] TcpServer.cpp:200 start_l | TCP server listening on [::]: 8003 2025-08-22 17:30:55.623 I MediaServer[1272485-MediaServer] TcpServer.cpp:200 start_l | TCP server listening on [::]: 1443 2025-08-22 17:30:55.624 I MediaServer[1272485-MediaServer] UdpServer.cpp:104 start_l | UDP server bind to [::]: 8000 2025-08-22 17:30:55.625 I MediaServer[1272485-MediaServer] TcpServer.cpp:200 start_l | TCP server listening on [::]: 8000 2025-08-22 17:30:55.625 I MediaServer[1272485-MediaServer] UdpServer.cpp:104 start_l | UDP server bind to [::]: 9000 2025-08-22 17:30:55.625 I MediaServer[1272485-MediaServer] main.cpp:370 start_main | 已启动http api 接口 2025-08-22 17:30:55.626 I MediaServer[1272485-MediaServer] main.cpp:372 start_main | 已启动http hook 接口 2025-08-22 17:30:55.636 T MediaServer[1272485-event poller 1] HttpSession.cpp:27 HttpSession | 1-106(172.20.8.7:50610) 2025-08-22 17:30:55.637 D MediaServer[1272485-event poller 1] WebApi.cpp:250 http api debug | # request: POST /index/api/setServerConfig # header: Accept-Encoding : gzip Connection : close Content-Length : 1137 Content-Type : application/x-www-form-urlencoded Host : 172.20.8.7:8003 User-Agent : okhttp/4.9.0 # content: secret=035c73f7-bb6b-4889-a715-d9eb2d1925jl&hook.on_stream_changed=http%3A%2F%2F172.20.8.7%3A18087%2Fstream-server%2Findex%2Fhook%2Fon_stream_changed&hook.on_flow_report=&hook.on_publish=http%3A%2F%2F172.20.8.7%3A18087%2Fstream-server%2Findex%2Fhook%2Fon_publish&ffmpeg.cmd=%25s%20-fflags%20nobuffer%20-i%20%25s%20-c%3Aa%20aac%20-strict%20-2%20-ar%2044100%20-ab%2048k%20-c%3Av%20libx264%20%20-f%20flv%20%25s&hook.enable=1&hook.on_http_access=&hook.on_stream_not_found=http%3A%2F%2F172.20.8.7%3A18087%2Fstream-server%2Findex%2Fhook%2Fon_stream_not_found&rtp_proxy.port_range=30000-38700&hook.on_record_mp4=http%3A%2F%2F127.0.0.1%3A18081%2Fstream-storage%2Fapi%2Frecord%2Fon_record_mp4&hook.on_stream_none_reader=http%3A%2F%2F172.20.8.7%3A18087%2Fstream-server%2Findex%2Fhook%2Fon_stream_none_reader&hook.on_shell_login=http%3A%2F%2F172.20.8.7%3A18087%2Fstream-server%2Findex%2Fhook%2Fon_shell_login&hook.on_play=http%3A%2F%2F172.20.8.7%3A18087%2Fstream-server%2Findex%2Fhook%2Fon_play&api.secret=035c73f7-bb6b-4889-a715-d9eb2d1925jl&hook.on_server_started=http%3A%2F%2F172.20.8.7%3A18087%2Fstream-server%2Findex%2Fhook%2Fon_server_started # response: { "changed" : 0, "code" : 0 } 2025-08-22 17:30:55.637 T MediaServer[1272485-event poller 1] HttpSession.cpp:119 onError | 1-106(172.20.8.7:50610) close connection after send http body completed. 2025-08-22 17:30:55.637 T MediaServer[1272485-event poller 1] HttpSession.cpp:33 ~HttpSession | 1-106(172.20.8.7:50610) 2025-08-22 17:31:00.001 T MediaServer[1272485-event poller 1] HttpSession.cpp:27 HttpSession | 2-105(172.20.8.7:50624) 分析获取直播流时报错
最新发布
08-23
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值