線程池-Threadlocal

ThreadLoclc初衷是線程併發時,解決變量共享問題,可是因爲過分設計,好比弱引用的和哈希碰撞,致使理解難度大、使用成本高,反而成爲故障高發點,容易出現內存泄露,髒數據、貢獻對象更新等問題。單從ThreadLoacl命名來看人們認爲只要用它就對了,包治變量共享問題,然而並非。一下之內存模型、弱引用,哈希算法爲鋪墊,而後從cs真人遊戲的示例代碼入手,詳細分析Threadlocal源碼。咱們從中能夠學習到全新的編程思惟方式,並認識到問題的來源,也可以幫助咱們諳熟此類設計之道,揚長避短。java

引用類型算法

對象在堆上建立以後所持有的引用實際上是一種變量類型,引用之間能夠經過賦值構成一條引用鏈。從GC Roots 開始遍歷,判斷引用是否可達。引用的可達性是判斷可否被垃圾回收的基本條件。JVM會據此自動管理內存分配與回收,不須要開發工程師干預。可是在某些場景下,即便引用可達,也但願根據語義的強弱進行有選擇的回收,以保證系統的正常運行。根據引用類型語義的強弱來決定垃圾回收的階段,咱們能夠把引用分爲強引用,軟引用,弱引用和虛引用四類。後三類引用,本質上可讓開發工程師經過代碼的方式來決定對象的垃圾回收時機。咱們先簡要了解一下這個四類引用。編程

強引用,即Strong Reference , 最爲常見,如Object object = new Object();這樣的變量聲明和定義就會產生該對象的強引用。只要對象有強引用指向,而且GC roots 可達,那麼java內存回收時,即便瀕臨內存耗盡,也不會回收該對象。緩存

軟引用,即soft Reference ,引用力度弱於"強引用",是用在非必須對象的場景。在即將OOM以前,垃圾回收器會把這些軟引用指向的對象加入回收範圍,以得到更多的內存空間,讓程序可以繼續健康運行。主要用來緩存服務器中間計算結果集不須要試試保存的用戶行爲等。安全

弱引用,即Weak Reference,引用強度較前二者更弱,也是用來描述非必須對象的。若是弱引用指向的對象只存在弱引用這一條線路,則在下一次YGC的時候被回收。因爲YGC時間的不肯定性,弱引用什麼時候被回收也有不肯定性。弱引用主要用於指向某個易消失的對象,在強引用斷開後,此引用不會劫持對象。調用WeakReference.get() 可能返回null,要注意空指針異常。服務器

虛引用,即Phantom Reference ,是極弱的一種引用關係,定義完成後,就沒法經過該引用獲取指定的對象。爲對象設置虛引用的惟一目的就是但願能在這個對象被回收時收到一個系統通知,虛引用必須與引用隊列聯合使用,當垃圾回收時,若是發現存在虛引用,就會在回收對象內存前,把這個虛引用加入與之關聯的引用隊列中。session

強引用是最經常使用的,而虛引用在業務中幾乎很難用到。下面重點介紹一下軟引用和弱引用。先來講明一下軟引用的回收機制。首先設置JVM 參數:-Xms 20m,-Xmx 20m,即只有20m的堆內存空間。多線程

 1 public class SoftReferenceHouse {
 2     public static void main(String[] args) {
 3         //List<House> houses = new ArrayList<>(); //(第1處)
 4         List<SoftReference> houses = new ArrayList<>();
 5 
 6         //劇情反轉註釋處
 7         int i = 0;
 8         while (true){
 9             //houses.add(new House()); //(第2處)
10 
11             //劇情反轉註釋處
12             SoftReference<House> buyer2 = new SoftReference<>(new House());
13 
14             //劇情反轉註釋處
15             houses.add(buyer2);
16             System.out.println("i=" + (++i));
17         }
18     }
19 }
20 
21 class House{
22     private static final Integer DOOR_NUMBER = 2000;
23     public Door [] doors = new Door[DOOR_NUMBER];
24     class Door{}
25 }

