我的原則:先會用再說,內部慢慢來。
學以致用,根據場景學源碼
一、架構圖
二、爲什麼會內存泄漏?
- 看下圖,當 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 看到只有一個弱引用會進行回收。
- 如果是強引用的話,那麼只能手動設置 table[1] = null 才能把這個對象回收。
=== 點擊查看top目錄 ===
四、ThreadLocal是如何減少內存泄漏的?
4.1. 泄漏在哪裏?
看上圖 === gc回收threadLocal2 ===,雖然weakReference的 threadLocal2佔用的空間被回收了,但是Entry2(k=null,v=v2) 這個沒用了,屬於髒數據。但是沒被回收,所以泄漏在這裏。除非線程 thread 結束。
4.2 線程什麼時候終結?
- 如果你是 new 一個的話,那麼 run方法跑完的了,自然就會結束
- 在實際使用中我們都是會用線程池去維護我們的線程,比如在Executors.newFixedThreadPool()時創建線程的時候,爲了複用線程是不會結束的。所以上面的 Entry2(k=null,v=v2) 就會一直存在,所以就泄漏了。
4.3. 源碼如何解決?
在threadLocal的生命週期裏(set,getEntry,remove)裏,都會針對key爲null的髒entry進行處理。每次調用都會去處理一下髒 Entry.
4.4 源碼已經解決了,爲什麼依舊會泄漏?
因爲你得調用4.3所說的那幾個方法(set,getEntry,remove),如果你一直不去理他,不去調用,那麼他自然在。這個解決方案很被動!!!
五、人工如何解決內存泄漏?
- 上述 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();
}
- 當 balance 與 cost 調用 remove 方法後,GC後續進行垃圾回收
=== 點擊查看top目錄 ===
六、番外篇
下一章節:【線程】InheritableThreadLocal 剖析 (十六)
上一章節:【線程】ThreadLocal 剖析 (十四)