外賣等實(shí)踐項(xiàng)目問題記錄(1)
ThreadLocal內(nèi)存泄漏問題
最近在復(fù)盤重新黑馬還有尚硅谷一些項(xiàng)目,發(fā)現(xiàn)我們?cè)谑褂毛@取當(dāng)前用戶id這項(xiàng)功能的時(shí)候主要是使用ThreadLocal來進(jìn)行存取
定義:
public class BaseContext { public static ThreadLocal<Long> threadLocal = new ThreadLocal<>(); public static void setCurrentId(Long id) { threadLocal.set(id); } public static Long getCurrentId() { return threadLocal.get(); } public static void removeCurrentId() { threadLocal.remove(); } }
存入:
/** * jwt令牌校驗(yàn)的攔截器 */ @Component @Slf4j public class JwtTokenUserInterceptor implements HandlerInterceptor { @Autowired private JwtProperties jwtProperties; public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //判斷當(dāng)前攔截到的是Controller的方法還是其他資源 if (!(handler instanceof HandlerMethod)) { //當(dāng)前攔截到的不是動(dòng)態(tài)方法,直接放行 return true; } //1、從請(qǐng)求頭中獲取令牌 String token = request.getHeader(jwtProperties.getUserTokenName()); //2、校驗(yàn)令牌 try { log.info("jwt校驗(yàn):{}", token); Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token); Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString()); log.info("當(dāng)前用戶的id:", userId); BaseContext.setCurrentId(userId); //3、通過,放行 return true; } catch (Exception ex) { //4、不通過,響應(yīng)401狀態(tài)碼 response.setStatus(401); return false; } } }
獲?。?/strong>
//清空當(dāng)前用戶的購(gòu)物車數(shù)據(jù) shoppingCartMapper.deleteByUserId(BaseContext.getCurrentId());
但我們知道ThreadLocal是存在內(nèi)存泄漏問題的
原因分析
ThreadLocal的工作原理 ThreadLocal通過為每個(gè)線程維護(hù)一個(gè)獨(dú)立的變量副本,實(shí)現(xiàn)了線程間的數(shù)據(jù)隔離。具體來說,ThreadLocal的值存儲(chǔ)在Thread對(duì)象的ThreadLocalMap中,鍵是ThreadLocal實(shí)例,值是存儲(chǔ)的數(shù)據(jù)。
內(nèi)存泄漏的可能性
如果ThreadLocal變量沒有被正確清理(調(diào)用remove()方法),即使ThreadLocal實(shí)例本身被回收,ThreadLocalMap中的鍵(弱引用)可能已經(jīng)被回收,但值仍然存在,導(dǎo)致無法釋放內(nèi)存。這種情況尤其容易發(fā)生在使用線程池的場(chǎng)景中,因?yàn)榫€程池中的線程是復(fù)用的,線程的生命周期很長(zhǎng)。
我們整個(gè)項(xiàng)目里面只用到了存取ThreadLocal中的線程id,卻并沒有在方法結(jié)束后對(duì)ThreadLocal進(jìn)行remove這樣會(huì)造成一定的風(fēng)險(xiǎn)。
如果線程池中的線程被復(fù)用,而ThreadLocal沒有及時(shí)清理,可能會(huì)導(dǎo)致數(shù)據(jù)污染。例如,前一個(gè)請(qǐng)求的用戶ID殘留到了下一個(gè)請(qǐng)求中。
解決方案
在本項(xiàng)目中,我們可以修改原本的攔截器,在請(qǐng)求結(jié)束時(shí)統(tǒng)一清理ThreadLocal中的數(shù)據(jù)。例如:
@Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { log.info("請(qǐng)求結(jié)束,執(zhí)行afterCompletion方法..."); // 在這里編寫請(qǐng)求結(jié)束后的邏輯,例如清理ThreadLocal數(shù)據(jù) BaseContext.removeCurrentId(); // 清理當(dāng)前用戶ID }
使用afterCompletion方法
afterCompletion的作用 afterCompletion方法在請(qǐng)求處理完成(包括視圖渲染)后執(zhí)行,無論請(qǐng)求是否成功或拋出異常。它適合用于清理資源、記錄日志等操作。
不知道我這樣做的是否正確,歡迎各位大佬指教!
#實(shí)習(xí)##蒼穹外賣項(xiàng)目包裝#