文章目錄
ThreadLocal是什麼?
ThreadLocal是Java類庫提供的在多線程環境下保證對共享資源安全訪問的類
ThreadLocal與Thread、ThreadLocalMap是什麼關係?
通過對源碼分析發現,ThreadLocalMap是每一個線程Thread類的成員變量,裏面有一個鍵值對數據Entry[] table,可以認爲是一個map。
一個Thread對象持有一個ThreadLocalMap成員變量,而ThreadLocalMap依託Entry靜態類來存儲數據,Entry結構中key表示ThreadLocal,value表示要存儲的數據
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;
}
}
private Entry[] table;
ThreadLocal 有哪些常用方法?
initialValue
該方法會返回當前線程對應的初始值。
此方法默認實現返回null。
protected T initialValue() {
return null;
}
可以使用匿名內部類的方式重寫initialValue(),以便在後續使用中可以初始化副本對象。
這是一個延遲加載的方法,只有在調動get方法的時候纔會出觸發。
當線程第一次使用get訪問變量時,將調用此方法。若線程先調用了set方法,則不會再調用initialValue方法。
請看源代碼,便一目瞭然爲什麼這麼說了
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();
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
當第一次調用get()時,map爲null,代碼流轉到setInitialValue方法,在setInitialValue方法中首先會去讀取initialValue()初始化的值。如果有重寫initialValue方法,則會走到我們重寫的方法裏
每個線程最多調用一次這個方法。但是如果已經調用了remove()後,再調用get()依然可以調用此方法。
public class ThreadLocalDemo {
// private ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
// @Override
// protected Integer initialValue() {
// return 0;
// }
// };
private static ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> {
System.out.println("我是InitialValue方法");
return 0;
});
public static void main(String[] args) {
// threadLocal.set(1);
System.out.println(threadLocal.get());
System.out.println(threadLocal.get());
System.out.println("即將執行remove方法");
threadLocal.remove();
System.out.println(threadLocal.get());
}
}
輸出結果:
我是InitialValue方法
0
0
即將執行remove方法
我是InitialValue方法
0
set(T t) 爲線程設置一個新值
T get() 得到這個線程對應的value
void remove() 刪除對應這個線程的值
ThreadLocal使用須知
1、在ThreadLocal使用之前,一定要使用initialValue
初始化或set(T t)
賦初值,否則可能會報空指針異常
public class ThreadLocalDemo {
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
private static int getValue(){
return threadLocal.get();
}
public static void main(String[] args) {
System.out.println(ThreadLocalDemo.getValue());
}
}
結論:ThreadLocal#initialValue默認實現返回null,而Integer->int需要拆箱,誘發空指針異常。若getValue()返回Integer,在上面的程序不會報錯,但在使用這個數據時依然可能報錯。
2、不要重複造輪子,優先使用框架提供出來的工具類。
ThreadLocal使用舉例
就以SimpleDateFormat爲例,看看ThreadLocal是怎麼幫助其實現線程安全的?
演示多線程下使用SimpleDateFormat格式化時間
public class ThreadNotSafeDemo{
private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
public static void main(String[] args) {
BasicThreadFactory threadFactory = new BasicThreadFactory.Builder().namingPattern("wojiushiwo-pool-%d").build();
ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 15, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(20),
threadFactory, new ThreadPoolExecutor.CallerRunsPolicy());
for (int i = 1; i <= 50; i++) {
long num = i;
executor.submit(() -> {
String format = dateFormat.format(System.currentTimeMillis()+num*1000);
System.out.println(Thread.currentThread().getName() + "當前時間:" + format);
});
}
executor.shutdown();
}
}
上面的代碼演示了使用線程池執行50個解析時間的任務。由於每個任務中的時間都是System.currentTimeMillis()+num*1000
不會重複,所以解析出來的時間也應該不重複纔對。
輸出結果:
wojiushiwo-pool-1當前時間:2020-07-04 02:08:19
wojiushiwo-pool-2當前時間:2020-07-04 02:08:20
wojiushiwo-pool-3當前時間:2020-07-04 02:08:21
wojiushiwo-pool-4當前時間:2020-07-04 02:08:22
wojiushiwo-pool-5當前時間:2020-07-04 02:08:23
wojiushiwo-pool-6當前時間:2020-07-04 02:08:24
wojiushiwo-pool-7當前時間:2020-07-04 02:08:25
wojiushiwo-pool-8當前時間:2020-07-04 02:08:26
wojiushiwo-pool-9當前時間:2020-07-04 02:08:27
wojiushiwo-pool-10當前時間:2020-07-04 02:08:28
wojiushiwo-pool-1當前時間:2020-07-04 02:08:29
wojiushiwo-pool-1當前時間:2020-07-04 02:08:30
wojiushiwo-pool-3當前時間:2020-07-04 02:08:31
wojiushiwo-pool-3當前時間:2020-07-04 02:08:32
wojiushiwo-pool-5當前時間:2020-07-04 02:08:33
wojiushiwo-pool-6當前時間:2020-07-04 02:08:35
wojiushiwo-pool-7當前時間:2020-07-04 02:08:35
wojiushiwo-pool-8當前時間:2020-07-04 02:08:36
wojiushiwo-pool-9當前時間:2020-07-04 02:08:37
wojiushiwo-pool-10當前時間:2020-07-04 02:08:39
wojiushiwo-pool-1當前時間:2020-07-04 02:08:39
wojiushiwo-pool-2當前時間:2020-07-04 02:08:40
wojiushiwo-pool-4當前時間:2020-07-04 02:08:41
wojiushiwo-pool-4當前時間:2020-07-04 02:08:42
wojiushiwo-pool-5當前時間:2020-07-04 02:08:43
wojiushiwo-pool-6當前時間:2020-07-04 02:08:44
wojiushiwo-pool-7當前時間:2020-07-04 02:08:45
wojiushiwo-pool-8當前時間:2020-07-04 02:08:46
wojiushiwo-pool-9當前時間:2020-07-04 02:08:47
wojiushiwo-pool-9當前時間:2020-07-04 02:08:48
wojiushiwo-pool-1當前時間:2020-07-04 02:08:50
wojiushiwo-pool-2當前時間:2020-07-04 02:08:50
wojiushiwo-pool-1當前時間:2020-07-04 02:08:52
wojiushiwo-pool-3當前時間:2020-07-04 02:08:52
wojiushiwo-pool-1當前時間:2020-07-04 02:08:53
wojiushiwo-pool-6當前時間:2020-07-04 02:08:55
wojiushiwo-pool-1當前時間:2020-07-04 02:08:55
wojiushiwo-pool-8當前時間:2020-07-04 02:08:56
wojiushiwo-pool-1當前時間:2020-07-04 02:08:57
wojiushiwo-pool-8當前時間:2020-07-04 02:08:58
wojiushiwo-pool-2當前時間:2020-07-04 02:08:59
wojiushiwo-pool-4當前時間:2020-07-04 02:09:00
wojiushiwo-pool-2當前時間:2020-07-04 02:09:01
wojiushiwo-pool-3當前時間:2020-07-04 02:09:02
wojiushiwo-pool-7當前時間:2020-07-04 02:09:03
wojiushiwo-pool-6當前時間:2020-07-04 02:09:04
wojiushiwo-pool-10當前時間:2020-07-04 02:09:05
wojiushiwo-pool-9當前時間:2020-07-04 02:09:07
wojiushiwo-pool-1當前時間:2020-07-04 02:09:07
wojiushiwo-pool-8當前時間:2020-07-04 02:09:08
發現結果中時間2020-07-04 02:08:50有重複現象,說明SimpleDateFormat在多線程環境下不是線程安全的。
令SimpleDateFormat線程安全的方式有多種,這裏主要討論ThreadLocal
下面以ThreadLocal來演示實現SimpleDateFormat線程安全的輸出時間
public class ThreadSafeDemo {
private static ThreadLocal<SimpleDateFormat> threadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));
public static void main(String[] args) {
BasicThreadFactory threadFactory = new BasicThreadFactory.Builder().namingPattern("wojiushiwo-pool-%d").build();
ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 15, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<>(20),
threadFactory, new ThreadPoolExecutor.CallerRunsPolicy());
for (int i = 1; i <= 50; i++) {
long num = i;
executor.submit(() -> {
String format = threadLocal.get().format(System.currentTimeMillis()+num*1000);
System.out.println(Thread.currentThread().getName() + "當前時間:" + format);
});
}
executor.shutdown();
}
}
輸出結果:
wojiushiwo-pool-15當前時間:2020-07-04 02:20:23
wojiushiwo-pool-15當前時間:2020-07-04 02:19:59
wojiushiwo-pool-15當前時間:2020-07-04 02:20:00
wojiushiwo-pool-15當前時間:2020-07-04 02:20:01
wojiushiwo-pool-15當前時間:2020-07-04 02:20:02
wojiushiwo-pool-15當前時間:2020-07-04 02:20:03
wojiushiwo-pool-15當前時間:2020-07-04 02:20:04
wojiushiwo-pool-15當前時間:2020-07-04 02:20:05
wojiushiwo-pool-15當前時間:2020-07-04 02:20:06
wojiushiwo-pool-15當前時間:2020-07-04 02:20:07
wojiushiwo-pool-15當前時間:2020-07-04 02:20:08
wojiushiwo-pool-15當前時間:2020-07-04 02:20:09
wojiushiwo-pool-15當前時間:2020-07-04 02:20:10
wojiushiwo-pool-14當前時間:2020-07-04 02:20:22
wojiushiwo-pool-1當前時間:2020-07-04 02:19:49
wojiushiwo-pool-15當前時間:2020-07-04 02:20:11
wojiushiwo-pool-14當前時間:2020-07-04 02:20:12
wojiushiwo-pool-1當前時間:2020-07-04 02:20:13
wojiushiwo-pool-15當前時間:2020-07-04 02:20:14
wojiushiwo-pool-14當前時間:2020-07-04 02:20:15
wojiushiwo-pool-1當前時間:2020-07-04 02:20:16
wojiushiwo-pool-15當前時間:2020-07-04 02:20:17
wojiushiwo-pool-14當前時間:2020-07-04 02:20:18
main當前時間:2020-07-04 02:20:24
wojiushiwo-pool-5當前時間:2020-07-04 02:19:53
wojiushiwo-pool-5當前時間:2020-07-04 02:20:25
wojiushiwo-pool-4當前時間:2020-07-04 02:19:52
wojiushiwo-pool-15當前時間:2020-07-04 02:20:26
wojiushiwo-pool-14當前時間:2020-07-04 02:20:27
wojiushiwo-pool-5當前時間:2020-07-04 02:20:28
wojiushiwo-pool-4當前時間:2020-07-04 02:20:29
wojiushiwo-pool-1當前時間:2020-07-04 02:20:30
wojiushiwo-pool-15當前時間:2020-07-04 02:20:31
wojiushiwo-pool-14當前時間:2020-07-04 02:20:32
wojiushiwo-pool-5當前時間:2020-07-04 02:20:33
wojiushiwo-pool-4當前時間:2020-07-04 02:20:34
wojiushiwo-pool-1當前時間:2020-07-04 02:20:35
wojiushiwo-pool-7當前時間:2020-07-04 02:19:55
wojiushiwo-pool-7當前時間:2020-07-04 02:20:36
wojiushiwo-pool-14當前時間:2020-07-04 02:20:37
wojiushiwo-pool-5當前時間:2020-07-04 02:20:38
wojiushiwo-pool-10當前時間:2020-07-04 02:19:58
wojiushiwo-pool-11當前時間:2020-07-04 02:20:19
wojiushiwo-pool-6當前時間:2020-07-04 02:19:54
wojiushiwo-pool-3當前時間:2020-07-04 02:19:51
wojiushiwo-pool-12當前時間:2020-07-04 02:20:20
wojiushiwo-pool-8當前時間:2020-07-04 02:19:56
wojiushiwo-pool-13當前時間:2020-07-04 02:20:21
wojiushiwo-pool-2當前時間:2020-07-04 02:19:50
wojiushiwo-pool-9當前時間:2020-07-04 02:19:57
ThreadLocal爲什麼會內存泄露?
前面有討論過ThreadLocalMap中靜態Entry類,這裏詳細說下
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
首先Entry繼承自弱引用,並且其構造函數中k是使用WeakReference賦值的。所以可以斷定Entry中key是弱引用。而value毫無疑問是強引用。
由JVM知識可以得知,弱引用在垃圾回收時會被主動回收,而強引用只有當GC觸發時纔會被回收。
正常情況下,當線程終止時,保存在ThreadLocal裏的value會被垃圾回收。但是,如果線程不終止(如線程需要保持很久),那麼key對應的value就不會被回收,就會導致內存無法被回收,最終可能出現OOM。
幸好,ThreadLocal中set、remove、rehash方法中會掃描key爲null的Entry,並將對應的value也設置爲null,這樣value就可以被回收了。
若像上面說的,ThreadLocal不再使用,但線程未終止而且會顯式調用set、remove、rehash等方法,那麼內存中的調用鏈就一直存在,極易引起內存泄露。
ThreadLocal如何避免內存泄露?
在使用完ThreadLocal之後手動調用remove方法,刪除對應的Entry對象。
以上,若存在表述不明確或表述錯誤的地方,請指正,謝謝!