[多線程] ThreadLocal總結

一  問題拋出

  SimpleDateFormat是非線程安全的,在多線程狀況下會碰見問題:html

  public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        List<String> dateStrList = Lists.newArrayList(
                "2018-04-01 10:00:01",
                "2018-04-02 11:00:02",
                "2018-04-03 12:00:03",
                "2018-04-04 13:00:04",
                "2018-04-05 14:00:05"
        );
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        for (String str : dateStrList) {
            executorService.execute(() -> {//多線程共享同一個simpleDateFormat對象 try {
                    simpleDateFormat.parse(str);
                    TimeUnit.SECONDS.sleep(1);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }
    }

   上述代碼在多線程下可能會拋出異常。算法

   解決方案1,使用局部變量:編程

public static void main(String[] args) {
        ExecutorService executorService = Executors.newCachedThreadPool();
        List<String> dateStrList = Lists.newArrayList(
                "2018-04-01 10:00:01",
                "2018-04-02 11:00:02",
                "2018-04-03 12:00:03",
                "2018-04-04 13:00:04",
                "2018-04-05 14:00:05"
        );
        for (String str : dateStrList) {
            executorService.execute(() -> {
                try {
             SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");                    
            simpleDateFormat.parse(str);
                    TimeUnit.SECONDS.sleep(1);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            });
        }
    }

   這樣雖然解決的線程安全問題,可是每次執行都須要建立一個SimpleDateFormat對象,性能不是很好。安全

   解決方案二,使用線程局部變量:  多線程

/** 
 * 使用ThreadLocal以空間換時間解決SimpleDateFormat線程安全問題
 */
public class DateUtil {
    private static final String DATE_FORMAT = "yyyy-MM-dd HH:mm:ss";
    @SuppressWarnings("rawtypes")
    private static ThreadLocal threadLocal = new ThreadLocal() {
        protected synchronized Object initialValue() {
            return new SimpleDateFormat(DATE_FORMAT);
        }
    };

    public static DateFormat getDateFormat() {
        return (DateFormat) threadLocal.get();
    }

    public static Date parse(String textDate) throws ParseException {
        return getDateFormat().parse(textDate);
    }
}

二  理解ThreadLocal

  ThreadLocal,即線程本地變量。ThreadLocal爲變量在每一個線程中都建立了一個副本,那麼每一個線程能夠訪問本身內部的副本變量。這樣多個線程均可以隨意更改本身線程局部的變量,不會影響到其餘線程。併發

  須要注意的是,ThreadLocal提供的只是一個淺拷貝,若是變量是一個引用類型,那麼就要考慮它內部的狀態是否會被改變,想要解決這個問題能夠經過重寫ThreadLocal的initialValue()函數來本身實現深拷貝,建議在使用ThreadLocal時一開始就重寫該函數。函數

  ThreadLocal與像synchronized這樣的鎖機制是不一樣的。首先,它們的應用場景與實現思路就不同,鎖更強調的是如何同步多個線程去正確地共享一個變量,ThreadLocal則是爲了解決同一個變量如何不被多個線程共享。從性能開銷的角度上來說,若是鎖機制是用時間換空間的話,那麼ThreadLocal就是用空間換時間。post

  一、ThreadLocal提供了一種訪問某個變量的特殊方式:訪問到的變量屬於當前線程,即保證每一個線程的變量不同,而同一個線程在任何地方拿到的變量都是一致的,這就是所謂的線程隔離。性能

  二、若是要使用ThreadLocal,一般定義爲private static類型,在我看來最好是定義爲private static final類型。this

  ThreadLocal能夠總結爲一句話:ThreadLocal的做用是提供線程內的局部變量,這種變量在線程的生命週期內起做用,減小同一個線程內多個函數或者組件之間一些公共變量的傳遞的複雜度。

  先了解一下ThreadLocal類提供的幾個方法:

  public T get() { }  //用來獲取ThreadLocal在當前線程中保存的變量副本
  public void set(T value) { }  //用來設置當前線程中變量的副本
  public void remove() { }  //用來移除當前線程中變量的副本
  protected T initialValue() { }  //一個protected方法,用來返回此線程局部變量的當前線程的初始值,通常是在使用時進行重寫的,它是一個延遲加載方法

一、get()方法解析

  首先咱們來看一下ThreadLocal類是如何爲每一個線程建立一個變量的副本的。先看下get方法的實現:

  public T get() {
      //1.首先獲取當前線程
      Thread t = Thread.currentThread();
      //2.獲取當前線程的ThreadLocalMap對象
      ThreadLocalMap map = getMap(t);
      //3.若是map不爲空,以threadlocal實例爲key獲取到對應Entry,而後從Entry中取出對象便可。
      if (map != null) {
          ThreadLocalMap.Entry e = map.getEntry(this);//這裏的this即ThreadLocal對象
          if (e != null)
              return (T)e.value;
      }
      //若是map爲空,也就是第一次沒有調用set直接get(或者調用過set,又調用了remove)時,爲其設定初始值
      return setInitialValue();
  }  

  首先是取得當前線程,而後經過getMap(t)方法獲取到一個map,map的類型爲ThreadLocalMap,這裏的入參爲當前線程,返回的是當前線程中的實例變量。

  而後接着下面獲取到Entry<key,value>鍵值對,注意這裏獲取鍵值對傳進去的是this即當前ThreadLocal對象,而不是當前線程t。若是獲取成功,則返回value值。若是map爲空,則調用setInitialValue方法初始化value。

   首先看一下getMap方法中作了什麼:

  ThreadLocalMap getMap(Thread t) {
      return t.threadLocals;
  }

  在getMap中,是調用當期線程t,返回當前線程t中的一個成員變量threadLocals,線程Thread類裏持有了一個threadLocals成員變量:

  ThreadLocal.ThreadLocalMap threadLocals = null;

   ThreadLocalMap是ThreadLocal類的一個內部類,ThreadLocalMap的Entry繼承了WeakReference,而且使用ThreadLocal做爲鍵值。

   所以,get()方法的主要操做是獲取屬於當前線程的ThreadLocalMap,若是這個map不爲空,咱們就以當前的ThreadLocal爲鍵,去獲取相應的Entry,Entry是ThreadLocalMap的靜態內部類,它繼承於弱引用,因此在get()方法裏面如第10行同樣調用e.value方法就能夠獲取實際的資源副本值。

  可是若是獲取到的map爲空,說明屬於該線程的資源副本還不存在,則須要去建立資源副本,從代碼中能夠看到是調用setInitialValue()方法,其定義以下:

  private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();//獲取到當前線程
        ThreadLocalMap map = getMap(t);//獲取到當前線程的成語變量 if (map != null)//若是不爲空
            map.set(this, value);//設置值,這裏this即當前ThreadLocal對象 else
            createMap(t, value);//若是map爲空,則須要先初始化一個map再設置值 return value;
    }

   第2行調用initialValue()方法初始化一個值。接下來是判斷線程的ThreadLocalMap是否爲空,不爲空就直接設置值(鍵爲this,值爲value),爲空則建立一個Map,調用方法爲createMap(),其定義以下: 

  void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

   在進行get以前,必須先set,不然會報空指針異常。 若是想在get以前不須要調用set就能正常訪問的話,必須重寫initialValue()方法。所以若是沒有執行set操做初始化Thread的threadLocals,則在建立ThreadLocal時必須重寫initialValue()方法,不然會拋出異常:

   private static ThreadLocal threadLocal = new ThreadLocal() {
        protected synchronized Object initialValue() {
            return new SimpleDateFormat(DATE_FORMAT);
        }
    };

 二、set()方法解析

public void set(T value) {  
    // 獲取當前線程對象  
    Thread t = Thread.currentThread();  
    // 獲取當前線程本地變量Map  
    ThreadLocalMap map = getMap(t);  
    // map不爲空  
    if (map != null)  
        // 存值  
        map.set(this, value);  
    else  
        // 建立一個當前線程本地變量Map  
        createMap(t, value);  
}

  首先經過getMap(Thread t)方法獲取一個和當前線程相關的ThreadLocalMap,而後將變量的值設置到這個ThreadLocalMap對象中,固然若是獲取到的ThreadLocalMap對象爲空,就經過createMap方法建立。

  所以ThreadLocal爲每一個線程建立變量的副本的具體流程以下:

    (1)首先,在每一個線程Thread內部有一個ThreadLocal.ThreadLocalMap類型的成員變量threadLocals,這個threadLocals就是用來存儲實際的變量副本的,鍵值爲當前ThreadLocal變量,value爲變量副本(即T類型的變量)。

    (2)初始時,在Thread裏面,threadLocals爲空,當經過ThreadLocal變量調用get()方法或者set()方法,就會對Thread類中的threadLocals進行初始化,而且以當前ThreadLocal變量爲鍵值,以ThreadLocal要保存的副本變量爲value,存到threadLocals。

    (3)而後在當前線程裏面,若是要使用副本變量,就能夠經過get方法在當前線程的threadLocals裏面查找。

三  ThreadLocal使用的通常步驟

  (1)在多線程的類(如ThreadDemo類)中,建立一個private static類型的ThreadLocal對象threadXxx,用來保存線程間須要隔離處理的對象xxx。  

  (2)在ThreadDemo類中,建立一個獲取要隔離訪問的數據的方法getXxx(),在方法中判斷,若ThreadLocal對象爲null時候,應該new()一個隔離訪問類型的對象,並強制轉換爲要應用的類型。  

  (3)在ThreadDemo類的run()方法中,經過getXxx()方法獲取要操做的數據,這樣能夠保證每一個線程對應一個數據對象,在任什麼時候刻都操做的是這個對象。

七、ThreadLocal 與 synchronized 的對比

  (1)ThreadLocal和synchonized都用於解決多線程併發訪問。可是ThreadLocal與synchronized有本質的區別。synchronized是利用鎖的機制,使變量或代碼塊在某一時該只能被一個線程訪問。而ThreadLocal爲每個線程都提供了變量的副本,使得每一個線程在某一時間訪問到的並非同一個對象,這樣就隔離了多個線程對數據的數據共享。而synchronized卻正好相反,它用於在多個線程間通訊時可以得到數據共享。

  (2)synchronized用於線程間的數據共享,而ThreadLocal則用於線程間的數據隔離。

八、一句話理解ThreadLocal:向ThreadLocal裏面存東西就是向它裏面的Map存東西的,而後ThreadLocal把這個Map掛到當前的線程底下,這樣Map就只屬於這個線程了。

四  ThreadLocal中的內存泄漏

  若是ThreadLocal被設置爲null後,並且沒有任何強引用指向它,根據垃圾回收的可達性分析算法,ThreadLocal將會被回收。這樣一來,ThreadLocalMap中就會含有key爲null的Entry,並且ThreadLocalMap是在Thread中的,只要線程遲遲不結束,這些沒法訪問到的value會造成內存泄漏。爲了解決這個問題,ThreadLocalMap中的getEntry()、set()和remove()函數都會清理key爲null的Entry,如下面的getEntry()函數的源碼爲例。

    private ThreadLocal.ThreadLocalMap.Entry getEntry(ThreadLocal<?> var1) {
            int var2 = var1.threadLocalHashCode & this.table.length - 1;
            ThreadLocal.ThreadLocalMap.Entry var3 = this.table[var2];
            return var3 != null && var3.get() == var1 ? var3 : this.getEntryAfterMiss(var1, var2, var3);
        }

        private ThreadLocal.ThreadLocalMap.Entry getEntryAfterMiss(ThreadLocal<?> var1, int var2, ThreadLocal.ThreadLocalMap.Entry var3) {
            ThreadLocal.ThreadLocalMap.Entry[] var4 = this.table;

            for(int var5 = var4.length; var3 != null; var3 = var4[var2]) {
                ThreadLocal var6 = (ThreadLocal)var3.get();
                if (var6 == var1) {
                    return var3;
                }

                if (var6 == null) {
                    this.expungeStaleEntry(var2);
                } else {
                    var2 = nextIndex(var2, var5);
                }
            }

            return null;
        }

  在上文中咱們發現了ThreadLocalMap的key是一個弱引用,那麼爲何使用弱引用呢?使用強引用key與弱引用key的差異以下:

  • 強引用key:ThreadLocal被設置爲null,因爲ThreadLocalMap持有ThreadLocal的強引用,若是不手動刪除,那麼ThreadLocal將不會回收,產生內存泄漏。

  • 弱引用key:ThreadLocal被設置爲null,因爲ThreadLocalMap持有ThreadLocal的弱引用,即使不手動刪除,ThreadLocal仍會被回收,ThreadLocalMap在以後調用set()、getEntry()和remove()函數時會清除全部key爲null的Entry。

  但要注意的是,ThreadLocalMap僅僅含有這些被動措施來補救內存泄漏問題。若是你在以後沒有調用ThreadLocalMap的set()、getEntry()和remove()函數的話,那麼仍然會存在內存泄漏問題。

 參考:

  一、Java併發編程:深刻剖析ThreadLocal  https://www.cnblogs.com/xiaoxi/p/7755253.html

相關文章
相關標籤/搜索