动态

详情 返回 返回

ThreadLocal 為何能取代 Session? - 动态 详情

本文探討了 Session 的原理及其與 Cookie 和 Token 的區別。Session 通過服務器端存儲 Session ID 來識別用户狀態,涵蓋創建、存儲、維護和銷燬的完整流程。與 Cookie 和 Token 比較,分析了它們在存儲、安全性、生命週期和應用場景上的差異。此外,Session 在高併發場景下可能面臨查找效率、代碼複雜性、線程安全、網絡傳輸和性能等問題。
為解決這些問題,提出使用 ThreadLocal 替代傳統 Session。ThreadLocal 可以減少資源開銷、提升代碼質量、確保線程安全、減輕傳輸負擔,並有效應對高併發挑戰。文中還介紹了 ThreadLocal 的原理及內存泄漏的解決方法。

一、什麼是 Session?

Session 是一種在服務器端保存用户狀態信息的機制。每個用户在與服務器建立會話時,服務器會為其創建一個唯一的 Session ID,並將該 ID 存儲在服務器端的會話存儲中(例如內存、數據庫或文件)。客户端通過 Cookie 或 URL 參數將 Session ID 發送到服務器,以便服務器可以識別用户並恢復其狀態。

二、Session 的工作原理

Session 的工作原理通常包含以下幾個步驟:

創建 Session:當用户第一次訪問網站時,服務器會為該用户創建一個 Session,並生成一個唯一的標識符,稱為 Session ID。

存儲 Session ID:服務器會通過 Cookie 或 URL 參數將 Session ID 發送給用户的瀏覽器。

維護 Session:瀏覽器會在每次請求時,將 Session ID 發送給服務器,服務器根據這個 ID 找到對應的 Session,進而識別用户的狀態。

銷燬 Session:當用户註銷、關閉瀏覽器或 Session 過期時,Session 將被銷燬,服務器不再保存用户的狀態信息。

三、Session 的使用和常用方法

  • Session 作用域:擁有存儲數據的空間,作用範圍是一次會話有效,一次會話是使用同一瀏覽器發送的多次請求。一旦瀏覽器關閉,則結束會話。
  • 可以將數據存入 Session 中,在一次會話的任意位置進行獲取,可傳遞任何數據(基本數據類型、對象、集合、數組)。
  • resquest.getSession():得到請求遊覽器(客户端)對應的 session。如果沒有,那麼就創建應該新的 session。如果有那麼就返回對應的 session。
  • setAttribute(String s, Object o):在 session 存放屬性
  • getAttribute(String s):從 session 中得到 s 所對應的屬性
  • removeAttribute(String s):從 session 中刪除 s 對應的屬性
  • getId():得到 session 所對應的 id
  • invalidate():使 session 立即無效
  • setMaxInactiveInterval(int i):設置 session 最大的有效時間。
    注意,這個有效時間是兩次訪問服務器所間隔的最大時間,如果超過最大的有效時間,那麼這個 session 就失效了。
public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) throws Exception {
        HttpSession session = request.getSession();
        Object token =session.getAttribute("token");

        if (token == null) {
            response.sendRedirect("/admin/toLogin");
            return false;
        }

        return true;
    }

    @Override
    public void afterCompletion(final HttpServletRequest request, final HttpServletResponse response, final Object handler, final Exception ex) throws Exception {

    }
}

四、Session、Cookie 和 Token 區別?

1. Session

Session是一種在服務器端保存用户狀態信息的機制。每個用户在與服務器建立會話時,服務器會為其創建一個唯一的 Session ID,並將該 ID 存儲在服務器端的會話存儲中(例如內存、數據庫或文件)。客户端通過 Cookie 或 URL 參數將 Session ID 發送到服務器,以便服務器可以識別用户並恢復其狀態。

  • 存儲位置:服務器端。
  • 安全性:較高,因為敏感數據存儲在服務器端。
  • 生命週期:通常在用户關閉瀏覽器或會話超時後失效。
  • 使用場景:適用於需要在服務器端保持用户會話狀態的場景,例如購物車、用户登錄狀態等。

