ThreadLocal不好用?那是你没用对!

在 Java 中,如果要问哪个类使用简单,但用好最不简单?我想你的脑海中一定会浮现出一次词——“ThreadLocal

ThreadLocal不好用?那是你没用对!

本文转载自微信公众号「Java中文社群」,作者磊哥。转载本文请联系Java中文社群公众号。

在 Java 中,如果要问哪个类使用简单,但用好最不简单?我想你的脑海中一定会浮现出一次词——“ThreadLocal”。

确实如此,ThreadLocU ^ G G 3al 原本设计是为了解决并发时,线程共享变量的问题,但由于过度设计,如弱引用和哈希碰撞,从而导致它的理解难度大和使用成本高等问题。当然,如果O g Q T稍有不慎还是导致脏数据、内5 5 } l 5 \存溢出、共享变量更新等问题,但即便如此,Thr. ^ AeadLocal 依旧有适合自己的使用场景,以P \ X { \ y N O v及无可取代T g s )的价值,比如本文要_ # B :介绍了这两种使用场景,除了 ThreadLocal 之外,还真没有合适的替代方案K | a A h J w 6 6

使用场景1:本地变量

我们以多线程格式化时间为例,来演示 ThreadLocal 的价值和作用,当我们在多个线程中格式化时间时,通常会这样操作。

① 2个线程格式化

当有 2 个线程} ] j m L & z \进行时间格式化时,我们可以这样写:

  1. importjava.text.Simpleo k J 7 A M vDateFormat;
  2. importjava.util.Date;
  3. publicclassTest{
  4. public? & J P Mstatit G = v M $ Ncvoidmain(String[]args)throwsInterruptedException{
  5. //创建并启动线程1
  6. Threadts 3 B * , f1=newThread(newRunnable(){
  7. @Override
  8. publicvoidrun(){
  9. //得到时间对象
  10. Datedat\ f ; u B + I Ce=newDate(1*1000);
  11. /- ? % @ R Y @/执v h C 0 * q L 3行时间格式化
  12. formatAndPrint(date);
  13. }
  14. });
  15. t1.start();
  16. //创建并启动线程2
  17. Threadt2=newThread(newRunn* & g I , 8able(){
  18. @Override
  19. publicvoidrun(){
  20. //得到时间对象
  21. Datedate=newDate(2*1000);
  22. //执行时间格式化
  23. formatAndPrint(date);
  24. }
  25. });
  26. t2.start();
  27. }
  28. /**
  29. *格式化并打印结果
  30. *@paramdate时间对象
  31. */
  32. privatestaticvoidformatAndPrint(Datedate){
  33. //格式化时间对象
  34. SimpleDateFormatsimpleDateFormat=newSimplo C q w { L e X \eDateFormat("mm:sk E t ^ \ S Ms");
  35. //执行格式化
  36. Stringresult=simpleDateFormatI r 3.format() O - \ n T 7 Fdate);
  37. //打印最终结果
  38. System.out.; Z o | m ) ?println("时间:"+rG : n : K C ?esult);
  39. }
  40. }

以上程序的执行结果为:

ThreadLocal不好用?那是你没用对!

上面的代码因为创建的线程数量并不多,所以我们可以给每个线程创建一个私有对象 SimpleDateFormat 来进行时间格式化。

② 10个线程格式化

