ThreadLocal可以讓線程擁有本地變量,在web環(huán)境中,為了方便代碼解耦,我們通常用它來保存上下文信息,然后用一個util類提供訪問入口,從controller層到service層可以很方便的獲取上下文。下面我們通過代碼來研究一下ThreadLocal。
新建一個ThreadContext類,用于保存線程上下文信息
public class ThreadContext { private static ThreadLocaluserResource = new ThreadLocal (); public static UserObj getUser() { return userResource.get(); } public static void bindUser(UserObj user) { userResource.set(user); } public static UserObj unbindUser() { UserObj obj = userResource.get(); userResource.remove(); return obj; } }
新建一個sessionFilter ,用來操作線程變量
@Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) servletRequest; try { // 假設(shè)這里是從cookie拿token信息, 調(diào)用服務(wù)/或者從緩存查詢用戶信息 // 為了避免后續(xù)邏輯中多次查詢/請求緩存服務(wù)器, 這里拿到user后放到線程本地變量中 UserObj user = ThreadContext.getUser(); // 如果當(dāng)前線程中沒有綁定user對象,那么綁定一個新的user if (user == null) { ThreadContext.bindUser(new UserObj("usertest")); } filterChain.doFilter(servletRequest, servletResponse); } finally { // ThreadLocal的生命周期不等于一次request請求的生命周期 // 每個request請求的響應(yīng)是tomcat從線程池中分配的線程, 線程會被下個請求復(fù)用. // 所以請求結(jié)束后必須刪除線程本地變量 // ThreadContext.unbindUser(); } }
新建UserUtils工具類
/** * 配合SessionFilter使用,從上下文中取user信息 */ public class UserUtils { public static UserObj getCurrentUser() { return ThreadContext.getUser(); } }
新建一個servlet測試
public class HelloworldServlet extends HttpServlet { private static Logger logger = LoggerFactory.getLogger(HelloworldServlet.class); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { UserObj user = UserUtils.getCurrentUser(); logger.info(user.getName() + user.hashCode()); super.doGet(req, resp); } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { super.doGet(req, resp); } }
循環(huán)請求servlet,控制臺顯示結(jié)果如下。可以發(fā)現(xiàn)tomcat線程池的初始大小是10個,后面的請求復(fù)用了前面的線程,ThreadContext中的user對象的hashcode也一樣。
2016-11-29 17:21:35.975 INFO 36672 --- [nio-8080-exec-2] com.zallds.xy.servlet.HelloworldServlet : usertest818202673 2016-11-29 17:21:38.923 INFO 36672 --- [nio-8080-exec-3] com.zallds.xy.servlet.HelloworldServlet : usertest1582591702 2016-11-29 17:21:45.810 INFO 36672 --- [nio-8080-exec-4] com.zallds.xy.servlet.HelloworldServlet : usertest55755037 2016-11-29 17:21:46.773 INFO 36672 --- [nio-8080-exec-5] com.zallds.xy.servlet.HelloworldServlet : usertest1495466807 2016-11-29 17:21:47.345 INFO 36672 --- [nio-8080-exec-6] com.zallds.xy.servlet.HelloworldServlet : usertest1149360245 2016-11-29 17:21:47.613 INFO 36672 --- [nio-8080-exec-7] com.zallds.xy.servlet.HelloworldServlet : usertest518375339 2016-11-29 17:21:47.837 INFO 36672 --- [nio-8080-exec-8] com.zallds.xy.servlet.HelloworldServlet : usertest92458992 2016-11-29 17:21:48.012 INFO 36672 --- [nio-8080-exec-9] com.zallds.xy.servlet.HelloworldServlet : usertest944867034 2016-11-29 17:21:48.199 INFO 36672 --- [io-8080-exec-10] com.zallds.xy.servlet.HelloworldServlet : usertest1410972809 2016-11-29 17:21:48.378 INFO 36672 --- [nio-8080-exec-1] com.zallds.xy.servlet.HelloworldServlet : usertest805332046 2016-11-29 17:21:48.552 INFO 36672 --- [nio-8080-exec-2] com.zallds.xy.servlet.HelloworldServlet : usertest818202673 2016-11-29 17:21:48.730 INFO 36672 --- [nio-8080-exec-3] com.zallds.xy.servlet.HelloworldServlet : usertest1582591702 2016-11-29 17:21:48.903 INFO 36672 --- [nio-8080-exec-4] com.zallds.xy.servlet.HelloworldServlet : usertest55755037 2016-11-29 17:21:49.072 INFO 36672 --- [nio-8080-exec-5] com.zallds.xy.servlet.HelloworldServlet : usertest1495466807 2016-11-29 17:21:49.247 INFO 36672 --- [nio-8080-exec-6] com.zallds.xy.servlet.HelloworldServlet : usertest1149360245 2016-11-29 17:21:49.402 INFO 36672 --- [nio-8080-exec-7] com.zallds.xy.servlet.HelloworldServlet : usertest518375339
去掉注釋// ThreadContext.unbindUser(); 重新請求,每次從ThreadLocal中拿到的user對象完全不一樣了。
2016-11-29 17:30:37.150 INFO 36903 --- [nio-8080-exec-1] com.zallds.xy.servlet.HelloworldServlet : usertest413138571 2016-11-29 17:30:42.932 INFO 36903 --- [nio-8080-exec-2] com.zallds.xy.servlet.HelloworldServlet : usertest1402191945 2016-11-29 17:30:43.124 INFO 36903 --- [nio-8080-exec-3] com.zallds.xy.servlet.HelloworldServlet : usertest1957579173 2016-11-29 17:30:43.313 INFO 36903 --- [nio-8080-exec-4] com.zallds.xy.servlet.HelloworldServlet : usertest1582591702 2016-11-29 17:30:43.501 INFO 36903 --- [nio-8080-exec-5] com.zallds.xy.servlet.HelloworldServlet : usertest1917479582 2016-11-29 17:30:43.679 INFO 36903 --- [nio-8080-exec-6] com.zallds.xy.servlet.HelloworldServlet : usertest772036767 2016-11-29 17:30:43.851 INFO 36903 --- [nio-8080-exec-7] com.zallds.xy.servlet.HelloworldServlet : usertest162020761 2016-11-29 17:30:44.024 INFO 36903 --- [nio-8080-exec-8] com.zallds.xy.servlet.HelloworldServlet : usertest682232950 2016-11-29 17:30:44.225 INFO 36903 --- [nio-8080-exec-9] com.zallds.xy.servlet.HelloworldServlet : usertest2140650341 2016-11-29 17:30:44.419 INFO 36903 --- [io-8080-exec-10] com.zallds.xy.servlet.HelloworldServlet : usertest1327601763 2016-11-29 17:30:44.593 INFO 36903 --- [nio-8080-exec-1] com.zallds.xy.servlet.HelloworldServlet : usertest647738411 2016-11-29 17:30:44.787 INFO 36903 --- [nio-8080-exec-2] com.zallds.xy.servlet.HelloworldServlet : usertest944867034 2016-11-29 17:30:45.045 INFO 36903 --- [nio-8080-exec-3] com.zallds.xy.servlet.HelloworldServlet : usertest1886154520 2016-11-29 17:30:45.317 INFO 36903 --- [nio-8080-exec-4] com.zallds.xy.servlet.HelloworldServlet : usertest1592904273 2016-11-29 17:30:46.380 INFO 36903 --- [nio-8080-exec-5] com.zallds.xy.servlet.HelloworldServlet : usertest1410972809 2016-11-29 17:30:46.524 INFO 36903 --- [nio-8080-exec-6] com.zallds.xy.servlet.HelloworldServlet : usertest1705570689 2016-11-29 17:30:46.692 INFO 36903 --- [nio-8080-exec-7] com.zallds.xy.servlet.HelloworldServlet : usertest1105134375 2016-11-29 17:30:46.802 INFO 36903 --- [nio-8080-exec-8] com.zallds.xy.servlet.HelloworldServlet : usertest407377722ThreadLocal子線程場景
需求新增, 需要在原有的業(yè)務(wù)邏輯中增加一個給用戶發(fā)送郵件的操作。發(fā)送郵件我們采用異步處理,新建一個線程來執(zhí)行。
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { UserObj user = UserUtils.getCurrentUser(); logger.info(user.getName() + user.hashCode()); SendEmailTask emailThread = new SendEmailTask(); new Thread(emailThread).start(); super.doGet(req, resp); } class SendEmailTask implements Runnable { @Override public void run() { UserObj user = UserUtils.getCurrentUser(); logger.info("子線程中:" + (user == null ? "user為null" : user.getName() + user.hashCode())); } }
主線程中創(chuàng)建異步線程,子線程中能拿到嗎?通過測試發(fā)現(xiàn)是不能的
2016-11-29 18:09:16.482 INFO 38092 --- [nio-8080-exec-2] com.zallds.xy.servlet.HelloworldServlet : usertest1425505918 2016-11-29 18:09:16.483 INFO 38092 --- [ Thread-4] com.zallds.xy.servlet.HelloworldServlet : 子線程中:user為null 2016-11-29 18:09:20.995 INFO 38092 --- [nio-8080-exec-3] com.zallds.xy.servlet.HelloworldServlet : usertest1280373552 2016-11-29 18:09:20.996 INFO 38092 --- [ Thread-5] com.zallds.xy.servlet.HelloworldServlet : 子線程中:user為null
子線程怎么拿到父線程的ThreadLocal數(shù)據(jù)?jdk給我們提供了解決辦法,ThreadLocal有一個子類InheritableThreadLocal,創(chuàng)建ThreadLocal時候我們采用InheritableThreadLocal類可以實(shí)現(xiàn)子線程獲取到父線程的本地變量。
private static ThreadLocaluserResource = new InheritableThreadLocal ();
然后子線程中就可以正常拿到user對象了
2016-11-29 19:07:01.518 INFO 39644 --- [nio-8080-exec-2] com.zallds.xy.servlet.HelloworldServlet : usertest495550128 2016-11-29 19:07:01.518 INFO 39644 --- [ Thread-4] com.zallds.xy.servlet.HelloworldServlet : 子線程中:usertest495550128 2016-11-29 19:07:05.839 INFO 39644 --- [nio-8080-exec-3] com.zallds.xy.servlet.HelloworldServlet : usertest1851717404 2016-11-29 19:07:05.840 INFO 39644 --- [ Thread-5] com.zallds.xy.servlet.HelloworldServlet : 子線程中:usertest1851717404ThreadLocal 子線程傳遞-線程池場景
當(dāng)我們執(zhí)行異步任務(wù)時,大多會采用線程池的機(jī)制(如Executor)。這樣就會存在一個問題,即使父線程已經(jīng)結(jié)束,子線程依然存在并被池化。這樣,線程池中的線程在下一次請求被執(zhí)行的時候,ThreadLocal的get()方法返回的將不是當(dāng)前線程中設(shè)定的變量,因?yàn)槌刂械摹白泳€程”根本不是當(dāng)前線程創(chuàng)建的,當(dāng)前線程設(shè)定的ThreadLocal變量也就無法傳遞給線程池中的線程。
我們修改一下發(fā)送郵件的代碼,改用線程池來實(shí)現(xiàn)。
2016-11-29 19:51:51.973 INFO 40937 --- [nio-8080-exec-1] com.zallds.xy.servlet.HelloworldServlet : usertest1417641261 2016-11-29 19:51:51.974 INFO 40937 --- [pool-1-thread-1] com.zallds.xy.servlet.HelloworldServlet : 子線程中:usertest1417641261 2016-11-29 19:51:55.746 INFO 40937 --- [nio-8080-exec-2] com.zallds.xy.servlet.HelloworldServlet : usertest1116537955 2016-11-29 19:51:55.746 INFO 40937 --- [pool-1-thread-1] com.zallds.xy.servlet.HelloworldServlet : 子線程中:usertest1417641261 2016-11-29 19:51:58.825 INFO 40937 --- [nio-8080-exec-3] com.zallds.xy.servlet.HelloworldServlet : usertest1489938856 2016-11-29 19:51:58.826 INFO 40937 --- [pool-1-thread-1] com.zallds.xy.servlet.HelloworldServlet : 子線程中:usertest1417641261
可以發(fā)現(xiàn)發(fā)送郵件的任務(wù)三次用的都是同一個線程[pool-1-thread-1],第一次子線程和父線程中的user對象相同,后面的“子線程”(前面提到過,后面的已經(jīng)不是子線程了)中的user對象都是和第一個父線程中的相同。
那么在線程池的場景下,怎么能讓“子線程”正常拿到父線程傳遞過來的變量呢?如果我們能在創(chuàng)建task的時候主動傳遞過去就好了。按照這個想法我們來實(shí)施一下。
繼續(xù)修改代碼
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { UserObj user = UserUtils.getCurrentUser(); logger.info(user.getName() + user.hashCode()); SendEmailTask emailThread = new SendEmailTask(); executor.execute(new UserRunnable(emailThread, user)); super.doGet(req, resp); } /** * 做一個wrapper, 將目標(biāo)任務(wù)做一層包裝, 在run方法中傳遞線程本地變量 */ class UserRunnable implements Runnable { /** * 目標(biāo)任務(wù)對象 */ Runnable runnable; /** * 要綁定的user對象 */ UserObj user; public UserRunnable(Runnable runnable, UserObj user) { this.runnable = runnable; this.user = user; } @Override public void run() { ThreadContext.bindUser(user); runnable.run(); ThreadContext.unbindUser(); } } class SendEmailTask implements Runnable { @Override public void run() { UserObj user = UserUtils.getCurrentUser(); logger.info("子線程中:" + (user == null ? "user為null" : user.getName() + user.hashCode())); } }
重新請求,得到我們想要的結(jié)果
2016-11-29 20:04:12.153 INFO 41258 --- [nio-8080-exec-1] com.zallds.xy.servlet.HelloworldServlet : usertest1565180744 2016-11-29 20:04:12.154 INFO 41258 --- [pool-1-thread-1] com.zallds.xy.servlet.HelloworldServlet : 子線程中:usertest1565180744 2016-11-29 20:04:14.142 INFO 41258 --- [nio-8080-exec-2] com.zallds.xy.servlet.HelloworldServlet : usertest481396704 2016-11-29 20:04:14.142 INFO 41258 --- [pool-1-thread-1] com.zallds.xy.servlet.HelloworldServlet : 子線程中:usertest481396704 2016-11-29 20:04:15.248 INFO 41258 --- [nio-8080-exec-3] com.zallds.xy.servlet.HelloworldServlet : usertest400717395 2016-11-29 20:04:15.249 INFO 41258 --- [pool-1-thread-1] com.zallds.xy.servlet.HelloworldServlet : 子線程中:usertest400717395
到此為止,ThreadLocal常見的場景和對應(yīng)解決方案應(yīng)該可以滿足了。接下來就是怎么在實(shí)際應(yīng)用中運(yùn)用了。
為了引出此文的初衷以及后面要講的東西,針對最后一個解決方案,我們可以進(jìn)一步完善一下。
ThreadContext.bindUser(user); runnable.run(); ThreadContext.unbindUser();
這個地方在bind的時候是直接覆蓋,無法對線程之前的狀態(tài)進(jìn)行保存和恢復(fù)。要實(shí)現(xiàn)這一點(diǎn),我們可以抽象一個ThreadState來保存線程的狀態(tài),在bind之前保存original,任務(wù)執(zhí)行完以后進(jìn)行restore。
public interface ThreadState { void bind(); void restore(); void clear(); } public class UserThreadState implements ThreadState { private UserObj original; private UserObj user; public UserThreadState(UserObj user) { this.user = user; } @Override public void bind() { this.original = ThreadContext.getUser(); ThreadContext.bindUser(this.user); } @Override public void restore() { ThreadContext.bindUser(this.original); } @Override public void clear() { ThreadContext.unbindUser(); } } protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { UserObj user = UserUtils.getCurrentUser(); logger.info(user.getName() + user.hashCode()); SendEmailTask emailThread = new SendEmailTask(); executor.execute(new UserRunnable(emailThread, new UserThreadState(user))); super.doGet(req, resp); } /** * 做一個wrapper, 將目標(biāo)任務(wù)做一層包裝, 在run方法中傳遞線程本地變量 */ class UserRunnable implements Runnable { /** * 目標(biāo)任務(wù)對象 */ Runnable runnable; /** * 要綁定的user對象 */ UserThreadState userThreadState; public UserRunnable(Runnable runnable, UserThreadState userThreadState) { this.runnable = runnable; this.userThreadState = userThreadState; } @Override public void run() { userThreadState.bind(); runnable.run(); userThreadState.restore(); UserObj userOrig = UserUtils.getCurrentUser(); logger.info("original:" + userOrig.getName() + userOrig.hashCode()); } } class SendEmailTask implements Runnable { @Override public void run() { UserObj user = UserUtils.getCurrentUser(); logger.info("子線程中:" + (user == null ? "user為null" : user.getName() + user.hashCode())); } }
實(shí)現(xiàn)效果是相同的,至于為什么三次的original對象都是一樣的,通過前面的說明應(yīng)該能夠理解
2016-11-29 20:19:48.694 INFO 41671 --- [nio-8080-exec-1] com.zallds.xy.servlet.HelloworldServlet : usertest114760676 2016-11-29 20:19:48.699 INFO 41671 --- [pool-1-thread-1] com.zallds.xy.servlet.HelloworldServlet : 子線程中:usertest114760676 2016-11-29 20:19:48.700 INFO 41671 --- [pool-1-thread-1] com.zallds.xy.servlet.HelloworldServlet : original:usertest114760676 2016-11-29 20:19:57.123 INFO 41671 --- [nio-8080-exec-2] com.zallds.xy.servlet.HelloworldServlet : usertest941302199 2016-11-29 20:19:57.123 INFO 41671 --- [pool-1-thread-1] com.zallds.xy.servlet.HelloworldServlet : 子線程中:usertest941302199 2016-11-29 20:19:57.123 INFO 41671 --- [pool-1-thread-1] com.zallds.xy.servlet.HelloworldServlet : original:usertest114760676 2016-11-29 20:20:04.385 INFO 41671 --- [nio-8080-exec-3] com.zallds.xy.servlet.HelloworldServlet : usertest1489938856 2016-11-29 20:20:04.385 INFO 41671 --- [pool-1-thread-1] com.zallds.xy.servlet.HelloworldServlet : 子線程中:usertest1489938856 2016-11-29 20:20:04.385 INFO 41671 --- [pool-1-thread-1] com.zallds.xy.servlet.HelloworldServlet : original:usertest114760676
由于在使用shiro框架的SecurityUtils.getSubject()過程中碰到問題,才有了本文的示例,例子中的部分代碼參考了shiro框架的實(shí)現(xiàn)機(jī)制。后面會再研究一下shiro的subject相關(guān)設(shè)計(jì)。
http://shiro.apache.org/subject.html
作者:?99793933e682
原文地址:?https://www.jianshu.com/p/85d96fe9358b
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/75757.html
摘要:并發(fā)設(shè)計(jì)模式一模式的使用表示線程本地存儲模式。為不同的任務(wù)創(chuàng)建不同的線程池,這樣能夠有效的避免死鎖問題。兩階段終止,即將線程的結(jié)束分為了兩個階段,第一個階段是一個線程向另一個線程發(fā)送終止指令,第二個階段是線程響應(yīng)終止指令。 Java 并發(fā)設(shè)計(jì)模式 一、Thread Local Storage 模式 1. ThreadLocal 的使用 Thread Local Storage 表示線程...
摘要:簡要言之往中填充的變量屬于當(dāng)前線程,該變量對其他線程而言是隔離的。在辦理時又告訴機(jī)構(gòu)來,把我的身份證房產(chǎn)證學(xué)生證通通給他。 前言 今天要研究的是ThreadLocal,這個我在一年前學(xué)習(xí)JavaWeb基礎(chǔ)的時候接觸過一次,當(dāng)時在baidu搜出來的第一篇博文ThreadLocal,在評論下很多開發(fā)者認(rèn)為那博主理解錯誤,給出了很多有關(guān)的鏈接來指正(可原博主可能沒上博客了,一直沒做修改)。我...
摘要:下面是線程相關(guān)的熱門面試題,你可以用它來好好準(zhǔn)備面試。線程安全問題都是由全局變量及靜態(tài)變量引起的。持有自旋鎖的線程在之前應(yīng)該釋放自旋鎖以便其它線程可以獲得自旋鎖。 最近看到網(wǎng)上流傳著,各種面試經(jīng)驗(yàn)及面試題,往往都是一大堆技術(shù)題目貼上去,而沒有答案。 不管你是新程序員還是老手,你一定在面試中遇到過有關(guān)線程的問題。Java語言一個重要的特點(diǎn)就是內(nèi)置了對并發(fā)的支持,讓Java大受企業(yè)和程序員...
摘要:進(jìn)程線程與協(xié)程它們都是并行機(jī)制的解決方案。選擇是任意性的,并在對實(shí)現(xiàn)做出決定時發(fā)生。線程池的大小一旦達(dá)到最大值就會保持不變,如果某個線程因?yàn)閳?zhí)行異常而結(jié)束,那么線程池會補(bǔ)充一個新線程。此線程池支持定時以及周期性執(zhí)行任務(wù)的需求。 并發(fā)與并行的概念 并發(fā)(Concurrency): 問題域中的概念—— 程序需要被設(shè)計(jì)成能夠處理多個同時(或者幾乎同時)發(fā)生的事件 并行(Parallel...
摘要:多線程類庫對于共享數(shù)據(jù)的讀寫控制主要采用鎖機(jī)制保證線程安全,本文所要探究的則采用了一種完全不同的策略。所以出現(xiàn)內(nèi)存泄露的前提必須是持有的線程一直存活,這在使用線程池時是很正常的,在這種情況下一直不會被,因?yàn)? Java 多線程類庫對于共享數(shù)據(jù)的讀寫控制主要采用鎖機(jī)制保證線程安全,本文所要探究的 ThreadLocal 則采用了一種完全不同的策略。ThreadLocal 不是用來解決共享數(shù)...
閱讀 2052·2021-11-15 11:39
閱讀 3236·2021-10-09 09:41
閱讀 1500·2019-08-30 14:20
閱讀 3269·2019-08-30 13:53
閱讀 3333·2019-08-29 16:32
閱讀 3389·2019-08-29 11:20
閱讀 3029·2019-08-26 13:53
閱讀 783·2019-08-26 12:18