【線程】ThreadLocal 內存泄漏問題(十五)

我的原則:先會用再說,內部慢慢來。
學以致用,根據場景學源碼


一、架構圖

在這裏插入圖片描述

=== 點擊查看top目錄 ===

二、爲什麼會內存泄漏?

  1. 看下圖,當 threadLocal1,threadLocal2,threadLocal3 = null的時候,table中 Entry的引用依然還在。假如thread01,thread02,一直不結束。那麼Entry中的引用會一直存在(thread01Ref->Thread01->threadLocalMap01->entry->valueRef->valueMemory), 導致在垃圾回收的時候進行可達性分析的時候,value可達從而不會被回收掉,但是該value永遠不能被訪問到,這樣就存在了內存泄漏。
    在這裏插入圖片描述
    === 點擊查看top目錄 ===

三、ThreadLocal的Entry 爲什麼要用 weakReference ?

  • ThreadLocal.ThreadLocalMap.Entry
public class ThreadLocal<T> {
	static class ThreadLocalMap {
		static class Entry extends WeakReference<ThreadLocal<?>> {
		    Object value;
		    Entry(ThreadLocal<?> k, Object v) {
		        super(k);
		        value = v;
		    }
		}
	}
}
  • Thread
public java.lang.class Thread implements Runnable {
	...     
    ThreadLocal.ThreadLocalMap threadLocals = null;
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    ...
}
  • Thread的 exit 方法
    private void exit() {
        if (group != null) {
            group.threadTerminated(this);
            group = null;
        }
        /* Aggressively null out all reference fields: see bug 4006245 */
        target = null;
        /* !!!看這裏!!! */
        threadLocals = null;
         /* !!!看這裏!!! */
        inheritableThreadLocals = null;
        inheritedAccessControlContext = null;
        blocker = null;
        uncaughtExceptionHandler = null;
    }

Thread#exit方法會把內部的threadLocals設置爲 null,threadLocalMap生命週期實際上thread的生命週期相同。

3.1 爲什麼ThreadLocal不用強引用?

  • 因爲weakReference 能夠一定程度上避免內存泄漏

當Thread一直存在,threadLocal1,threadLocal2,threadLocal3 = null的時候。GC看到 threadLocal1,threadLocal2,threadLocal3 指向的對象的只有 weakReference 引用的時候,會將 threadLocal 指向對象的空間進行回收。

  • 假如ThreadLocal2 = null。那麼 GC 看到只有一個弱引用會進行回收。

在這裏插入圖片描述

四、ThreadLocal是如何減少內存泄漏的?

【線程】ThreadLocal 剖析 (十四)

4.1. 泄漏在哪裏?

看上圖 === gc回收threadLocal2 ===,雖然weakReference的 threadLocal2佔用的空間被回收了,但是Entry2(k=null,v=v2) 這個沒用了,屬於髒數據。但是沒被回收,所以泄漏在這裏。除非線程 thread 結束。

4.2 線程什麼時候終結?

  1. 如果你是 new 一個的話,那麼 run方法跑完的了,自然就會結束
  2. 在實際使用中我們都是會用線程池去維護我們的線程,比如在Executors.newFixedThreadPool()時創建線程的時候,爲了複用線程是不會結束的。所以上面的 Entry2(k=null,v=v2) 就會一直存在,所以就泄漏了。

4.3. 源碼如何解決?

在threadLocal的生命週期裏(set,getEntry,remove)裏,都會針對key爲null的髒entry進行處理。每次調用都會去處理一下髒 Entry.

4.4 源碼已經解決了,爲什麼依舊會泄漏?

因爲你得調用4.3所說的那幾個方法(set,getEntry,remove),如果你一直不去理他,不去調用,那麼他自然在。這個解決方案很被動!!!

=== 點擊查看top目錄 ===

五、人工如何解決內存泄漏?

  • 上述 4.4 是被動進行髒數據清理。那麼我們開發時要養成良好習慣,對於 ThreadLocal 我們要進行手動釋放。

5.1 如何進行手動釋放?

  • 每次使用完ThreadLocal,都調用它的remove()方法,清除數據。

  • ThreadLocal#remove 方法

public void ThreadLocal#remove() {
     ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null)
         m.remove(this);
 }
  • ThreadLocal.ThreadLocalMap#remove 方法
private void ThreadLocal.ThreadLocalMap#remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i); // 再清一波髒數據
            return;
        }
    }
}
  • Reference#clear() 方法
public void Reference#clear() {
    this.referent = null;
}

5.2 demo 釋放

//每個人都有一個賬戶,每次買東西都會進行扣費,每個人花的是自己的錢,每個人的賬戶存款都不一樣。一個人就是一條線程,賬戶存款就是線程內的局部變量。
public class _22_TestThreadLocal {
    public static void main(String[] args) {
        wallet wallet = new wallet();
        // 3.  3個線程共享wallet,各自消費
        new Thread(new TaskDemo(wallet), "A").start();
        new Thread(new TaskDemo(wallet), "B").start();
        new Thread(new TaskDemo(wallet), "C").start();
    }

    private static class TaskDemo implements Runnable { //一個人就像一個線程
        private wallet wallet;

        public TaskDemo(wallet wallet) {
            this.wallet = wallet;
        }

        public void run() {
            try{
                for (int i = 0; i < 3; i++) {
                    wallet.spendMoney();

                    // 4. 每個線程打出3個
                    System.out.println(Thread.currentThread().getName() + " --> balance["
                            + wallet.getBalance() + "] ," + "cost [" + wallet.getCost() + "]");
                }
            }finally {
                wallet.removeAll();
            }

        }
    }

    private static class wallet { //一個人就像一個線程
        // 1.通過匿名內部類覆蓋ThreadLocal的initialValue()方法,指定初始值
        private static ThreadLocal<Integer> balance = ThreadLocal.withInitial(new Supplier<Integer>() {
            @Override
            public Integer get() {
                return 100;
            }
        }); // 假設初始賬戶有100塊錢

        private static ThreadLocal<Integer> cost = ThreadLocal.withInitial(() -> 0); // 假設初始賬戶有100塊錢
//        private static ThreadLocal<Integer> balance = new ThreadLocal<>();
//        private static ThreadLocal<Integer> cost = new ThreadLocal<>();

        public int getBalance() {
            return balance.get();
        }

        public int getCost() {
            return cost.get();
        }

        // 2。 消費
        public void spendMoney() {
            int balanceNow = balance.get();
            int costNow = cost.get();
            balance.set(balanceNow - 10); // 每次花10塊錢
            cost.set(costNow + 10);
        }


        public void removeBalance(){
            balance.remove();
            balance = null;
        }

        public void removeCost(){
            cost.remove();
            cost = null;
        }

        public void removeAll(){
            removeBalance();
            removeCost();
        }
    }
}
  • 重點看 remove 方法
public void removeBalance(){
   balance.remove();
    balance = null;
}

public void removeCost(){
    cost.remove();
    cost = null;
}

public void removeAll(){
    removeBalance();
    removeCost();
}

六、番外篇

下一章節:【線程】InheritableThreadLocal 剖析 (十六)
上一章節:【線程】ThreadLocal 剖析 (十四)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章