2. Cookie

Cookie是一種在客户端(通常是瀏覽器)存儲數據的小文件。服務器通過 HTTP 響應頭 Set-Cookie 將 Cookie 發送到客户端,客户端會在後續請求中自動包含這些 Cookie。Cookie 可以用於存儲用户的會話信息、偏好設置等。

  • 存儲位置:客户端(瀏覽器)。
  • 安全性:較低,容易被竊取和篡改。可以通過設置 HttpOnly 和 Secure 屬性提高安全性。
  • 生命週期:可以設置為會話 Cookie(瀏覽器關閉後失效)或持久 Cookie(設置過期時間)。
  • 使用場景:適用於需要在客户端存儲少量數據的場景,例如用户偏好設置、跟蹤用户活動等。

3. Token

Token是一種用於身份驗證的字符串,通常由服務器生成併發送給客户端。Token 常用於無狀態的身份驗證機制,如 JSON Web Token (JWT)。客户端在每次請求時將 Token 發送到服務器,服務器通過驗證 Token 來識別用户身份。

  • 存儲位置:客户端(可以存儲在 Cookie、LocalStorage 或 SessionStorage 中)。
  • 安全性:較高,Token 通常包含簽名和加密信息,可以防止篡改。JWT 中的簽名可以驗證 Token 的完整性和真實性。
  • 生命週期:可以設置過期時間,通常需要定期刷新 Token(如使用 Refresh Token)。
  • 使用場景:適用於分佈式系統和微服務架構中無狀態的身份驗證,特別是需要跨域的場景。
    總結
  • Session:服務器端存儲用户會話狀態,通過 Session ID 識別用户。適用於需要在服務器端保持用户狀態的場景。
  • Cookie:客户端存儲少量數據,可以用於會話管理和用户偏好設置。安全性較低,需要注意保護敏感信息。
  • Token:客户端存儲的身份驗證字符串,常用於無狀態的身份驗證機制。適用於分佈式系統和需要跨域的場景。

五、為什麼不能依賴 Session 存儲用户信息?

在項目設計和開發中,Session 是用於存儲用户會話信息的重要機制。用户在登錄後,通常會將用户信息存儲在 Session 中,以便在後續的請求中可以訪問到這些信息。

如果想在其它地方獲取 session 中的用户信息,我們需要先獲取 HttpServletRequest,再通過 request.getSession 得到 HttpSession。例如下代碼:

public static User getSessionUser(HttpServletRequest request)
    {
        if(request.getSession().getAttribute( "sessionuser" ) != null)
        {
            return (User)request.getSession().getAttribute( "sessionuser" );
        }
        return null;
    }

然而,直接通過 Session 來訪問用户信息在高併發和大規模系統中會帶來一些問題和挑戰。

1. Session 查找的開銷

Session 通常存儲在服務器的內存中,儘管它提供了快速訪問機制,但每次從 Session 中查找用户信息也會產生開銷。

  • 內存存儲和查找:每次調用 request.getSession().getAttribute("sessionuser"),實際上是向服務器的內存或持久化存儲查詢信息,尤其在多個請求併發時,頻繁訪問 Session 會對性能產生影響。
  • Session 存儲機制:對於大規模分佈式系統,Session 有可能存儲在分佈式緩存中(如 Redis、Memcached 等)。這就引入了網絡請求的延遲,進一步加劇了性能問題。

2. 代碼的可讀性和維護性

直接在代碼中獲取 Session 中的信息,通常會使代碼顯得簡單,但實際上這種方式的可維護性和可讀性差。

  • 重複代碼:如你提到的代碼示例,每次都需要調用 request.getSession() 和 request.getSession().getAttribute(),這種方式會導致代碼重複、冗長,且當項目中有多個地方需要獲取 Session 時,可能會導致維護困難。
  • 封裝性差:直接操作 Session 使得代碼不具備良好的封裝性。每個地方都直接依賴於 HttpServletRequest 和 Session,這違背了代碼解耦的原則。

3. 線程安全問題

