內(nèi)存泄漏(Memory Leak)是指程序中已動(dòng)態(tài)分配的堆內(nèi)存由于某種原因程序未釋放或無法釋放,造成系統(tǒng)內(nèi)存的浪費(fèi),導(dǎo)致程序運(yùn)行速度減慢甚至系統(tǒng)崩潰等嚴(yán)重后果。

ThreadLocal基礎(chǔ)部分
ThreadLoal的作用
保存線程的獨(dú)立變量,這種變量在線程的生命周期內(nèi)起作用,減少同一個(gè)線程內(nèi)多個(gè)函數(shù)之間公共變量傳遞麻煩。
使用場景
需要給不同的線程保存不同的信息時(shí)。
基礎(chǔ)使用
public class TestThreadLocal {
private static ThreadLocal<Integer> threadLocal=new ThreadLocal<Integer>();
// private static ThreadLocal<Integer> threadLocal=new ThreadLocal<Integer>(){
//
// @Override
// protected Integer initialValue() {
// return 0;
// }
// };
public static void main(String[] args) {
Thread t1=new Thread(new Runnable() {
@Override
public void run() {
System.out.println(threadLocal.get());
}
});
Thread t2=new Thread(new Runnable() {
@Override
public void run() {
threadLocal.set(3);
System.out.println("t2:"+threadLocal.get());
}
});
t1.start();
t2.start();
System.out.println(threadLocal.get());
}
}
如果需要設(shè)置默認(rèn)值的話,可以實(shí)現(xiàn)initialValue方法。
典型場景1:我們知道SimpleDateFormat的對象如果多線程使用的話會(huì)有線程不安全的問題。具體代碼如下:
public class TestThreadLocal {
public static ExecutorService executorService = Executors.newFixedThreadPool(16);
private static SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) throws InterruptedException {
for (int i=0;i<1000;i++){
executorService.submit(new Runnable() {
@Override
public void run(){
String format = simpleDateFormat.format(new Date());
try {
Date parse = simpleDateFormat.parse("2021-09-01 00:00:00");
} catch (ParseException e) {
e.printStackTrace();
}
System.out.println(format);
}
});
}
Thread.sleep(3000);
executorService.shutdownNow();
}
}
運(yùn)行結(jié)果如下:

可以看出,發(fā)生了異常。
方法1:我們可以改為每次都new一個(gè)新的SimpleDateFormat對象的話,這樣再運(yùn)行是沒問題的。但是有些資源浪費(fèi)。
方法2:使用ThreadLocal來解決。假設(shè)線程池里共16個(gè)線程,那我們總共16個(gè)SimpleDateFormat對象就可以應(yīng)付所有的日期格式化的調(diào)用。
代碼如下:
public class TestThreadLocal {
public static ExecutorService executorService = Executors.newFixedThreadPool(16);
private static ThreadLocal<SimpleDateFormat> threadLocal=new ThreadLocal<SimpleDateFormat>(){
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
private static SimpleDateFormat simpleDateFormat=new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) throws InterruptedException {
for (int i=0;i<1000;i++){
executorService.submit(new Runnable() {
@Override
public void run() {
String format = threadLocal.get().format(new Date());
try {
Date parse = threadLocal.get().parse("2021-09-01 00:00:00");
} catch (ParseException e) {
e.printStackTrace();
}
System.out.println(format);
}
});
}
Thread.sleep(3000);
executorService.shutdownNow();
}
}
注意: 如果不使用線程池,線程結(jié)束,線程里的threadLocalMap也會(huì)被回收。但是如果使用線程池,線程池里面的線程會(huì)被復(fù)用,線程里的threadLocalMap不會(huì)被回收,就造成了內(nèi)存泄漏。按照正確的使用方法應(yīng)該是每次用完了remove,但是這樣效率就很低。還不如方法1每次去new一個(gè)新的SimpleDateFormat對象。(但個(gè)人覺得其實(shí)還好,泄漏一點(diǎn)也沒關(guān)系,不過threadlocal畢竟不是專門解決線程安全問題的,不推薦這么用)
正確使用方法
- 每次使用完ThreadLocal都調(diào)用它的remove()方法清除數(shù)據(jù)
- 將ThreadLocal變量定義成private static,這樣就一直存在ThreadLocal的強(qiáng)引用,也就能保證任何時(shí)候都能通過ThreadLocal的弱引用訪問到Entry的value值,進(jìn)而清除掉 。
ThreadLocal 高級部分
ThreadLocal為什么會(huì)內(nèi)存泄露?
內(nèi)存泄漏(Memory Leak)是指程序中已動(dòng)態(tài)分配的堆內(nèi)存由于某種原因程序未釋放或無法釋放,造成系統(tǒng)內(nèi)存的浪費(fèi),導(dǎo)致程序運(yùn)行速度減慢甚至系統(tǒng)崩潰等嚴(yán)重后果。
試想:
一個(gè)線程對應(yīng)一塊工作內(nèi)存,線程可以存儲(chǔ)多個(gè)ThreadLocal。那么假設(shè),開啟1萬個(gè)線程,每個(gè)線程創(chuàng)建1萬個(gè)ThreadLocal,也就是每個(gè)線程維護(hù)1萬個(gè)ThreadLocal小內(nèi)存空間,而且當(dāng)線程執(zhí)行結(jié)束以后,假設(shè)這些ThreadLocal里的Entry還不會(huì)被回收,那么將很容易導(dǎo)致堆內(nèi)存溢出。
怎么辦?難道JVM就沒有提供什么解決方案嗎?
答案:
- JVM利用設(shè)置ThreadLocalMap的Key為弱引用,來避免內(nèi)存泄露。
- JVM利用調(diào)用remove、get、set方法的時(shí)候,回收臟value值。
ThreadLocal的關(guān)系圖如下所示:

Thread里面維護(hù)了一個(gè)ThreadLocalMap,這個(gè)map里面的key是弱引用的readLocal實(shí)例。value是我們設(shè)置進(jìn)去的值。當(dāng)把treadLocal實(shí)例對象置為null后,沒有任何強(qiáng)引用指向threadLocal實(shí)例,所以theadLocal將會(huì)被gc回收。但是我們的value不會(huì)被回收,因?yàn)榇嬖谝粋€(gè)thread連接過來的強(qiáng)引用。只有當(dāng)thread結(jié)束后,強(qiáng)引用斷開,map、value等將全部被回收。
如下圖:

但是很多時(shí)候我們使用線程池,為了復(fù)用線程,thread生命周期沒有結(jié)束,所以無法回收,造成內(nèi)存泄漏。