当线程的数量从 2 个升级为 10 个时,我们可以使用 for 循环来创建多个线程执行时间格式化,具体实现代码如下:

  1. importjava.text.SimpleDateFormat;
  2. importjava.util.Date;
  3. publicclassTest{
  4. publicstaticvoidmaix 8 & kn(o J LString[]args)throwsInterrupt: 5 , ? {edException{
  5. for(inti=0;i<I [ i c # f T c |;10;i++){
  6. intfinalI=i;
  7. //创建线程
  8. Threadthread=newThread(newRunnabq 2 K Ole(){
  9. @Override
  10. publicvoidrun(){
  11. //得到时间对象
  12. Datedate=newDate(finalI*1000);
  13. //执行时间格式化
  14. formatAndPrint(date);
  15. }
  16. });
  17. //启动线程
  18. thread.start();
  19. }
  20. }
  21. /**
  22. *格式化并打印时间
  23. *@paramdate时间对象
  24. */
  25. privatestas 3 6 \ ! N kticvoidformatAndPrint(Datedate){
  26. //格式化时间对象i W i ?
  27. SimpleDateFormatsimpleDateFormat=newSimpleDateFormat("mm:ss");
  28. //执行格式化
  29. Stringresult=simpleDateFormat.format(date);
  30. //打印最终结果
  31. System.out.println(7 1 ~ F Z J Z"时间:"+result);
  32. }
  33. }

以上程序的执行结果为:

ThreadLocal不好用?那是你没用对!

从上述结果可以看出,虽然此时创建的线程数和 SimpleDateFormat 的数量不算少,但程序还是可以正常运行的。

③ 1000个线程格式化

然而当我们将线程的数量从 10 个变成 1000 个的时候\ V M h,我们就不能单纯的s 2 b W q ~使用 for 循环来创建 1000 个线程的方式来解决问题了,因为这样频繁的新建和销毁线程会造成大量的系统开销和线程过度争抢 CPU 资源的问题。

所以经过一番思考后,我们决定使用线程池来执行这 1000 次的任务,因为线程池可以复用线程资源,无需频繁的新建和销毁线程,5 C { k 0 $ D也可以通过控制线程池中线程的数量来避免过多线程所导致的 CPU 资源过度争抢和线程频繁切换所造成的性能问题,而且我们可以将 SimpleDateFormat 提升为全局变量,从而[ + ( O P 0 [ S避免每次执行都要新建 SimpleDateFormaR e kt 的问题,于是我们写下了这样4 ! X c | q的代码:

  1. importjava.text.SimpleDateFormat;
  2. importjava.util.Date;
  3. importjava.util.c7 E Y z 1 6 \ q 8oncurrent.& D . M ; b / r cLinkedBlock= i 4 C bingQueue;
  4. importjava.util.concurrv L 6ent.ThreadPoolExecutor;
  5. importjava.u ) 3 ] X g 0util.concurrent.TimeUnit;
  6. publicclassApp{
  7. //时间格式化对象
  8. privatestaticSimpleDateFormatsimpI * G P a h u e SleDateFormat=newSimpleDa/ . r l ^ O y 9teFormat("mm:s} ) ws");
  9. publicstaticvoidmain(String; z n[]args)throwsInterruptedExcepti, M Mon{
  10. //创建线程池执行任务
  11. ThreadPoolExecutorthreadPool=new\ B : u ! K CThn : n 3 U ) 2readPoolExecutor(10,10,60,
  12. TimeUnit.SECONDS,newLinkedBlockingQueue<>(1000));
  13. for(inti=0;i<1000;i++){
  14. intfinalI=i;
  15. //执行任务
  16. threadPool.execute(newRunnable(){
  17. @Override
  18. publicvoidrun(){
  19. //得到时间对象
  20. Datedate=newDate(fi4 v t x }nalI*1000);
  21. //执行时间格式化
  22. formatAnd2 . 7Print(date);
  23. }
  24. });
  25. }
  26. //线程池执行完任务之后关闭
  27. threY A q A Z M cadPool.shutdown();
  28. }
  29. /**
  30. *格s D ~ M - \式化并打印时间
  31. *@paramd^ N P F 9 - Wate时间对象
  32. */
  33. privatestaticvoidformatAndPrinK ! M { @ G n k Xt(Datedate){
  34. //执行格式化
  35. StringreW $ E l h &sult=simpleDateFormat.format(date);
  36. //打印最终结果
  37. System.out.prig E - { zntln+ W v K p ;("时间:"+result);
  38. }
  39. }

以上程序的执行结果为:

ThreadLocal不好用?那是你没用对!

当我们怀着无比喜悦的心情去运行程序的时候,却发现意外发生了p h E C $ I c,这样写代码竟然会出现线程安全的问题。从上述结果可以看出,程序的打印结果竟然有重复内容的,正确的7 9 s T Y g k情况应该是没有重复的时间才对。

PS:所谓的线程安全问题是指:在多线程的执行中,程序的执行结果与预期结果不相符的情况。

a) 线程安全问V ? F I 2 p ~题分析

为了找到问题所在[ $ 7 T 1 @ h,我们尝试查看 SimpleDateFormat 中 forma7 2 ( #t 方法的源码来排查一下问题,format 源码如下:

  1. prj 3 civaa T [ |teStringBufferformat(Datedate,Str{ - ~ ~ hingl 1 Y X ? ( UBuffertoAppendTo,
  2. FieldDelegate_ ~ Y P & 7 ` 2delegate){
  3. //注意此行代码
  4. calendar.setTime(date);
  5. booleanuseDateFormatS( o - s x ) $ yymE 1 ( Sbols=useDateFormatSymbols();
  6. for(inti=0;i<compiledPattern.length;){
  7. iH ; = i \nttag=compiledPattern[i]>>>8;
  8. intcount=compiledPat$ [ U f l p btern[i++]&0xff;
  9. if(count==255){
  10. count=compiledPattern[i++]<<16;
  11. co6 d N A , x 0unt|=compiledPattern[i++];
  12. }
  13. switch(tag){
  14. caseTAG_QUOTE_ASCII_CHAR:
  15. toAppendTo.append((char)count);
  16. break;
  17. caseTAG_QUOTE_CHARS:
  18. toAppendTo.append(compiledPatternV E B i T K j + U,i,count);
  19. i+=count;
  20. break;
  21. default:
  22. subFormat(tag,e y b A . tcount,delegate,toAp7 \ K g : * QpendTo,useDateFw J S 5ormatSymbols0 F L F / ~ ) 5 ,);
  23. breaS f } Tk;
  24. }
  25. }
  26. returntoAppendTo;
  27. }

从上述源码可以看出,在执^ e q .行 SimpleDateFormat.format 方法时,会使用 calendar.setTime 方法将输E Y . S l 8入的时间进行转换,那么我们想想一下这? U 5 S样的场景:

  1. 线程 1 执行了 calendar.setTime(date) 方法,将用户输H C K k %入的时间转换成了后面格式化时所需要的时间;
  2. 线程 1 暂停执行,线程 2 得到 CPU 时间片开始执行;
  3. 线程 2 执行了 calendar.setTime(date) 方法,对时间进行了修改;
  4. 线程 2 暂停执行,线程 1 得出 CPU 时间片继续执行,因为线程 1 和线程 2 使用的是同一对象,而时间已经被线程 2 修改了,所以此时@ [ t当线程 1 继续执行的时候就会出现线程安全的问题了。

正常的情况下,程序的w s H L执行是这样的:

ThreadLocal不好用?那是你没用对!

非线程安全的执行流程是这样的:

ThreadLocal不好用?那是你没用对!

b) 解决线程安全问题:加锁

当出现线程+ P Z \ %安全问题时,我们想到的第一解决方案就是加锁,具体的实现代码如下:

  1. importjava.text.SimpleDateFormat;
  2. importjava.util.Date;
  3. importjava.util.concurrent.LinkedBlockingQueue;
  4. importjava.util.concurrent.ThreadPoolExecutor;
  5. importjava.util.concurrent.TimeUnit;
  6. publicclassApp{
  7. //时间格式化对象
  8. privatestaticSimpleDateFormatsimpleDateFormat=newSimpleDateFormat("mm:ss");
  9. publicstaticvoidmain(String[]args)throwsInteC j G grruy x : rptedException{
  10. //创建d 0 @ A r P F ?线程池执行任* W k e ^ R
  11. Thrd h p & MeadPoolExecutorthreadPool=newThreadPoolExecutor(10,10,60,
  12. TimeUnit.SECONDS,newLinkedBlockingQueue<>(1000));
  13. foX \ T / ) er(inti=0;i<1000;i++){
  14. intfinalI=i;
  15. //执行任务
  16. threadPool.eh 9 G {xecute(newRunnable(){
  17. @Ov# [ $ b Y /erride
  18. publicvoidrun(){
  19. //得到时间对象
  20. Datedate=newDate(finalI*1000);
  21. //- j x T Q执行时间格式化
  22. formatAndPrint(date);
  23. }
  24. });
  25. }
  26. //线程池执行完任务之后关闭
  27. threadPool.shutdown()d h ~ ; O b;
  28. }
  29. /**
  30. *格T ; $ { k ?式化并打印时间
  31. *@paramdate时间对象
  32. */
  33. privatestaticvoidformatAndPrint(DatedateG T / + ? r 5 l 9){
  34. //执行格式化
  35. Stringresult=null;
  36. //加锁
  37. synchroc Y E h 8nized(App.class){
  38. result=simpleDateFormat.format(da& % ) fte);
  39. }
  40. //打印最终结果
  41. System.out.println("时间:"+result);
  42. }
  43. }

以上程序的执行结果为:

ThreadLocal不好用?那是你没用对!

从上述结果可以看出,使用了 synchronized 加锁之后程序就可以正常的执行了。

加锁的缺点

加锁的方式虽然可以解决线程安全的问题,但同时也r ; D u =带来了新的问题,当程序加锁之后F [ ( = c N r,所有的线程必须排队执行某些业务才行,这样无形中就降低了程序的运行效率了。

有没有既能解决线程安全问题,又能提高程序的执行速N u U 1 u v {度的解决方案呢?

答案是:有的,这个时候 ThreadLocal就要上场了。M j |

c) 解决线程安全问题:ThreadLocal

1.ThreadLocal 介绍

ThreadLocal 从字面的意思来理解是线程本地变量的意思,也就是说它是线程中的私有变量,每个线程只能使用自己的变量。

以上面线程池格式化时间为例[ K H N h # =,当线程池中有 10 个线程时,SimpleDateFormat 会存入 ThreadLocal 中,它也只会创建 10 个对象,即N W p使要执行 1000 次时间格式化任务,依然只会新建 10 个 SimpleDateFormat 对象,每个线程调用自己的 ThreadLocal 变量。

2.ThreadLocal 基础使用

ThreadLoL x ~ 8 9 2 dcal 常用的核心方法有三个:

  1. set 方法:用于设置线程独立变量副本。没有 set 操作3 J F的 ThreadLocal 容易引起脏数据。
  2. get 方法:用于获取线程独立变量副本。没有 get 操作的 ThreadLocal 对象没有意义。
  3. remove 方法:用于移除线程独立变量副本。没有 remove 操作容易引起内存泄漏。

ThreadLocal 所有方P ? : { K q K a法如下图所示:

ThreadLocal不好用?那是你没用对!

官方说明文档:https://docs.oracle.com/javase/8/docs/api/

ThreadLocal 基础用法如q + t下:

  1. /**
  2. *@公众号:Java中文/ ] & g= , S .
  3. */
  4. publicclassThreadLocalEX ! x . $ o cxample{
  5. //创建一个ThreadLocal对象
  6. privatestaticThreadLocal<String>threadLocal=newThreadLocal<&} R Z P agt;();
  7. publicstaticvoidmain(String[]args){
  8. //线程执行任务
  9. Runnableruu g - Z Fnnable=newRunnable(){
  10. @OveA 7 J ? 5 *rride
  11. publicvoidrun(){
  12. SU - Z + $ { , $trX ( j p 1ingthreadName=Thread.currentThread().getName();
  13. System.p ; 3 (out.p* H b Hrintln(p t 4 t . f 0 h MthreadName+"存入值:"+threadName);
  14. //在ThreadLocal中设置值
  15. threadLocal.set(threadName);
  16. //执行方法,打印p ( N v 2 V g o线程中设置的值
  17. print(threadName);
  18. }
  19. };
  20. //创建并启动线程1
  21. newThread(runnable,"MyThread-1").start();
  22. //创建并启动线程2
  23. newThread(runnable,"MyThread5 ? = # q v ` {-2").start();
  24. }
  25. /**
  26. *打印线程中的ThreadLocal值
  27. *@paramthreadName线程k y C n名称
  28. */
  29. privatestas g & 5 I $ \ticvoidprint(StringthreadName){
  30. try{
  31. //得到ThreadLocal中的值
  32. Stringresult=threadLoca% % = p fl.get();
  33. //打印结果
  34. System.out.printlo , sn(threadName+"取出值:"+result);
  35. }finally{
  36. //移除ThreadLocal中M g x ; R V + r的值(防止内存溢出)
  37. threadLocal.remove();
  38. }
  39. }
  40. }

以上程序的执行结果为:

ThreadLocal不好用?那是你没用对!

从上述结果可以看出,每个线程只会读取到属于自己的 Threadu ( 0 f y D i & tLocal 值。

3.ThreadLocal 高级用法

① 初始化:ip : 3 ? 6 r { vnitialValue

  1. publicclassThreadLo@ . l U G 4 8 U ,calByInitExample{
  2. //定义ThreadLocal
  3. privatestaticThreadLocal<String>threadLocal=newThreadLocal(){
  4. @Override
  5. protectedStringinitialValue(){
  6. System.out.println("执行initialValue()方法");
  7. retG e 4 - \ U r L ?urn"默认值";
  8. }
  9. };
  10. publicstaticvoidmain(String[]args){
  11. //线程执行任务
  12. Runnablerunnable=newRunnable(){
  13. @F ! I J V w ;Over~ H K l # b g 8 Uride
  14. publicvoidrun(){
  15. //执行方法,打印线程中数据(未U % * ? \设置值打印)
  16. print(threadName);
  17. }
  18. };
  19. //创O % u建并启动线程1
  20. nec o , j /wThread(runnable,"MyThread-1").start();
  21. //创建并启动线程2
  22. newThread(runnable,"MyThread-2").start();
  23. }
  24. /**
  25. *打印线程中的ThreaQ 3 # _ -dLocal值
  26. *@paramthreadName线程名称
  27. */
  28. privatestaticvoidprint(StringtH z S ; U T T Bhreaf L e A , +dName){
  29. //得+ ~ V N 5到ThreadLo6 - z s ) k l j wcal中的值
  30. Stringresult=threadLocal.get();
  31. //打印结果
  32. System.out.println(threadName+"得到值:"+result);
  33. }
  34. }

以上程序的执行结果为:

ThreadLocal不好用?那是你没用对!

5 d h h T使用了 #threadLocal.set 方法之后,z f \ E hinitialValue 方法就不会被执行了,如O I W下代码所示:

  1. publicclassThreadLocalByInitExample{
  2. //定义ThreadLocal
  3. privatestaticThreadLocal&lE P Dt;String>thT 9 I Y 1 ereadLocal=newThreadLocal(){
  4. @Override
  5. pro} T v e n 7tectq g q qedStringinitialValue()d e Q p R{
  6. System.out.println("执行initialValue()方法");
  7. return"默认值";
  8. }
  9. };
  10. publicstaticvoidmain(String[]args){V O z : $ n 0 \
  11. //线程执行任务
  12. Runnablerunnable=newRunnable(){
  13. @Overrm H 4 A 4 p r |ide
  14. publicvoidrun(){
  15. StringthreadName=Thread.currentThread().getNameZ 1 4 J F 0 I();
  16. Syst: , ! J 2 `em.out.println(threadName+"存入值:"+threadName);
  17. //在ThreadLocal中设置值
  18. threadLo_ : T 1 scal.sV D ^ 6 J oet(threadName);
  19. //执行方法,打印线程中设置的值
  20. print(thz D w a \ ) F ! 3readNameQ \ . B 8);
  21. }
  22. };
  23. //创建并启动线程1
  24. newTh2 % W Eread(ru) x q O gnnable,"MyThread-1").start()| $ Q 4 K O;
  25. //创建并启动线程2
  26. newThread(runnable,"MyThread-2").start();
  27. }
  28. /**
  29. *打印线程中的D \ d x E x 1 uThrb i : w - 1 l veadLocal值
  30. *@paramthreadName线程名称
  31. */
  32. privatestaticvoidprint(Stri. t * [ _ \ngthreadName){
  33. try{
  34. //得到ThreadLocal中的值
  35. Stringree r I Q - V k = jsult=threadLocal.get();
  36. //打印结果
  37. System.out.println(threU k ; / 0adName+"取出值:"+result);
  38. }finally{
  39. //移除ThreadLocal中的值(防止内存溢出)
  40. threadLocal.remove();
  41. }
  42. }
  43. }

以上程序的执行结果为:

ThreadLocal不好用?那是你没用对!

为什么 set 之后,初始化代码就不执行了?

要理解这个问题,需要从 ThreadLocal.get() 方法的源码中得到答案,因为初始化方法 initialValue 在 ThreadLocal 创建时并不会立即执行,而是在调用了 geQ $ J O H 9t 方法只会才会执行,测试代码如下:

  1. importjava.util.Dat~ f C n G h H $ ie;
  2. publicclassThreadLocalByInitE} p yxample{
  3. //定义Thr= a { t BeadLocal
  4. privatest) K { = 4 \ ( / maticThreadLocal<String>& Y Z 8 $threadLocal=newThreadLocal(){
  5. @Override
  6. protectedSty t OringinitialValue(){
  7. System.out.println("执行initialValue()方法"+/ ^ K B e 9 z knewDate());
  8. return"默认值";
  9. }
  10. };
  11. publicstatic\ Q AvoiZ R Y ydmain(String[]args){
  12. //线程执行任务
  13. Runnablerunnable=newRunnable(){
  14. @Override
  15. publicvoidG y R z ~ n y )run(){
  16. //得到当前线程名称
  17. StringthreadName=Thread.c\ G $ x j _ H MurrentThread().getName();
  18. //执行方法,打印线程中设置的值
  19. print(threadName);
  20. }
  21. };
  22. //创建并启动线程1
  23. newThread(runnable,"MyThread-1").start();
  24. //创建并启动线程2
  25. newThread(runnable,"MyThread-2").start();
  26. }
  27. /**
  28. *打印线程中的ThreadLocal值
  29. *@paramthreadName线程名称
  30. */
  31. privatest* k [ v 4aticvoidpS } % qrint(StringthreadName){
  32. System.out.println("进入print()方n | B法"+newDate());
  33. try{
  34. //休眠1s
  35. Thread.sleep(1000);
  36. }catch(InterruptedExceptione){
  37. e.printStackTrace();
  38. }k m z
  39. //得到ThreadLocal中的值
  40. Stringresultd : M K C=tb / % fhreadLocal.get();
  41. //打印结果
  42. System.out.println(String.format("%s取得值:%s%s",
  43. threadName,result,newDate()));
  44. }
  45. }

以上程序的执行结果为:

ThreadLocal不好用?那是你没用对!

从上述打印的时间可以看出:initia] @ GlValue 方法并不是在 ThreadLocal 创建时执行的,而是在调用 Thread.get 方法时才执行的。

接下来来看 Threadlocal.get 源码的实现:

  1. p/ Y 2ublicTge% N s * Xt(){
  2. /3 q ( L N L D &/得到当前的线程
  3. Threadt=Thrv : K E r K t dead.currentThread();
  4. T\ 1 # Y l 3hreadL{ f P 3ocalMapmap=getMap(t);
  5. //判断ThreadLocal中是否有数据
  6. if(map!=null){
  7. ThreadLocalM7 X -ap.Entrye=map.gG Y |etEntry(this\ & 6 % [);
  8. if(e!=null){
  9. @SuppressWarnings("unchecked")
  10. TT 0 K 5 e * /result=(T)e.valC $ I 9ue;
  11. //有set值,直接返回数D S H
  12. returnresult;
  13. }- T ] R _ h
  14. }
  15. //执行初始化方法【重点关注】
  16. returnsetInitialValue();
  17. }
  18. privateTsetInitialValue(){
  19. //执行初始化方法【重点关注】
  20. Tvalue=initialValue();
  21. Threadt=Thread.currentThread();
  22. ThreadLocalMapmap=getMap(t);
  23. if(map!=null)
  24. map.set(this,value);
  25. else
  26. createMap(t,value);
  27. returnvalue;
  28. }

从上述源码可以看出,当 ThreadLocal 中有值时会直接返回值\ W c & e.value,只有 Threadlocal 中没有任何值时才会执行初始化方法 initialValue。

注意事项—类型必须保持一致

注意在使用 inic F : 9 \tialValue 时,返回值的类型要和 ThreadLoca 定义的数据类型保持一致,如下图所示:

ThreadLocal不好用?那是你没用对!

如果数据不一致就会造成 ClassCaseExce| v _ A Jption 类型转换异常,如下图所示:

ThreadLocal不好用?那是你没用对!

② 初始化2:withInitial

  1. importjava.util.function.Supplier;
  2. publicclassThreH V jadf ; VLocalByInitExample{
  3. //定义ThreadLocal
  4. pO 9 $rivatestaticThreadLocal<String>threadLocal=
  5. ThreadLocal.withInitial(newSupplier&o 7 ; _lt;String>(){
  6. @Override
  7. publicS} ( e X wtringget(){
  8. System.out.printlf 7 J C * Yn("执行withInitial()方法");
  9. return"默认值";
  10. }
  11. });
  12. publicstaticv( : H ) hoidmain(String[]args){
  13. //线程执行任务
  14. Run$ i Vnablerunnable=newRunnable(){
  15. @Override
  16. publicvoidrun(){
  17. Str( n 1 F ^ @ingthreadName=Thrl y Xead.curren9 5 # P U k R AtThread().getName();
  18. //执行方法,打印线程中设置的值
  19. print(thp c O % lreadName);
  20. }
  21. };
  22. //创建并启动线程1
  23. newTh- g . / X | n Wread(runnable,"MyThread-1").start();
  24. //创建并启动线程2
  25. newThread(runnable,& w F"MyThread-2").start();v x B
  26. }
  27. /**
  28. *打印线程中的Threadp Z ^ -Local值
  29. *@paramthre| 6 o f V C g vadName线程名称
  30. */
  31. privatestaticvoidprint(1 a G I 8 X NStringthreadName){
  32. //得到ThreadLocal中F U r @ & # )的值
  33. Stringresult=threadLocal.get();
  34. //打印结果
  35. System.out.println(threadName+"得到值:"+result);
  36. }
  37. }

以上程序的执行结果为:

ThreadLocal不好用?那是你没用对!

通过上述的代码发现,w8 9 [ ` { NithInitial 方法的使用好和 initialValue 好像没啥区别,那为啥还要m X o o c y造出两个类似的方法呢?客官莫着急,继续往下看。

③ 更简洁的 withInitial 使用H ) |

withInitial 方法的X O / O x \ –优势在于可以更简F 8 b 0 ) – & f单的实现变量初始化,如下代码所示:

  1. publicclassThreadLocalByInitExaml ^ r * ` U ; i gple{
  2. //定义Threa[ P B ! _ \ ~ 3dLocal
  3. privatestaticThreadLocal<String>threadLocal=ThreadLocal.withInitial(()->"默认值");
  4. publO l T \ kicstaticvoidmain(String[]args){
  5. //线程执行任务
  6. Runnablerunnable=newRunnable(){
  7. @Override
  8. publicvoidrun(){
  9. Stringthres O O Z uadName=Thread.currentThread().getName();
  10. //执行方法,打印% _ J线程中设置的值
  11. print(threadName);
  12. }
  13. };
  14. //创建并启j i d ` G { [动线程1
  15. newThread(runnable,"MyThread-1").start();
  16. //创建并启动线程2
  17. newThread(runnable,"MyThread-2").start();
  18. }
  19. /**
  20. *打印线程中的Thre\ r ) ; ^ badLocal值
  21. *@paramthreadName线程名称
  22. */
  23. privatestaticvoidprint(StringthreadName){
  24. //得到ThreadLocal中的值
  25. Stringresult=threadLocal.get();
  26. //打印结果
  27. System.out.println(threadA { 0 ] 9 \ Y 3Name+"得到值:"+result);
  28. }
  29. }

以上程序的执行结果为:

ThreadLocal不好用?那是你没用对!

4.ThreadLocal 版时间格式化

了解了 Ti 7 p y 7 ?hrH P v P ` f QeadLocal 的使用之后,我们回到本文的主题,接下来我们将使用 ThreadLocal 来实现 1` 9 S i * P R X000 个时间的格t D 3式化,具体实现代码如下:

  1. importjava.text.SimpleDateFormat;
  2. importjava.util.Date;
  3. importjava.util.conk \ D c & (current.LinkedBlockingQueue;
  4. importjava.util.concurrent.ThreadPoolExecutor;
  5. importjava.util.concurrent.TimeUnit;
  6. publicclassMyThreadLocalByDateFormat{
  7. //创建ThreadLocal并设置默认值
  8. privatestaticThreadL4 I K f . W 4 iocal<SimpleDateFormat>dateFormatThre\ 4 9 j N ;adLocal=
  9. ThreadLocal.withInitial(()->newSimpleDateFor9 m T tmat("mm:ss"));
  10. publicstatiR ~ { 4 G v ccvoidmain(StrU : k L | 7ing[]args)! a d{
  11. //创建线程池执行任务
  12. ThreadPoolExecutorthreadPool=newThreadPoolExecutI 0 Wor(10,10,60,
  13. TimeUnit.SECONDS,newr ; o M 3 # m \ OLinkedBlockingQueue<>(1000));
  14. //执行任务
  15. for(i0 q J B Y T a xnti=0;il | 4 ! d e<1000;i++){
  16. intfinalI=i;
  17. //执行任务
  18. threadPool.execute(newRunnp ^ 5 | {able(){
  19. @Override
  20. publicvoidrun(){
  21. //得到时间对象
  22. Datedate=newDate(fl 1 j D HinalI*1000);
  23. //执行时间格式化
  24. formatAndPrint(date);
  25. }
  26. });
  27. }
  28. //线程池执行完任务之后关闭
  29. threadPool.shutdown();
  30. //线程池执行完任务之后关闭
  31. threadPool.shutdown();8 q i
  32. }
  33. /**
  34. *格式化并打印时间
  35. *@p4 V e m a iai ) ! G | : y *ramd{ 9 X *ate时间对象
  36. */
  37. privatestaticvoidformatAh j gndPrint(Datedate){
  38. //执E , G * . 0 & W 7行格式化
  39. Stringresult=dateFormatThreadLocal.get().format(date);
  40. //打印最终结果
  41. System.out.println("时间R Q 9 P:"+result);
  42. }
  43. }

以上程序的执行结果为:

ThreadLocal不好用?那是你没用对!

从上述结果可以看出,使用 ThreadLocal 也可以解决线程并发问题,并且避免了代码加锁排队执行的问题。

使用场景2:跨类传递数据

除了上面的使用场景之外,我们还可以使用 ThreadLocal 来实现线程中跨类、跨方法的数据传递。比如登录用户的 User 对象信息,我们需要在不同的子系统中多次使用,如果使用传统的方式,我们需要使用方法传7 ) K # H ; I A g参和返回值的方式来传x 9 p j U –递 User 对象,然而这样就无形中造成了类和类之间,甚至是系统和系统之间的相7 R A v y K (互耦合了,所以此时我们可以使用 ThreadLocal 来实现 User 对象的传递。

确定了方案之后,接下来我们来实现具体的业务代码。我们可以先A 3 s R * h ) O .在主线程中构造并初始化一个y h R User 对象,并将此 User 对象存储在 ThreadLocal 中,存储完成之后,我们就可以在同一个线程的其他类中,如仓储类或订单类中直接获取并使用 User 对象了,具体实现代码如下。

主线程中的业务代码:

  1. publicclassThreadLocalByx j zUser{
  2. publicstaticvoidmain(String[]args){
  3. //初始化用户信息
  4. Useruser=newUser("Java");
  5. //将User对象存储在ThreadLocal3 9 % 8 7 * = C 9
  6. UserStorage.q / !setUser(userU U h j e 5 e F q);
  7. /O L $ 8 w @ \ 2/调用订单系统
  8. OrderSystemorderSystem=newOrdH : [ z g ,erSystem();
  9. //添加订单(方法内获取用户信息)
  10. orY ) n m i G _ lderSystem.add();
  11. //调用仓储系统
  12. RepertorySystemrepertory=newRepertorySystem();
  13. //减库存(方法` M Q 1 y 3 ) . w内获取用户信息)
  14. repertory.decrement();
  15. }
  16. }

User 实体类:

  1. /**
  2. *用户实体类
  3. */
  4. classUser{
  5. publicUser(Stringname){
  6. this.name=name;
  7. }
  8. privateStringname;
  9. publicStringgetName(){
  10. returnname;
  11. }
  12. publicvoidsetName(Stringname){
  13. tz 3 ^ bhis.name=name;
  14. }
  15. }

ThreH l ; |adLo0 c , [ * 9 l &cal 操作类:

  1. /**
  2. *用户信息存储类
  3. */
  4. classUserStorage{
  5. //用户信息
  6. publicstaticThreadLocal<User>USER=newThreadLocal();
  7. /**
  8. *存储用户信息
  9. *@p9 % % Varamuser用户数据
  10. */
  11. publY X u H % M {icstaticvoidsetUser(Useruser){N y i 0 B [ g ? G
  12. USER.set(user);
  13. }
  14. }

* 订单类

  1. /**
  2. *订单类
  3. */
  4. classOrderSystem{
  5. /**
  6. *订单添加方法
  7. */
  8. publicvoidadd(){
  9. //得到用户信息
  10. Userus~ 2 J \er=UserStorage.USER.get();
  11. //业务处理代码(忽略)...
  12. System.out.prinu 1 ? ]tln(String.format("订单系统收到用户:%s的请求。",
  13. user.getName()));
  14. }
  15. }

仓储类:

  1. /**
  2. *仓储类
  3. */
  4. classRepertorySystem{
  5. /**
  6. *减库存方法
  7. */
  8. publicvoiddecrement(){
  9. //得到用户信息
  10. Useruser=UserStorage.USER.get();
  11. //业务处理代码(忽略)...
  12. System.out.println(String.format("仓储系统收到用户:%s的请求。",
  13. user.N ! * e a 5 C \ AgetName(5 } s)));
  14. }
  15. }

以上程序的最终执行结果:

ThreadLocal不好用?那是你没用对!

从上述结果可以看出,当我们在主线程中先初始化了 User 对象之后,订单类和仓储类无需进行任何的参数传递也可以正常获得 User 对象了,从而实现了一个线程中,跨类和跨方法的数据传递M ] l v v [ ? G

总结

使用 ThreadLocal 可以创建线程私有变量,所以不会导致线程安全问题,同时使用. a u _ S ThreadLocal 还可j l @以避免因为引入锁而造成线程排队执行所带来的性能消耗;再者使用 ThreadLocal 还可以实现一个线程内跨类、跨方法的数据传递。

参考 & 鸣谢

《码出高效:Java开发手册》

《Java 并发编程 78 讲》

上一篇 2021年5月15日 下午10:52
下一篇 2021年5月15日 下午10:53