盤他!ThreadLocal 一次解決老大難問題

juejin.im/post/5e0d8765f265da5d332cde44

1.ThreadLocal的使用場景

1.1 場景1

每個線程需要一個獨享對象(通常是工具類,典型需要使用的類有SimpleDateFormat和Random)

每個Thread內有自己的實例副本,不共享

比喻:教材只有一本,一起做筆記有線程安全問題。複印後沒有問題,使用ThradLocal相當於複印了教材。

1.2 場景2

每個線程內需要保存全局變量(例如在攔截器中獲取用戶信息),可以讓不同方法直接使用,避免參數傳遞的麻煩

2.對以上場景的實踐

2.1 實踐場景1

<code>/***兩個線程打印日期*/publicclassThreadLocalNormalUsage00{  publicstaticvoidmain(String[]args)throwsInterruptedException{    newThread(newRunnable(){               @Override               publicvoidrun(){            Stringdate=newThreadLocalNormalUsage00().date(10);      System.out.println(date);    }  }).start();newThread(newRunnable(){           @Override           publicvoidrun(){  Stringdate=newThreadLocalNormalUsage00().date(104707);  System.out.println(date);}}).start();}publicStringdate(intseconds){  //參數的單位是毫秒,從1970.1.100:00:00GMT開始計時  Datedate=newDate(1000*seconds);  SimpleDateFormatdateFormat=newSimpleDateFormat("yyyy-MM-ddhh:mm:ss");  returndateFormat.format(date);}}/<code>

運行結果

盤他!ThreadLocal 一次解決老大難問題

因為中國位於東八區,所以時間從1970年1月1日的8點開始計算的

<code>/***三十個線程打印日期*/publicclassThreadLocalNormalUsage01{  publicstaticvoidmain(String[]args)throwsInterruptedException{    for(inti=0;i<30;i++){      intfinalI=i;      newThread(newRunnable(){                 @Override                 publicvoidrun(){        Stringdate=newThreadLocalNormalUsage01().date(finalI);        System.out.println(date);      }    }).start();//線程啟動後,休眠100ms    Thread.sleep(100);  }}publicStringdate(intseconds){  //參數的單位是毫秒,從1970.1.100:00:00GMT開始計時  Datedate=newDate(1000*seconds);  SimpleDateFormatdateFormat=newSimpleDateFormat("yyyy-MM-ddhh:mm:ss");  returndateFormat.format(date);}}/<code>

運行結果

盤他!ThreadLocal 一次解決老大難問題

多個線程打印自己的時間(如果線程超級多就會產生性能問題),所以要使用線程池。

<code>/***1000個線程打印日期,用線程池來執行*/publicclassThreadLocalNormalUsage02{  publicstaticExecutorServicethreadPool=Executors.newFixedThreadPool(10);publicstaticvoidmain(String[]args)throwsInterruptedException{  for(inti=0;i<1000;i++){    intfinalI=i;    //提交任務    threadPool.submit(newRunnable(){                      @Override                      publicvoidrun(){      Stringdate=newThreadLocalNormalUsage02().date(finalI);      System.out.println(date);    }  });}threadPool.shutdown();publicStringdate(intseconds){//參數的單位是毫秒,從1970.1.100:00:00GMT開始計時Datedate=newDate(1000*seconds);SimpleDateFormatdateFormat=newSimpleDateFormat("yyyy-MM-ddhh:mm:ss");returndateFormat.format(date);}}/<code>

運行結果

盤他!ThreadLocal 一次解決老大難問題

但是使用線程池時就會發現每個線程都有一個自己的SimpleDateFormat對象,沒有必要,所以將SimpleDateFormat聲明為靜態,保證只有一個

<code>/***1000個線程打印日期,用線程池來執行,出現線程安全問題*/publicclassThreadLocalNormalUsage03{  publicstaticExecutorServicethreadPool=Executors.newFixedThreadPool(10);//只創建一次SimpleDateFormat對象,避免不必要的資源消耗staticSimpleDateFormatdateFormat=newSimpleDateFormat("yyyy-MM-ddhh:mm:ss");publicstaticvoidmain(String[]args)throwsInterruptedException{  for(inti=0;i<1000;i++){    intfinalI=i;    //提交任務    threadPool.submit(newRunnable(){                      @Override                      publicvoidrun(){      Stringdate=newThreadLocalNormalUsage03().date(finalI);      System.out.println(date);    }  });}threadPool.shutdown();}publicStringdate(intseconds){  //參數的單位是毫秒,從1970.1.100:00:00GMT開始計時  Datedate=newDate(1000*seconds);  returndateFormat.format(date);}}/<code>

運行結果

出現了秒數相同的打印結果,這顯然是不正確的。

盤他!ThreadLocal 一次解決老大難問題

出現問題的原因

盤他!ThreadLocal 一次解決老大難問題

多個線程的task指向了同一個SimpleDateFormat對象,SimpleDateFormat是非線程安全的。

解決問題的方案

方案1:加鎖

格式化代碼是在最後一句return dateFormat.format(date);,所以可以為最後一句代碼添加synchronized鎖

<code>publicStringdate(intseconds){  //參數的單位是毫秒,從1970.1.100:00:00GMT開始計時  Datedate=newDate(1000*seconds);  Strings;  synchronized(ThreadLocalNormalUsage04.class){    s=dateFormat.format(date);  }  returns;}/<code>

運行結果

盤他!ThreadLocal 一次解決老大難問題

運行結果中沒有發現相同的時間,達到了線程安全的目的

缺點:因為添加了synchronized,所以會保證同一時間只有一條線程可以執行,這在高併發場景下肯定不是一個好的選擇,所以看看其他方案吧。

方案2:使用ThreadLocal

<code>/***利用ThreadLocal給每個線程分配自己的dateFormat對象*不但保證了線程安全,還高效的利用了內存*/publicclassThreadLocalNormalUsage05{  publicstaticExecutorServicethreadPool=Executors.newFixedThreadPool(10);publicstaticvoidmain(String[]args)throwsInterruptedException{  for(inti=0;i<1000;i++){    intfinalI=i;    //提交任務t    hreadPool.submit(newRunnable(){                     @Override                     publicvoidrun(){      Stringdate=newThreadLocalNormalUsage05().date(finalI);      System.out.println(date);    }  });}threadPool.shutdown();}publicStringdate(intseconds){  //參數的單位是毫秒,從1970.1.100:00:00GMT開始計時  Datedate=newDate(1000*seconds);  //獲取SimpleDateFormat對象  SimpleDateFormatdateFormat=ThreadSafeFormatter.dateFormatThreadLocal.get();  returndateFormat.format(date);}}classThreadSafeFormatter{  publicstaticThreadLocal<simpledateformat>dateFormatThreadLocal=newThreadLocal<simpledateformat>(){    //創建一份SimpleDateFormat對象    @Override    protectedSimpleDateFormatinitialValue(){      returnnewSimpleDateFormat("yyyy-MM-ddhh:mm:ss");    }  };}/<simpledateformat>/<simpledateformat>/<code>

運行結果

盤他!ThreadLocal 一次解決老大難問題

使用了ThreadLocal後不同的線程不會有共享的 SimpleDateFormat 對象,所以也就不會有線程安全問題

2.2 實踐場景2

當前用戶信息需要被線程內的所有方法共享

方案1:傳遞參數

盤他!ThreadLocal 一次解決老大難問題

可以將user作為參數在每個方法中進行傳遞,

缺點:但是這樣做會產生代碼冗餘問題,並且可維護性差。

方案2:使用Map

對此進行改進的方案是使用一個Map,在第一個方法中存儲信息,後續需要使用直接get()即可,

盤他!ThreadLocal 一次解決老大難問題

缺點:如果在單線程環境下可以保證安全,但是在多線程環境下是不可以的。如果使用加鎖和ConcurrentHashMap都會產生性能問題。

方案3:使用ThreadLocal,實現不同方法間的資源共享

使用 ThreadLocal 可以避免加鎖產生的性能問題,也可以避免層層傳遞參數來實現業務需求,就可以實現不同線程中存儲不同信息的要求。

盤他!ThreadLocal 一次解決老大難問題

<code>/***演示 ThreadLocal 的用法2:避免參數傳遞的麻煩*/publicclassThreadLocalNormalUsage06{  publicstaticvoidmain(String[]args){    newService1().process();  }}classService1{    publicvoidprocess(){      Useruser=newUser("魯毅");      //將User對象存儲到holder中      UserContextHolder.holder.set(user);      newService2().process();    }}classService2{  publicvoidprocess(){    Useruser=UserContextHolder.holder.get();    System.out.println("Service2拿到用戶名:"+user.name);    newService3().process();  }}classService3{    publicvoidprocess(){      Useruser=UserContextHolder.holder.get();      System.out.println("Service3拿到用戶名:"+user.name);    }}classUserContextHolder{  publicstaticThreadLocal<user>holder=newThreadLocal<>();}classUser{  Stringname;  publicUser(Stringname){    this.name=name;  }}/<user>/<code>

運行結果

盤他!ThreadLocal 一次解決老大難問題

3.對ThreadLocal的總結

  • 讓某個需要用到的對象實現線程之間的隔離(每個線程都有自己獨立的對象)
  • 可以在任何方法中輕鬆的獲取到該對象
  • 根據共享對象生成的時機選擇使用initialValue方法還是set方法
  • 對象初始化的時機由我們控制的時候使用initialValue 方式
  • 如果對象生成的時機不由我們控制的時候使用 set 方式

4.使用ThreadLocal的好處

  • 達到線程安全的目的
  • 不需要加鎖,執行效率高
  • 更加節省內存,節省開銷
  • 免去傳參的繁瑣,降低代碼耦合度

5.ThreadLocal原理

盤他!ThreadLocal 一次解決老大難問題

  • Thread
  • ThreadLocal
  • ThreadLocalMap

在Thread類內部有有ThreadLocal.ThreadLocalMap threadLocals = null;這個變量,它用於存儲ThreadLocal,因為在同一個線程當中可以有多個ThreadLocal,並且多次調用get()所以需要在內部維護一個ThreadLocalMap用來存儲多個ThreadLocal

5.1 ThreadLocal相關方法

T initialValue()

該方法用於設置初始值,並且在調用get()方法時才會被觸發,所以是懶加載。

但是如果在get()之前進行了set()操作,這樣就不會調用initialValue()。

通常每個線程只能調用一次本方法,但是調用了remove()後就能再次調用

<code>publicTget(){  Threadt=Thread.currentThread();  ThreadLocalMapmap=getMap(t);  //獲取到了值直接返回resule  if(map!=null){    ThreadLocalMap.Entrye=map.getEntry(this);    if(e!=null){      @SuppressWarnings("unchecked")      Tresult=(T)e.value;      returnresult;    }  }//沒有獲取到才會進行初始化  returnsetInitialValue();}privateTsetInitialValue(){  //獲取initialValue生成的值,並在後續操作中進行set,最後將值返回  Tvalue=initialValue();Threadt=Thread.currentThread();  ThreadLocalMapmap=getMap(t);  if(map!=null)    map.set(this,value);  else    createMap(t,value);  returnvalue;}publicvoidremove(){  ThreadLocalMapm=getMap(Thread.currentThread());  if(m!=null)    m.remove(this);}/<code>

void set(T t)

為這個線程設置一個新值

<code>publicvoidset(Tvalue){  Threadt=Thread.currentThread();  ThreadLocalMapmap=getMap(t);  if(map!=null)    map.set(this,value);  else    createMap(t,value);}/<code>

T get()

獲取線程對應的value

<code>publicTget(){  Threadt=Thread.currentThread();  ThreadLocalMapmap=getMap(t);  if(map!=null){    ThreadLocalMap.Entrye=map.getEntry(this);    if(e!=null){      @SuppressWarnings("unchecked")      Tresult=(T)e.value;      returnresult;    }  }  returnsetInitialValue();}/<code>

void remove()

刪除對應這個線程的值

6.ThreadLocal注意點

6.1 內存洩漏

內存洩露;某個對象不會再被使用,但是該對象的內存卻無法被收回

盤他!ThreadLocal 一次解決老大難問題

<code>staticclassThreadLocalMap{  staticclassEntryextendsWeakReference<threadlocal>>{    /**ThevalueassociatedwiththisThreadLocal.*/    Objectvalue;    Entry(ThreadLocal>k,Objectv){      //調用父類,父類是一個弱引用      super(k);      //強引用      value=v;    }}/<threadlocal>/<code>

強引用:當內存不足時觸發GC,寧願拋出OOM也不會回收強引用的內存

弱引用:觸發GC後便會回收弱引用的內存

正常情況

當Thread運行結束後,ThreadLocal中的value會被回收,因為沒有任何強引用了

非正常情況

當Thread一直在運行始終不結束,強引用就不會被回收,存在以下調用鏈 Thread-->ThreadLocalMap-->Entry(key為null)-->value因為調用鏈中的 value 和 Thread 存在強引用,所以value無法被回收,就有可能出現OOM。

JDK的設計已經考慮到了這個問題,所以在set()、remove()、resize()方法中會掃描到key為null的Entry,並且把對應的value設置為null,這樣value對象就可以被回收。

<code>privatevoidresize(){  Entry[]oldTab=table;  intoldLen=oldTab.length;  intnewLen=oldLen*2;  Entry[]newTab=newEntry[newLen];  intcount=0;  for(intj=0;j<oldlen>k=e.get();      //當ThreadLocal為空時,將ThreadLocal對應的value也設置為null      if(k==null){        e.value=null;        //HelptheGC      }else{        inth=k.threadLocalHashCode&(newLen-1);        while(newTab[h]!=null)          h=nextIndex(h,newLen);        newTab[h]=e;        count++;      }    }  }  setThreshold(newLen);  size=count;  table=newTab;}/<oldlen>/<code>

但是隻有在調用set()、remove()、resize()這些方法時才會進行這些操作,如果沒有調用這些方法並且線程不停止,那麼調用鏈就會一直存在,所以可能會發生內存洩漏。

6.2 如何避免內存洩漏(阿里規約)

調用remove()方法,就會刪除對應的Entry對象,可以避免內存洩漏,所以使用完ThreadLocal後,要調用remove()方法。

<code>classService1{  publicvoidprocess(){    Useruser=newUser("魯毅");    //將User對象存儲到holder中    UserContextHolder.holder.set(user);    newService2().process();  }}classService2{  publicvoidprocess(){    Useruser=UserContextHolder.holder.get();    System.out.println("Service2拿到用戶名:"+user.name);    newService3().process();  }}classService3{  publicvoidprocess(){    Useruser=UserContextHolder.holder.get();    System.out.println("Service3拿到用戶名:"+user.name);    //手動釋放內存,從而避免內存洩漏    UserContextHolder.holder.remove();  }}/<code>

6.3 ThreadLocal的空指針異常問題

<code>/***ThreadLocal的空指針異常問題*/publicclassThreadLocalNPE{  ThreadLocal<long>longThreadLocal=newThreadLocal<>();publicvoidset(){  longThreadLocal.set(Thread.currentThread().getId());}publicLongget(){  returnlongThreadLocal.get();}publicstaticvoidmain(String[]args){  ThreadLocalNPEthreadLocalNPE=newThreadLocalNPE();  //如果get方法返回值為基本類型,則會報空指針異常,如果是包裝類型就不會出錯  System.out.println(threadLocalNPE.get());  Threadthread1=newThread(newRunnable(){                              @Override                              publicvoidrun(){    threadLocalNPE.set();    System.out.println(threadLocalNPE.get());  }});thread1.start();}}/<long>/<code>

6.4 空指針異常問題的解決

如果get方法返回值為基本類型,則會報空指針異常,如果是包裝類型就不會出錯。這是因為基本類型和包裝類型存在裝箱和拆箱的關係,造成空指針問題的原因在於使用者。

6.5 共享對象問題

如果在每個線程中ThreadLocal.set()進去的東西本來就是多個線程共享的同一對象,比如static對象,那麼多個線程調用ThreadLocal.get()獲取的內容還是同一個對象,還是會發生線程安全問題。

6.6 可以不使用ThreadLocal就不要強行使用

如果在任務數很少的時候,在局部方法中創建對象就可以解決問題,這樣就不需要使用ThreadLocal。

6.7 優先使用框架的支持,而不是自己創造

例如在Spring框架中,如果可以使用RequestContextHolder,那麼就不需要自己維護ThreadLocal,因為自己可能會忘記調用remove()方法等,造成內存洩漏。

本文僅為自己學習時記下的筆記,參考自慕課:

https://coding.imooc.com/class/409.html


分享到:


相關文章: