您好,登錄后才能下訂單哦!
這篇文章將為大家詳細講解有關springboot如何使用ThreadLocal,小編覺得挺實用的,因此分享給大家做個參考,希望大家閱讀完這篇文章后可以有所收獲。
在spring boot中使用ThreadLocal實現線程封閉的實例。
首先創建一個包含ThreadLocal成員變量的實例:
public class RequestHolder { private final static ThreadLocal<Long> requestHolder = new ThreadLocal<>(); public static void add(Long id) { requestHolder.set(id); } public static Long getId() { return requestHolder.get(); } public static void remove() { requestHolder.remove(); } }
編寫一個Controller類,請求該類的test()方法獲取ThreadLocal中存儲的id:
import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseBody; @Controller @RequestMapping("/threadLocal") public class ThreadLocalController { @RequestMapping("/test") @ResponseBody public Long test() { return RequestHolder.getId(); } }
編寫過濾器,在請求到達Servlet之前(請求->tomcat容器->filter->servlet->inteceptor->controller),將當前線程的id添加到ThreadLocal中:
import com.mmall.concurrency.example.threadLocal.RequestHolder; import lombok.extern.slf4j.Slf4j; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import java.io.IOException; @Slf4j public class HttpFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; log.info("do filter, {}, {}", Thread.currentThread().getId(), request.getServletPath()); //在ThreadLocal中添加當前線程的id RequestHolder.add(Thread.currentThread().getId()); filterChain.doFilter(servletRequest, servletResponse); } @Override public void destroy() { } }
編寫攔截器,當請求處理完成后(從Controller返回后),清除ThreadLocal中的id,避免內存泄漏。
import com.mmall.concurrency.example.threadLocal.RequestHolder; import lombok.extern.slf4j.Slf4j; import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @Slf4j public class HttpInterceptor extends HandlerInterceptorAdapter { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { log.info("preHandle"); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { log.info("ThreadId:"+RequestHolder.getId()); RequestHolder.remove(); log.info("afterCompletion"); return; } }
最后,我們需要在spring boot啟動類上注冊我們定義的Filer及Inteceptor,并設置攔截路徑。
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; @SpringBootApplication public class ConcurrencyApplication extends WebMvcConfigurerAdapter{ public static void main(String[] args) { SpringApplication.run(ConcurrencyApplication.class, args); } @Bean public FilterRegistrationBean httpFilter() { FilterRegistrationBean registrationBean = new FilterRegistrationBean(); registrationBean.setFilter(new HttpFilter()); registrationBean.addUrlPatterns("/threadLocal/*"); return registrationBean; } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new HttpInterceptor()).addPathPatterns("/**"); } }
在瀏覽器或者postman中輸入http://localhost:8080/threadLocal/test
觀察輸出結果:
2018-11-09 11:16:51.287 INFO 34076 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2018-11-09 11:16:51.290 INFO 34076 --- [ main] c.m.concurrency.ConcurrencyApplication : Started ConcurrencyApplication in 1.718 seconds (JVM running for 2.132)
2018-11-09 11:17:03.060 INFO 34076 --- [nio-8080-exec-2] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring FrameworkServlet 'dispatcherServlet'
2018-11-09 11:17:03.060 INFO 34076 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization started
2018-11-09 11:17:03.072 INFO 34076 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization completed in 12 ms
2018-11-09 11:17:03.078 INFO 34076 --- [nio-8080-exec-2] com.mmall.concurrency.HttpFilter : do filter, 29, /threadLocal/test
2018-11-09 11:17:03.090 INFO 34076 --- [nio-8080-exec-2] com.mmall.concurrency.HttpInterceptor : preHandle
2018-11-09 11:17:03.124 INFO 34076 --- [nio-8080-exec-2] com.mmall.concurrency.HttpInterceptor : ThreadId:29
2018-11-09 11:17:03.124 INFO 34076 --- [nio-8080-exec-2] com.mmall.concurrency.HttpInterceptor : afterCompletion
從打印的日志結果中,我們看到在Filter中我們將當前線程的id 29添加到了ThreadLocal中,隨后在Inteceptor中打印并刪除了id。
ThreadLocal 適用于變量在線程間隔離,而在方法或類間共享的場景。現在在Springboot中我做如下場景的使用:
使用 Spring Boot 創建一個 Web 應用程序,使用 ThreadLocal 存放一個 Integer 的值,來暫且代表需要在線程中保存的用戶信息,這個值初始是 null。在業務邏輯中,我先從 ThreadLocal 獲取一次值,然后把外部傳入的參數設置到 ThreadLocal 中,來模擬從當前上下文獲取到用戶信息的邏輯,隨后再獲取一次值,最后輸出兩次獲得的值和線程名稱。
@RestController public class threadLocal { private ThreadLocal<Integer> currentUser = ThreadLocal.withInitial(() -> null); @RequestMapping("wrong") public Map wrong(@RequestParam("userId") Integer userId) { //設置用戶信息之前先查詢一次ThreadLocal中的用戶信息 String before = Thread.currentThread().getName() + ":" + currentUser.get(); //設置用戶信息到ThreadLocal currentUser.set(userId); //設置用戶信息之后再查詢一次ThreadLocal中的用戶信息 String after = Thread.currentThread().getName() + ":" + currentUser.get(); //匯總輸出兩次查詢結果 Map result = new HashMap(); result.put("before", before); result.put("after", after); return result; } }
為了讓問題快速的重現,我在配置文件中設置一下 Tomcat 的參數,把工作線程池最大線程數設置為 1,這樣始終是同一個線程在處理請求:
server.tomcat.max-threads=1
運行程序后先讓用戶 1 來請求接口,可以看到第一和第二次獲取到用戶 ID 分別是 null 和 1,符合預期:隨后用戶 2 來請求接口,這次就出現了 Bug,第一和第二次獲取到用戶 ID 分別是 1 和 2,顯然第一次獲取到了用戶 1 的信息,原因就是 Tomcat 的線程池重用了線程。
在 Tomcat 這種 Web 服務器下跑的業務代碼,本來就運行在一個多線程環境中,并不能認為沒有顯式開啟多線程就不會有線程安全問題,所以使用類似 ThreadLocal 工具來存放一些數據時,需要特別注意在代碼運行完后,顯式地去清空設置的數據。如果在代碼中使用了自定義的線程池,也同樣會遇到這個問題。修改后代碼如下:
@RestController public class threadLocal { private ThreadLocal<Integer> currentUser = ThreadLocal.withInitial(() -> null); @RequestMapping("wrong") public Map wrong(@RequestParam("userId") Integer userId) { //設置用戶信息之前先查詢一次ThreadLocal中的用戶信息 String before = Thread.currentThread().getName() + ":" + currentUser.get(); //設置用戶信息到ThreadLocal currentUser.set(userId); try { //設置用戶信息之后再查詢一次ThreadLocal中的用戶信息 String after = Thread.currentThread().getName() + ":" + currentUser.get(); //匯總輸出兩次查詢結果 Map result = new HashMap(); result.put("before", before); result.put("after", after); return result; } finally { //增加移除處理 currentUser.remove(); } } }
關于“springboot如何使用ThreadLocal”這篇文章就分享到這里了,希望以上內容可以對大家有一定的幫助,使各位可以學到更多知識,如果覺得文章不錯,請把它分享出去讓更多的人看到。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。