ThreadLocal原理概述
每一個Thread物件均含有一個ThreadLocalMap型別的成員變數threadLocals,它存盤當前執行緒中所有ThreadLocal物件及其對應的值,部分原始碼如下
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
}
而ThreadLocalMap中的核心就是一個個Entry物件,以下是ThreadLocalMap的部分原始碼
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
我們通過一張圖來清楚地表示ThreadLocal的參考關系

ThreadLocal簡單應用
業務需求:有10位顧客去餐廳分別點了1~10十款套餐,要求每個顧客都能準確拿到自己點的套餐
代碼示例(不使用ThreadLocal的情況)
public class Demo {
//餐廳選單
private String menu;
private String getMenu() {
return menu;
}
private void setMenu(String menu) {
this.menu = menu;
}
public static void main(String[] args) {
final Demo demo = new Demo();
for (int i = 1; i <= 10; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
//顧客點餐
demo.setMenu(Thread.currentThread().getName() + "的套餐");
System.out.println("----------------------");
//顧客取餐
System.out.println(Thread.currentThread().getName() + "拿走了" + "["+demo.getMenu()+"]");
}
});
thread.setName("顧客" + i);
thread.start();
}
}
}
運行結果(嘗試多運行幾次,排除程式運行結果出現的偶然性)
D:\installPath\Java\jdk1.8.0_121\bin\java.exe "-javaagent:D:\installPath\IntelliJ IDEA 2019.1.4\lib\idea_rt.jar=56135:D:\installPath\IntelliJ IDEA 2019.1.4\bin" -Dfile.encoding=UTF-8 -classpath D:\installPath\Java\jdk1.8.0_121\jre\lib\charsets.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\deploy.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\access-bridge-64.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\cldrdata.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\dnsns.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\jaccess.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\jfxrt.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\localedata.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\nashorn.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\sunec.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\sunjce_provider.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\sunmscapi.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\sunpkcs11.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\zipfs.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\javaws.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\jce.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\jfr.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\jfxswt.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\jsse.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\management-agent.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\plugin.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\resources.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\rt.jar;D:\Users\13209\IdeaProjects\spring-study\target\classes com.kang.controller.Demo
----------------------
----------------------
----------------------
顧客6拿走了[顧客5的套餐]
----------------------
----------------------
----------------------
顧客9拿走了[顧客9的套餐]
顧客3拿走了[顧客7的套餐]
----------------------
----------------------
顧客8拿走了[顧客10的套餐]
顧客4拿走了[顧客6的套餐]
----------------------
顧客1拿走了[顧客6的套餐]
顧客5拿走了[顧客10的套餐]
顧客7拿走了[顧客10的套餐]
----------------------
顧客2拿走了[顧客9的套餐]
顧客10拿走了[顧客10的套餐]
Process finished with exit code 0
我們開啟了10個執行緒分別表示10個顧客,由于執行緒之間相互沒有隔離,導致很多套餐被拿錯,甚至有些套餐沒人拿,這顯然不符合我們的業務需求,那么我們在程式中引入ThreadLocal物件來看一下運行結果
代碼示例(引入ThreadLocal物件)
public class Demo {
//引入ThreadLocal物件
ThreadLocal<String> threadLocal=new ThreadLocal();
//餐廳選單
private String menu;
private String getMenu() {
//return menu;
return threadLocal.get();
}
private void setMenu(String menu) {
//this.menu = menu;
//通過ThreadLocal物件將變數menu系結到當前執行緒
threadLocal.set(menu);
}
public static void main(String[] args) {
final Demo demo = new Demo();
for (int i = 1; i <= 10; i++) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
//顧客點餐
demo.setMenu(Thread.currentThread().getName() + "的套餐");
System.out.println("----------------------");
//顧客取餐
System.out.println(Thread.currentThread().getName() + "拿走了" + "["+demo.getMenu()+"]");
}
});
thread.setName("顧客" + i);
thread.start();
}
}
}
運行結果(嘗試多運行幾次,排除程式運行結果出現的偶然性)
D:\installPath\Java\jdk1.8.0_121\bin\java.exe "-javaagent:D:\installPath\IntelliJ IDEA 2019.1.4\lib\idea_rt.jar=56301:D:\installPath\IntelliJ IDEA 2019.1.4\bin" -Dfile.encoding=UTF-8 -classpath D:\installPath\Java\jdk1.8.0_121\jre\lib\charsets.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\deploy.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\access-bridge-64.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\cldrdata.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\dnsns.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\jaccess.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\jfxrt.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\localedata.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\nashorn.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\sunec.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\sunjce_provider.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\sunmscapi.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\sunpkcs11.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\zipfs.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\javaws.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\jce.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\jfr.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\jfxswt.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\jsse.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\management-agent.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\plugin.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\resources.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\rt.jar;D:\Users\13209\IdeaProjects\spring-study\target\classes com.kang.controller.Demo
----------------------
----------------------
----------------------
顧客2拿走了[顧客2的套餐]
顧客3拿走了[顧客3的套餐]
----------------------
----------------------
顧客1拿走了[顧客1的套餐]
----------------------
----------------------
----------------------
顧客5拿走了[顧客5的套餐]
----------------------
顧客7拿走了[顧客7的套餐]
----------------------
顧客6拿走了[顧客6的套餐]
顧客8拿走了[顧客8的套餐]
顧客9拿走了[顧客9的套餐]
顧客10拿走了[顧客10的套餐]
顧客4拿走了[顧客4的套餐]
Process finished with exit code 0
這樣的設計便實作了業務需求,由此我們可以看出ThreadLocal類的作用是:提供執行緒內的區域變數,不同執行緒之間不會相互干擾,這種變數僅在執行緒的生命周期內起作用,
ThreadLocal的記憶體泄漏問題
首先我們先了解一下記憶體泄漏和記憶體溢位的概念
記憶體泄漏(Memory Leak):程式中已動態分配的堆記憶體由于某種原因程式未釋放或無法釋放,造成系統記憶體的浪費,導致程式運行速度減慢甚至系統崩潰等嚴重后果,我們都知道記憶體泄漏的堆積終將導致嚴重的記憶體溢位,
記憶體溢位(Out Of Memory):無法給申明的物件提供足夠的記憶體空間,程式無法再正常運行,也就是我們常說的OOM,
說到這,我們順便來回顧一下Java物件的四大參考
強參考(Strong Reference):最普遍使用的參考,垃圾回收器不會回收一個持有強參考的物件,即使記憶體空間不足時,Java虛擬機寧愿拋出Out Of Memory Error 錯誤,終止程式運行,也不會依靠回收具有強參考的物件來解決記憶體空間不足的問題,
軟參考(Soft Reference):一個只持有軟參考的物件,只要記憶體空間充足,垃圾回收器就不會回收它,一旦記憶體空間不足,就會回收這些物件的記憶體來保證程式的運行,
弱參考(Weak Reference):只持有弱參考的物件比只持有軟參考的物件擁有更加短暫的生命周期,當垃圾回收器執行緒掃描到只具有弱參考的物件,不管當前記憶體空間是否充足,都會回收這些物件的記憶體,不過,垃圾回收器執行緒是一個優先級很低的執行緒,所以不一定會很快發現那些只具有弱參考的物件,
虛參考(Phantom Reference):就是字面意思,與其他參考都不同的是虛參考不會決定物件的生命周期,也就是說只持有虛參考的物件就和沒有任何參考一樣,任何時候都可能被垃圾回收器執行緒回收,所以虛參考必須和參考佇列(Reference Queue)聯合使用來發揮自己的作用,
記憶體泄漏的根本原因
所有Entry物件都被ThreadLocalMap類的實體化物件threadLocals持有,當ThreadLocal物件不再使用時,ThreadLocal物件在堆疊中的參考就會被回收,一旦沒有任何參考指向ThreadLocal物件,Entry只持有弱參考的key就會自動在下一次YGC時被回收,而此時持有強參考的Entry物件并不會被回收,
簡而言之: threadLocals物件中的entry物件不在使用后,沒有及時remove該entry物件 ,然而程式自身也無法通過垃圾回識訓制自動清除,從而導致記憶體泄漏,
解決方案:只要在使用完ThreadLocal物件后,呼叫其remove方法洗掉對應的Entry,即可從根本解決問題,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/277165.html
標籤:java
