最近從網上看到一個關於ThreadLocal的問題,
TheadLocal是否會導致內存溢出?
從理論上說是會的,但是要看怎麼使用,既然Java設計了這個東西,肯定是有考慮過它的很多使用場景的,所以大部分情況下其實還是可以放心使用的。
在回答會不會導致OOM之前我們先來了解一下什麼是ThreadLocal?通過英文字義,很容易就可以猜到它作用就是用來保存線程的本地變量。
1.下面我們來分析一下ThreadLocal的原理
我們先來看ThreadLocal的get(),set()方法的源碼(通過源碼去分析它的原理是最直接的)
public void set(T value) {
//獲取當前線程
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
//把this(ThreadLocal)當作map的Key
map.set(this, value);
else
createMap(t, value);
}
public T get() {
//獲取當前線程
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
//把this(ThreadLocal)當作Key,拿到對應的Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
//從當前的線程對象裏面拿出ThreadLocalMap對象
return t.threadLocals;
}
通過源碼可以發現,TheadLocal.set的value保存的位置不是在TheadLocal裏面,而是在當前線程的ThreadLocalMap裏面,並且ThreadLocalMap的key是ThreadLocal本身。當調用get()的通過傳入ThreadLocal本身就可以拿到對應的Entry。接下來我們看到Entry的實現,它繼承了WeakReference類
(WeakReference是Java的弱引用,每次調用GC的時候如果某個對象只存在弱應用,那麼它一定會被回收掉)。
下面是ThreadLocalMap 的Entry 的實現
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
。。。
}
在Entry中,將key傳入了WeakReference的構造函數,也就是說只有Key是弱引用,value是強引用,而threadLocalMap對Entry的引用也是強引用。(如下圖)
由於Key是弱引用,也就是說,當threadLocalMap中的key只要在外面不存在任何強引用的情況threadLocalMap中的key是會被回收的,那key回收了之後有什麼作用呢?我們再來看一下threadLocalMap的set方法實現
private void set(ThreadLocal<?> key, Object value) {
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)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
//當key爲null時,將key爲null的entry的引用去掉
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
通過上面的代碼,我們可以看出來,調用set方法時每次都會去查詢threadLocalMap的entry,如果存在key爲空時,它的entry的將會被清除掉。(調用threadLocalMap的get,remove方法也會)
所以通過上面的代碼分析之後,我們得出了兩個結論:
1.threadlocal變量是緩存在thread對象裏面,threadlocal只是做key來使用,那麼如果thread給回收了,那麼threadlocal的變量緩存也會被回收。
2.threadLocalMap對threadlocal採用的是弱引用, threadlocal在外面不存在強引用時,只要有其他的方法調用了的threadLocalMap的get,set或remove方法,它會清除掉一些已經無效的threadlocal的緩存。
我們知道要導致OOM就得必須不停的創建對象,並且創建的對象不會被回收掉。那麼要threadlocal的緩存不被回收,那需要滿足以下兩點:
1.首先調用Threadlocal的線程,壽命必須要有足夠長(這個是有可能的,很多線程池的線程都會重複使用)
2.如果只是一個threadlocal對象是很難導致OOM的,假如我們的應用程序有1000個線程,那最多這個threadlocal對象使用的本地緩存變量最多也1000個,現在的服務器那麼多的內存,這個佔用還不至於導致內存泄漏,所以我們需要非常氾濫的使用threadlocal變量,而且必須是靜態的,因爲靜態變量的引用纔會永久存在。
分析完了上面的原理之後,我們再來分析一下實際中使用的場景
場景1
class BizContext {
private ThreadLocal<Object> local = new ThreadLocal<>();
public void setVal(Object val) {
local.set(val);
}
public Object getVal() {
return local.get();
}
}
class TestBiz1 {
BizContext context;
public TestBiz1(BizContext context) {
this.context = context;
}
public void doSomething() {
BizContext ctx = this.context;
Random random = new Random();
for (int i = 0; i < 1000; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
ctx.setVal(random.nextInt());
TestBiz2 testBiz2=new TestBiz2(context);
testBiz2.doSomething();
}
});
thread.start();
}
}
}
class TestBiz2 {
BizContext context;
public TestBiz2(BizContext context) {
this.context = context;
}
public void doSomething() {
System.out.println(context.getVal());
}
}
class Test {
public static void main(String[] arr) {
Random random = new Random();
for (int i = 0; i < 1000000; i++) {
BizContext context = new BizContext();
TestBiz1 testBiz1 = new TestBiz1(context);
testBiz1.doSomething();
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
這種場景不會導致內存溢出,爲什麼呢?
1.首先上面例子每次調用testBiz1.doSomething(),都會創建1000個線程,但是線程的壽命都很短,線程使用完之後就會被回收(我們先不管這樣使用性能的好壞),所以ThreadLocal的緩存也會跟着一起回收。
2.在main方法的for循環裏面,每次調用調用完testBiz1.doSomething()方法之後,BizContext 都是新實例化的,舊的BizContext 實例它沒有被任何對象引用,所以它和裏面的ThreadLocal都會被回收掉,所以即使調用的線程即使調用完之後沒有死,在調用它的ThreadLocalMap的set方法時,引用舊的ThreadLocal的entry也會被回收掉。
場景二
class BizContext {
public static ThreadLocal<Object> local1=new ThreadLocal<>();
public static ThreadLocal<Object> local2=new ThreadLocal<>();
public static ThreadLocal<Object> local3=new ThreadLocal<>();
public static ThreadLocal<Object> local4=new ThreadLocal<>();
public static ThreadLocal<Object> local5=new ThreadLocal<>();
public static ThreadLocal<Object> local6=new ThreadLocal<>();
public static ThreadLocal<Object> local7=new ThreadLocal<>();
....
public static ThreadLocal<Object> local10000=new ThreadLocal<>();
}
class Test{
public static void main(String[] arr) {
ExecutorService executorService = new ThreadPoolExecutor(1000, 1000,
0, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1000000),
new ThreadPoolExecutor.DiscardPolicy());
}
for (; ; ) {
Person person = new Person();
executorService.execute(() -> {
BizContext.local1.set(1);
BizContext.local2.set(2);
BizContext.local3.set(3);
BizContext.local4.set(4);
BizContext.local5.set(5);
......
BizContext.local10000.set(10000);
});
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
上面這種場景模擬的是ThreadLocal變量氾濫的情況 (雖然有點誇張,沒有人會可能這樣用,但是這裏只是爲了舉例),這種是可能會發生內存溢出的
結論
ThreadLocal的設計是非常合理的,並不是說用了它就一定會導致內存溢出,正常的使用是不用擔心的。