new House() 是匿名對象,產生以後即賦值給軟引用。正常運行一段時間後,內存達到耗盡的臨界狀態。架構

 

ThreadLoacl 價值併發

咱們從真人 CS 遊戲提及。遊戲開始時,每一個人可以領到一把電子槍,槍把上有三個數字,子彈數,殺敵數,本身的命數,爲其設置的初始值分別爲:1500,0,10.假設戰場上每一個人都是一個線程,那麼這三個出事值寫在哪裏呢?若是每一個線程寫死這三個值,萬一將初始字段數統一改爲1000發呢?若是共享,那麼線程直接的併發修改會致使數據不許確。能不能構造這樣一個對象,將這個對象設置爲共享變量,統一設置初始值,可是每一個線程都這個值的修改都是相互獨立的。這個對象就是ThreadLoacl。注意不能將其翻譯成線程本地化或者本地線程。英語恰當的名稱應該叫作:CopyValueIntoEveryThread。具體代碼示例以下:

 

 1 /**
 2  * @Author: MikeWang
 3  * @Date: 2019/1/13 3:38 PM
 4  * @Description:
 5  */
 6 public class CsGameByThreadLoacl {
 7     private static final Integer BULLET_NUMBER = 1500;
 8     private static final Integer KILLED_ENEMIES = 0;
 9     private static final Integer LIFE_VALUE = 10;
10     private static final Integer TOTAL_PLAYERS = 10;
11     //隨機數用來展現每一個對象的不一樣的數據(第1處)
12     private static final ThreadLocalRandom RANDOM = ThreadLocalRandom.current();
13 
14     //初始化子彈數
15     private static final ThreadLocal<Integer> BULLET_NUMBER_THREADLOCAL = new ThreadLocal<Integer>(){
16         @Override
17         protected Integer initialValue() {
18             return BULLET_NUMBER;
19         }
20     };
21     //初始化殺敵數
22     private static final ThreadLocal<Integer> KILLED_ENEMIES_THREADLOCAL = new ThreadLocal<Integer>(){
23         @Override
24         protected Integer initialValue() {
25             return KILLED_ENEMIES;
26         }
27     };
28     //初始化本身的命數
29     private static final ThreadLocal<Integer> LIFE_VALUE_THREADLOCAL = new ThreadLocal<Integer>(){
30         @Override
31         protected Integer initialValue() {
32             return LIFE_VALUE;
33         }
34     };
35 
36 
37     //定義每位隊員
38     private static class Player extends Thread{
39         @Override
40         public void run(){
41             Integer bullets = BULLET_NUMBER_THREADLOCAL.get() - RANDOM.nextInt(BULLET_NUMBER);
42             Integer killEnemies = KILLED_ENEMIES_THREADLOCAL.get() + RANDOM.nextInt(TOTAL_PLAYERS/2);
43             Integer lifeValue = LIFE_VALUE_THREADLOCAL.get() - RANDOM.nextInt(LIFE_VALUE);
44 
45             System.out.println(getName()+", BULLET_NUMBER is "+ bullets);
46             System.out.println(getName()+", KILLED_ENEMIES is "+ killEnemies);
47             System.out.println(getName()+", LIFE_VALUE is "+ lifeValue +"\n");
48 
49             BULLET_NUMBER_THREADLOCAL.remove();
50             BULLET_NUMBER_THREADLOCAL.remove();
51             BULLET_NUMBER_THREADLOCAL.remove();
52         }
53     }
54 
55     public static void main(String[] args) {
56 
57         for (int i = 0 ; i < TOTAL_PLAYERS;i++){
58             new Player().start();
59         }
60     }
61 }

 

