本文分如下幾個部分進行來對ThreadLocal進行學習:
1、什麼是ThreadLocal?
2、 Threadlcoal的組成部分和數據結構
3、ThreadLocal主要方法分析
4、ThreadLocal爲啥會有內存泄漏,如何避免?
我們從第一個開始
1、什麼是ThreadLocal?
我們通過一個簡單的demo來看下
public class MyClass {
private static Integer num = 0;
public static void main(String[] args) {
Thread[] threads = new Thread[5];
for (int x =0;x<threads.length;x++){
threads[x] = new Thread(new Runnable() {
@Override
public void run() {
num+=5;
}
}
);
}
for (int x =0;x<threads.length;x++){
threads[x] .start();
}
}
}
輸出結果爲:
09-10 15:11:08.679 6630-6644/= E/Thread: Thread-4996--5
09-10 15:11:08.679 6630-6645/ E/Thread: Thread-4997--10
09-10 15:11:08.679 6630-6646/ E/Thread: Thread-4998--15
09-10 15:11:08.684 6630-6647/ E/Thread: Thread-4999--20
09-10 15:11:08.684 6630-6648/ E/Thread: Thread-5000--25
我們將代碼改造下,加入ThreadLcoal
public class MyClass {
private static Integer num = 0;
ThreadLocal<Integer> integerThreadLocal = new ThreadLocal<Integer>(){
@Nullable
@Override
protected Integer initialValue() {
return num ;
}
};
public static void main(String[] args) {
Thread[] threads = new Thread[5];
for (int x =0;x<threads.length;x++){
threads[x] = new Thread(new Runnable() {
@Override
public void run() {
int i = integerThreadLocal.get().intValue();
i+=5;
integerThreadLocal.set(i);
Log.e("Thread",Thread.currentThread().getName()+"--"+
+integerThreadLocal.get());
}
}
);
}
for (int x =0;x<threads.length;x++){
threads[x] .start();
}
}
}
輸出結果爲:
09-10 15:31:41.084 8415-8449/? E/Thread: Thread-5044--5
09-10 15:31:41.084 8415-8450/? E/Thread: Thread-5045--5
09-10 15:31:41.084 8415-8452/? E/Thread: Thread-5046--5
09-10 15:31:41.084 8415-8454/? E/Thread: Thread-5047--5
09-10 15:31:41.084 8415-8455/? E/Thread: Thread-5048--5
09-10 15:31:47.459 8639-8656/ E/Thread: Thread-5047--5
09-10 15:31:47.459 8639-8657/ E/Thread: Thread-5048--5
09-10 15:31:47.459 8639-8658/ E/Thread: Thread-5049--5
09-10 15:31:47.459 8639-8659/ E/Thread: Thread-5050--5
09-10 15:31:47.459 8639-8660/ E/Thread: Thread-5051--5
我們發現這次每個線程對應的變量 ,互不相互干擾
那什麼是ThreadLcoal? 爲共享變量在每個線程中創建一個副本,每個線程可以訪問自己的內部的副本變量
2、 Threadlcoal的組成部分和數據結構
本來想自己畫張圖的,網絡上有個比較經典的就拿來吧,如果有侵權,可以聯繫刪除
從上圖我們可以看到這些相關的類
Thread 類
成員變量:ThreadLocal.ThreadLocalMap threadLocals
class Thread implements Runnable {
.....
ThreadLocal.ThreadLocalMap threadLocals = null;
.....
}
ThreadLocal類
靜態內部類: ThreadLocalMap
靜態內部類: Entry 是一個繼承WeakReference的,一個hash表結構的靜態內部類
public class ThreadLocal<T> {
............
static class ThreadLocalMap {
............
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
............
}
............
}
3、ThreadLocal主要方法分析
3.1 initialValue()方法;
protected T initialValue() {
return null;
}
該方法的訪問修飾符是protected,該方法爲第一次調用get方法提供一個初始值。默認情況下,第一次調用get方法返回值null。在使用時,我們一般會重寫ThreadLocal的initialValue方法,返回一個我們設定的初始值
3.2 set(T value)方法;
1.public void set(T value)設置當前線程的線程局部變量的值。
/**
* 設置ThreadLocal變量的值
*/
public void set(T value) {
// 獲取當前線程
Thread t = Thread.currentThread();
// 獲取當前線程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
// 如果當前線程的ThreadLocalMap非空
if (map != null)
// 往ThreadLocalMap中添加K-V (2)詳細
map.set(this, value);
else
// 如果當前線程的ThreadLocalMap爲空 (1)詳細
// 創建ThreadLocalMap對象
createMap(t, value);
}
/**
(1)詳細
* 查看createMap(t, value);
*/
void createMap(Thread t, T firstValue) {
//創建t內部的ThreadLocalMap並將firstValue存入
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
/**
* (2)詳細
* 查看createMap(t, value);
*/
private void set(ThreadLocal<?> key, Object value) {
//1 這裏和我們的hashMap非常像,算出下標
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
//遍歷hash表
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
//覆蓋之前的值
if (k == key) {
e.value = value;
return;
}
//如果當前遍歷發現key爲null則要重新計算位置,並且將null清除,這是因爲我們的key是弱/引用的
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
//如果上面的條件都不成立,則新增一個Entry,並判斷是否要擴容
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
3.3 get()方法
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
//不爲null的時候返回 詳細看1
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//否則返回默認值
return setInitialValue();
}
/** 1分析 getEntry
*
*/
private Entry getEntry(ThreadLocal<?> key) {
// key的hashcode & 1111,即保留key的hashcode的低4位
int i = key.threadLocalHashCode & (table.length - 1);
// 獲取hash表位置i處的Entry對象
Entry e = table[i];
// e非空且e的key等於key,說明就是要查找的Entry對象
if (e != null && e.get() == key)
// 返回Entry對象e
return e;
else
// 沒有找到對應的Entry,需要繼續查找 分析2
return getEntryAfterMiss(key, i, e);
}
/**
*分析2 getEntryAfterMiss();
* 直接在hash表對應的槽位上沒有找到對飲的Entry
* 繼續在hash表上查找
*/
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
// hash表的Entry數組
Entry[] tab = table;
// Entry數組的長度
int len = tab.length;
// 如果e非空
while (e != null) {
// 獲取e的key
ThreadLocal<?> k = e.get();
// 如果k等於key,找到對應的Entry
if (k == key)
// 返回Entry對象
return e;
// 如果k爲空
if (k == null)
// 移除這個失效的Entry對象
expungeStaleEntry(i);
else
// 修改i爲hash表的下一個位置
i = nextIndex(i, len);
// 修改e爲tab[i],進入下一次循環
e = tab[i];
}
// 沒有找到對應的Entry對象
return null;
}
分析 expungeStaleEntry(i);
/**
* 清理失效的Entry
*/
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// 清理staleSlot位置的Entry
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// 調整hash表直到後面第一個tab[i]爲null爲止
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 如果key爲null,將該entry置爲null,注意 我們這裏也將value也是設置成null
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
// 下面是新計算一下hash值,如果位置與當前位置不同
// 需要重新找一個位置放該節點。用的也是線性探測法
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
4、ThreadLocal爲啥會有內存泄漏,如何避免?
這裏還是上一張網絡圖片,如有版權聯繫刪除
爲什麼會有內存泄漏?
在上圖我們可以看到清晰一個引用鏈,ThreadLocal在ThreadLocalMap中是以一個弱引用身份被Entry中的Key引用的,因此如果ThreadLocal沒有外部強引用來引用它,那麼ThreadLocal會在下次JVM垃圾收集時被回收。這個時候就會出現Entry中Key已經被回收,出現一個null Key的情況,外部讀取ThreadLocalMap中的元素是無法通過null Key來找到Value的。因此如果當前線程的生命週期很長,一直存在,那麼其內部的ThreadLocalMap對象也一直生存下來,這些null key就存在一條強引用鏈的關係一直存在:Thread --> ThreadLocalMap-->Entry-->Value,這條強引用鏈會導致Entry不會回收,Value也不會回收,但Entry中的Key卻已經被回收的情況,造成內存泄漏。
如何解決?
其實我們在上面源碼分析中已經看到了,在ThreadLocal的get()、set()調用的時候會清除掉線程ThreadLocalMap中所有Entry中Key爲null的Value,並將整個Entry設置爲null,將這個潛在的引用鏈切斷了。
我們也可以在使用完畢ThreadLocal,手動的調用ThreadLocal的remove()方法。