ThreadLocal是一個關于創建線程局部變量的類。
通常情況下,我們創建的變量是可以被任何一個線程訪問并修改的。而使用ThreadLocal創建的變量只能被當前線程訪問,其他線程則無法訪問和修改。
ThreadLocal是如何為每個線程創建變量的副本的:
首先,在每個線程Thread內部有一個ThreadLocal.ThreadLocalMap類型的成員變量threadLocals,這個threadLocals就是用來存儲實際的變量副本的,鍵值為當前ThreadLocal變量,value為變量副本(即T類型的變量)。
初始時,在Thread里面,threadLocals為空,當通過ThreadLocal變量調用get()方法或者set()方法,就會對Thread類中的threadLocals進行初始化,并且以當前ThreadLocal變量為鍵值,以ThreadLocal要保存的副本變量為value,存到threadLocals。
以下代碼展示了如何創建一個ThreadLocal變量:
private ThreadLocal<String> myThreadLocal = new ThreadLocal<>();
@Test
public void testx() {
Thread t = new Thread() {
@Override
public void run() {
myThreadLocal.set("icecrea");
System.out.println(myThreadLocal.get());
}
};
t.start();
Thread t2 = new Thread() {
@Override
public void run() {
System.out.println("t2------"+myThreadLocal.get());
}
};
t2.start();
}
我們可以看到,通過這段代碼實例化了一個ThreadLocal對象。我們只需要實例化對象一次,并且也不需要知道它是被哪個線程實例化。雖然所有的線程都能訪問到這個ThreadLocal實例,但是每個線程卻只能訪問到自己通過調用ThreadLocal的set()方法設置的值。即使是兩個不同的線程在同一個ThreadLocal對象上設置了不同的值,他們仍然無法訪問到對方的值。
當然,我們也可以復習方法設置初始值,這樣上面的t2線程就會打印出初始值。
private ThreadLocal<String> myThreadLocal = new ThreadLocal<String>(){
@Override
public String initialValue(){
return "This is the initial value";
}
};
源碼解讀:
ThreadLocal的set方法,分為下面三步:
- 首先獲取當前線程
- 利用當前線程作為句柄獲取一個ThreadLocalMap的對象
- 如果上述ThreadLocalMap對象不為空,則設置值,否則創建這個ThreadLocalMap對象并設置值
注意: 這里set方法里,第二行獲取的是當前線程里的threadlocals這個變量副本,第四行傳入了this,即threadlocal對象作為key,之后注入到線程的變量副本里。通過ThreadLocal.set()將這個新創建的對象的引用保存到各線程的自己的一個map中,每個線程都有這樣一個map,執行ThreadLocal.get()時,各線程從自己的map中取出放進去的對象,因此取出來的是各自自己線程中的對象,ThreadLocal實例是作為map的key來使用的。
為什么threadLocals的類型ThreadLocalMap的鍵值為ThreadLocal對象?因為每個線程中可有多個threadLocal變量。
public class ThreadLocal<T> {
...
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
...
}
同理,ThreadLocal的get方法,
獲取當前線程,獲取線程持有的ThreadLocalMap,獲取值
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
在Thread類中,持有ThreadLocal.ThreadLocalMap的引用變量。實際上ThreadLocal的值是放入了當前線程的一個ThreadLocalMap實例中,所以只能在本線程中訪問,其他線程無法訪問。
public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
}
InheritableThreadLocal
是不是說ThreadLocal的值只能被一個線程訪問呢?
使用InheritableThreadLocal可以實現多個線程訪問ThreadLocal的值。
原因是Thread類的Init方法(此處只列相關代碼),
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
...
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
...
}
可以看出,使用InheritableThreadLocal可以將某個線程的ThreadLocal值在其子線程創建時傳遞過去。
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
return new ThreadLocalMap(parentMap);
}
private ThreadLocalMap(ThreadLocalMap parentMap) {
Entry[] parentTable = parentMap.table;
int len = parentTable.length;
setThreshold(len);
table = new Entry[len];
for (int j = 0; j < len; j++) {
Entry e = parentTable[j];
if (e != null) {
@SuppressWarnings("unchecked")
ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
if (key != null) {
Object value = key.childValue(e.value);
Entry c = new Entry(key, value);
int h = key.threadLocalHashCode & (len - 1);
while (table[h] != null)
h = nextIndex(h, len);
table[h] = c;
size++;
}
}
}
}
下面代碼子線程可以訪問到父線程中InheritableThreadLocal的值。打印icecrea。(此處如果用threadLocal實例返回的則是Null)
@Test
public void testInheritableThreadLocal() {
final ThreadLocal threadLocal = new InheritableThreadLocal();
threadLocal.set("icecrea");
Thread t = new Thread() {
@Override
public void run() {
super.run();
System.out.println(threadLocal.get());
}
};
t.start();
}
ThreadLocalMap
ThreadLocalMap有靜態內部類Entry,是ThreadLocal的弱引用類型,持有Object類型的引用。持有Entry[]數組。
/**
* 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;
}
}
private Entry[] table;
構造方法如下。通過ThreaLocal和Object值來構造ThreadLocalMap,再回顧上面的ThreadLocal的get方法,就是通過獲取ThreadLocalMap,在調用它的getEntry方法,計算HASH值,定位Entry在table數組中的位置返回,獲取value的值。
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
ThreadLocal會內存泄露么
內存泄漏的定義:對象已經沒有被應用程序使用,但是垃圾回收器沒辦法移除它們,因為還在被引用著。
threadlocal里面使用了一個存在弱引用的map,當釋放掉threadlocal的強引用以后,map里面的value卻沒有被回收.而這塊value永遠不會被訪問到了. 所以存在著內存泄露. 最好的做法是將調用threadlocal的remove方法.
每個thread中都存在一個map, map的類型是ThreadLocal.ThreadLocalMap. Map中的key為一個threadlocal實例. 這個Map的確使用了弱引用,不過弱引用只是針對key. 每個key都弱引用指向threadlocal. 當把threadlocal實例置為null以后,沒有任何強引用指向threadlocal實例,所以threadlocal將會被gc回收. 但是,我們的value卻不能回收,因為存在一條從current thread連接過來的強引用. 只有當前thread結束以后, current thread就不會存在棧中,強引用斷開, Current Thread, Map, value將全部被GC回收.
所以得出一個結論就是只要這個線程對象被gc回收,就不會出現內存泄露,但在threadLocal設為null和線程結束這段時間不會被回收的,就發生了我們認為的內存泄露。最要命的是線程對象不被回收的情況,這就發生了真正意義上的內存泄露。比如使用線程池的時候,線程結束是不會銷毀的,會再次使用的。就可能出現內存泄露。
Java為了最小化減少內存泄露的可能性和影響,在ThreadLocal的get,set的時候都會清除線程Map里所有key為null的value。所以最怕的情況就是,threadLocal對象設null了,開始發生“內存泄露”,然后使用線程池,這個線程結束,線程放回線程池中不銷毀,這個線程一直不被使用,或者分配使用了又不再調用get,set方法,那么這個期間就會發生真正的內存泄露。
對于單獨的java文件,要如下設置(參數不能放在Test后面)
java -Xms64m -Xmx256m Test
Linux tomcat下:
在/usr/local/apache-tomcat-5.5.23/bin目錄下的catalina.sh添加:JAVA_OPTS='-Xms512m -Xmx1024m'要加“m”說明是MB,否則就是KB了,在啟動tomcat時會報內存不足。
初始堆大小-Xms64m
最大堆大小 -Xmx256m
不知道springboot下如何設置tomcat?試了下面這個和類似的都沒成功
mvn spring-boot:run -DXms=64m -DXmx=256m
我的解決方案是:mvn package, 然后java -jar -Xms64m -Xmx256m xxx.war 這樣設置成功
測試代碼如下:
private ThreadLocal<List<String>> buffer=new ThreadLocal<>();
@RequestMapping("threadLocal")
@ResponseBody
public String threadLocal(){
List<String> list= Lists.newArrayList();
for(int i=0;i<1024000;i++){
list.add(String.valueOf(i));
}
buffer.set(list);
return "success";
}
報錯信息如下
Exception in thread "http-nio-8080-exec-4" java.lang.OutOfMemoryError: GC overhead limit exceeded
at javax.management.ObjectName.quote(ObjectName.java:1832)
at org.apache.coyote.AbstractProtocol.getName(AbstractProtocol.java:385)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.register(AbstractProtocol.java:1087)
at org.apache.coyote.AbstractProtocol$ConnectionHandler.process(AbstractProtocol.java:857)
at org.apache.tomcat.util.net.NioEndpoint$SocketProcessor.doRun(NioEndpoint.java:1459)
at org.apache.tomcat.util.net.SocketProcessorBase.run(SocketProcessorBase.java:49)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at org.apache.tomcat.util.threads.TaskThread$WrappingRunnable.run(TaskThread.java:61)
at java.lang.Thread.run(Thread.java:748)
觀察發現,value大內存并沒有回收
解決方案:添加 buffer.remove();方法手動回收Entry,解決了value無法回收的問題。
/**
* Remove the entry for key.
*/
private void 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;
}
}
}
應用場景
解決 數據庫連接、Session管理問題
數據庫連接如果用常規方式,多線程訪問要加鎖,要互相等待,降低效率。可以使用threadlocal,線程不用相互等待,且他們之間沒有關聯。但增加了內存開銷。
private static ThreadLocal<Connection> connectionHolder= new ThreadLocal<Connection>() {
public Connection initialValue() {
return DriverManager.getConnection(DB_URL);
}
};
public static Connection getConnection() {
return connectionHolder.get();
}
private static final ThreadLocal threadSession = new ThreadLocal();
public static Session getSession() throws InfrastructureException {
Session s = (Session) threadSession.get();
try {
if (s == null) {
s = getSessionFactory().openSession();
threadSession.set(s);
}
} catch (HibernateException ex) {
throw new InfrastructureException(ex);
}
return s;
}
參考文章:
https://www.cnblogs.com/dolphin0520/p/3920407.html
https://www.cnblogs.com/onlywujun/p/3524675.html