此例中,沒有進行set 操做,那麼初始值又是如何進入每一個線程成爲獨立拷貝的呢?首先,雖然ThreadLocal 在定義時覆寫了initiaValue() 方法,但並不是是在 BULLET_NUMBER_THREADLOCAL

對象加載靜態變量的時候執行的,而是每一個線程在ThreadLoacl.get() 的時候都會執行到,其源碼以下:

 

 1 public T get() {
 2         Thread t = Thread.currentThread();
 3         ThreadLocalMap map = getMap(t);
 4         if (map != null) {
 5             ThreadLocalMap.Entry e = map.getEntry(this);
 6             if (e != null) {
 7                 @SuppressWarnings("unchecked")
 8                 T result = (T)e.value;
 9                 return result;
10             }
11         }
12         return setInitialValue();
13     }

每一個線程都有本身的ThreadLoaclMap , 若是 map == null ,則直接執行setInitiaValue()。若是map 已經建立,就表示Thread 類的ThreadLocals 屬性已經初始化; 若是 e == null ,依然會執行到setInitiaValue()。setInitiaValue()的源碼以下:

 1 private T setInitialValue() {
 2         T value = initialValue();
 3         Thread t = Thread.currentThread();
 4         ThreadLocalMap map = getMap(t);
 5         if (map != null)
 6             map.set(this, value);
 7         else
 8             createMap(t, value);
 9         return value;
10     }

      在 CsGameByThreadLoacl 類的第1處 ,使用了ThreadLocalRandom 生成單獨的Random 實例。此類在JDK7 中引入,它使得每一個線程均可以有本身的隨機數生成器。咱們要避免Random 實例被多線程使用,雖然共享實例是線程安全的,可是會因競爭同一seed 而致使性能降低。 咱們已經知道ThreadLoacl是每一個線程單獨持有的。由於每一個線程都有獨立的變量副本。其餘線程不能訪問,因此不存在線程安全問題,也不會影響程序執行性能。ThreadLocal 對象一般是由private static 修飾的,由於都須要複製進入本地線程,因此非static 做用不大。須要注意的是,ThreadLocal 沒法解決共享對象的更新問題。因此使用某個引用來操做共享對象是,依然須要進行線程同步。

      ThreadLocal 有個靜態內部類叫ThreadLoaclMap,它還有個靜態內部類叫Entry ,在Thread 中的ThreadLocalMap 屬性的賦值是在ThreadLocal 類中的createMap() 中進行的,ThreadLoacl 與 ThreadLoclMap 有三組對應的方法:get()、set()、和remove(),在Threadlocal 中對他們只作校驗和判斷,最終的實現會落在ThreadLocalMap 上。Entry 繼承自WeakReference,沒有方法,只有一個value 成員變量,它的Key 是ThreadLocal對象。二者簡要關係以下:

 

  • 1個Thread 有且僅有一個ThreadLoaclMap 對象;
  • 1個Entry 對象的key 弱應用指向一個ThreadLocal對象;
  • 1個ThreadLocalMap 對象存儲多個Entry 對象;
  • 1個ThreadLocal 對象能夠被多個線程共享;
  • ThreadLocal 對象不持有Value,Value 由線程的Entry 對象持有。

Entry 源碼以下:

static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

全部Entry 對象都被ThreadLocalMap 類實例化對象threadLocals 持有。當線程對象執行完畢時,線程對象內的示例屬性均會被垃圾回收。源碼中weakReference 標識 ThreadLocal 的弱引用,及時線程正在執行中,只要ThreadLoacl對象引用被置成null,Entry 的key 就會在下一次YGC時被垃圾回收。而在ThreadLoacl 使用set() 和get()時,又會自動地將那些 key == null 的Value 置爲null,使value 可以被垃圾回收,避免內存泄露,可是理想很豐滿,現實很骨感,ThreadLocal 如源碼註釋所述:

ThreadLocal instances are typically private static fields in classes.