Session 在多線程環境下可能會導致線程安全問題。

  • 併發訪問:在高併發的環境中,多個請求可能同時訪問或修改 Session 中的數據。如果 Session 對象不是線程安全的,就需要額外的同步機制來避免數據衝突或競爭。
  • 同步開銷:為了確保線程安全,可能需要在代碼中加入同步鎖等機制,這不僅增加了開發複雜性,也可能帶來額外的性能開銷。使用同步機制可能導致線程阻塞,影響性能。

4. 不必要的網絡傳輸

在分佈式系統中,Session 通常需要在不同的服務器之間同步,尤其是在負載均衡的場景下,用户的請求可能被分配到不同的服務器。

  • Session 同步問題:如果使用的是基於內存的 Session 存儲,當一個請求的 Session 數據不在當前處理請求的服務器上時,可能需要訪問其他服務器的 Session。這種跨服務器的通信會引入網絡延遲,增加網絡傳輸的開銷。
  • 負載均衡影響:多個服務器共享 Session 時,需要實現 Session 複製或共享機制(如 sticky session、Redis 共享 Session 等)。這些機制通常會增加額外的網絡傳輸成本。

5. 面對高併發場景

在高併發的場景下,Session 的頻繁訪問和更新可能會導致性能瓶頸,特別是在分佈式環境下。

  • Session 頻繁讀寫:在高併發的環境下,多個請求可能會頻繁地訪問 Session。這不僅會增加服務器的內存負擔,還可能導致網絡傳輸延遲,進而影響整體系統性能。
  • Session 集中存儲壓力:如果所有用户信息都存儲在一個集中的 Session 存儲中,隨着用户量的增加,這種集中存儲可能會成為瓶頸,特別是在高負載時,訪問 Session 會變得緩慢,影響響應時間。

六、使用 ThreadLocal 替代 Session 存儲用户信息

1、減少 session 查找的開銷

session 一般都是存儲在服務器端的,如果每次都要從 session 中查找用户信息,那都要去服務器的內存或存儲系統去查找對應的 session 對象,這肯定會帶來一定的性能開銷。

那你用 TheadLocal 就直接把對象放在當前線程裏面,想用直接在當前線程找,效率肯定就會高很多。

2、提高代碼的可讀性和維護性

直接在 session 裏面拿用户信息,聽着很直接很簡單,實則需要調用多個層級才能找到,代碼特別複雜且冗餘,直接用 TheadLocal 拿用户信息肯定就更方便且直觀。

3、線程安全

ThreadLocal 為每個線程提供了一個獨立的變量副本,這意味着在多線程環境下,每個線程都可以安全地訪問自己的用户信息,而不會與其他線程發生衝突。如果直接操作 Session,需要額外的同步機制來保證線程安全。

4、減少不必要的網絡傳輸

當你的用户量特別多,你不先拿到 TheadLocal 裏面,那 session 裏面的用户信息會越來越多,Session 可能需要在多個服務器之間同步,這會增加網絡傳輸的開銷。

5、面對高併發場景

在高併發的應用場景下,頻繁地訪問 Session 可能會導致性能瓶頸。而 ThreadLocal 由於其線程局部性,可以提供更好的性能表現。

七、ThreadLocal 原理

ThreadLocal 的實現依賴於每個線程內部維護的一個 ThreadLocalMap 對象。每個線程都有自己的 ThreadLocalMap,而 ThreadLocalMap 中存儲了所有 ThreadLocal 變量及其對應的值。

主要組成部分

  1. ThreadLocal 類:提供了 set()、get()、remove()等方法,用於操作線程局部變量。
  2. ThreadLocalMap 類:是 ThreadLocal 的內部靜態類,用於存儲 ThreadLocal 變量及其值。
  3. Thread 類:每個線程內部都有一個 ThreadLocalMap 實例。