ThreadLocal 對象 一般做爲私有靜態變量使用,那麼其生命週期至少不會隨着線程池結束而結束。

線程池使用ThreadLocal 有三個重要方法。

set():若是沒有set 操做的ThreadLoacl,容易引發髒讀數據問題。

get():始終沒有get 操做的ThreadLocal 對象是沒有意義的。

remove() : 若是沒有remove 操做,容易引發內存泄露。

若是說一個Thread 是非靜態的,屬於某一個線程實例類,那就失去了線程間共享的本質屬性。那麼ThreadLocal 到底有什麼做用呢?咱們知道,局部變量在方法內各個代碼塊間進行傳遞,而類變量在類方法間進行傳遞。複雜的線程方法可能須要調用多個方法來實現某個功能,這個時候用什麼來傳遞線程內變量呢?答案就是ThreadLocal , 它一般用於同一線程內,跨類,誇方法傳遞數據。若是沒有ThreadLocal ,那麼相互之間的信息傳遞,勢必要靠返回值和參數,這樣無形之中,有些類甚至有些架構會相互耦合。經過將Thread構造方法的最後一個參數設置爲true,能夠把當前線程的變量繼續往下傳遞給它建立子線程。

 

ThreadLocal 反作用

    爲了使線程安全地共享某個變量,JDK 開出了ThreadLocal 這劑藥方,可是藥有三分毒。ThreadLocl 主要會產生髒數據和內存泄露。這兩個問題一般是在線程池的線程中使用ThreadLocal 引起的,由於線程池有線程複用和內存常駐兩個特色。

 1.髒數據

   線程複用會產生髒數據。因爲線程池會重用Thread對象,那麼與Thread綁定的類靜態屬性也會被重用。若是在實現線程run() 方法中不顯示的調用remove() 清理與線程相關的ThreadLocal 信息。若是先一個線程不調用set() 設置初始值,那麼就get() 到重用信息,包括ThreadLocl 所關聯線對象的值。

  髒數據問題在實際故障中十分常見。好比 用戶A下單後沒有看到訂單記錄,而B卻看到了A的訂單記錄。經過排查發現是經過session 優化引發的。在原來的請求中,用戶每次請求Server,都須要去緩存裏查詢用戶的session信息,這樣作無疑增長了一次調用。所以開發工程師決定採用某框架來緩存每一個用戶對應的SecurityContext,它封裝了session 相關信息。優化後雖然爲每個用戶新建了一個session 相關的上下文,可是由於ThreadLoacl 沒有再線程結束是及時進行remove() 清理操做,在高併發場景下,線程池中的線程可能會讀取到上一個線程緩存的用戶信息。爲了便於理解,用一段簡要代碼來模擬,以下所示:

public class DirtyDataInThreadLocal {
    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(1);
        for (int i = 0; i < 2; i++) {
            Mythread mythread = new Mythread();
            pool.execute(mythread);
        }
    }

    private static class Mythread extends Thread{
        private static boolean flag = true;

        @Override
        public void run() {
            if (flag){
                threadLocal.set(this.getName()+". session info .");
                flag = false;
            }
            System.out.println(this.getName()+" 線程是 "+threadLocal.get());
        }
    }
}

執行結果以下:

Thread-0 線程是 Thread-0. session info .
Thread-1 線程是 Thread-0. session info .

內存泄露

在源碼註釋中提示使用static 關鍵字來修改ThreadLocal。在此場景下,寄但願於ThreadLocal對象失去引用後,觸發弱引用機制來回收Entry 的Value 就不現實了。在上例中,若是不進行remove() 操做,那麼這個線程執行完成後,經過ThreadLocal 對象持有的string對象是不會被釋放的。

     以上兩個問題解決的辦法很簡單,就是每次用完ThreadLocal 時,必須調用remove() 方法清理。

 

ThreadLocal 並不解決多線程 共享 變量的問題。

相關文章
相關標籤/搜索