工作機制

  1. 創建 ThreadLocal 變量:當創建一個 ThreadLocal 變量時,實際上並沒有分配存儲空間。
  2. 獲取值 (get()方法):當調用 get()方法時,當前線程會通過自己的 ThreadLocalMap 獲取 ThreadLocal 變量的值。如果不存在,則調用 initialValue()方法獲取初始值。
  3. 設置值 (set()方法):當調用 set()方法時,當前線程會通過自己的 ThreadLocalMap 設置 ThreadLocal 變量的值。
  4. 刪除值 (remove()方法):當調用 remove()方法時,當前線程會通過自己的 ThreadLocalMap 刪除 ThreadLocal 變量的值。

使用 ThreadLocal 的方法

一般使用 ThreadLocal 的方法,就是建立一個 ThreadLocal 的工具類,在存儲和使用用户信息時,能方便地調用。

/**
 * @ Author:
 * @ CreateTime: 2025-01-07
 * @ Description: 登錄上下文對象
 * @ Version: 1.0
 */

public class ThreadLocal {
    private static final InheritableThreadLocal<Map<String, Object>> THREAD_LOCAL
            = new InheritableThreadLocal<>();

    public static void set(String key, Object val) {
        Map<String, Object> map = getThreadLocalMap();
        map.put(key, val);
    }

    public static Object get(String key){
        Map<String, Object> threadLocalMap = getThreadLocalMap();
        return threadLocalMap.get(key);
    }

    public static String getLoginId(){
        return (String) getThreadLocalMap().get("loginId");
    }

    public static void remove(){
        THREAD_LOCAL.remove();
    }

    public static Map<String, Object> getThreadLocalMap() {
        Map<String, Object> map = THREAD_LOCAL.get();
        if (Objects.isNull(map)) {
            map = new ConcurrentHashMap<>();
            THREAD_LOCAL.set(map);
        }
        return map;
    }
}

八、ThreadLocal 的內存泄漏問題

在使用 ThreadLocal 時,要注意 ThreadLocal 地內存泄漏問題。

ThreadLocal 的內存泄漏的原因

ThreadLocal 的內存泄漏問題主要源於 ThreadLocalMap 中使用的弱引用(WeakReference)機制和線程生命週期管理不當這兩個原因。

  • 弱引用機制:
    ThreadLocalMap 使用 Entry 類來存儲鍵值對,其中鍵是 ThreadLocal 對象的弱引用(WeakReference<ThreadLocal<?>>),值是實際存儲的數據。當 ThreadLocal 對象被垃圾回收時,弱引用會被清除,但 Entry 中的值對象仍然存在,導致內存無法及時釋放。
  • 線程生命週期:
    在一些長生命週期的線程(如線程池中的線程)中,如果不顯式地清除 ThreadLocal 變量,ThreadLocalMap 中的值對象會一直存在,導致內存泄漏。線程池中的線程不會在任務完成後立即銷燬,而是會被複用。如果 ThreadLocal 變量沒有被顯式清除,下一個使用該線程的任務可能會意外地訪問到上一個任務遺留的數據。

如何避免 ThreadLocal 的內存泄漏?

最直接和有效的方法是在使用完 ThreadLocal 變量後,顯式調用 remove()方法清除變量。

public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(final HttpServletRequest request, final HttpServletResponse response, final Object handler) throws Exception {
        String loginId = request.getHeader("loginId");
        if (loginId != null && loginId != "") {
            ThreadLocal.set("loginId", loginId);
        }
        return true;
    }

    @Override
    public void afterCompletion(final HttpServletRequest request, final HttpServletResponse response, final Object handler, final Exception ex) throws Exception {
        ThreadLocal.remove();
    }
}

就業陪跑訓練營學員投稿

歡迎關注 ❤

我們搞了一個免費的面試真題共享羣,互通有無,一起刷題進步。

沒準能讓你能刷到自己意向公司的最新面試題呢。

感興趣的朋友們可以加我微信:wangzhongyang1993,備註:思否面試羣。

user avatar u_16502039 头像 lu_lu 头像 chengxy 头像 wxweven 头像 easynvr 头像 manongtuwei 头像 litongjava 头像 xiangchujiadepubu 头像 nebulagraph 头像 yizhidanshendetielian 头像 macrozheng 头像 headofhouchang 头像
点赞 12 用户, 点赞了这篇动态!
点赞

Add a new 评论

Some HTML is okay.