1、什么是ThreadLocal變量
ThreadLoal
變量,線程局部變量,同一個 ThreadLocal
所包含的對象,在不同的 Thread
中有不同的副本。
這里有幾點需要注意:
-
因為每個
Thread
內有自己的實例副本,且該副本只能由當前Thread
使用。這是也是ThreadLocal
命名的由來。 -
既然每個
Thread
有自己的實例副本,且其它Thread
不可訪問,那就不存在多線程間共享的問題。
ThreadLocal
提供了線程本地的實例。它與普通變量的區別在于,每個使用該變量的線程都會初始化一個完全獨立的實例副本。ThreadLocal
變量通常被private static
修飾。當一個線程結束時,它所使用的所有 ThreadLocal
相對的實例副本都可被回收。
總的來說,ThreadLocal
適用于每個線程需要自己獨立的實例且該實例需要在多個方法中被使用,也即變量在線程間隔離而在方法或類間共享的場景。
2、ThreadLocal實現原理
首先 ThreadLocal
是一個泛型類,保證可以接受任何類型的對象。
因為一個線程內可以存在多個 ThreadLocal
對象,所以其實是 ThreadLocal
內部維護了一個 Map ,這個 Map 不是直接使用的 HashMap
,而是 ThreadLocal
實現的一個叫做 ThreadLocalMap
的靜態內部類。而我們使用的 get()
、set()
方法其實都是調用了這個ThreadLocalMap
類對應的 get()
、set()
方法。
例如下面的 set 方法:
1
2
3
4
5
6
7
8
|
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null ) map.set( this , value); else createMap(t, value); } |
get方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
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(); } |
createMap方法:
1
2
3
|
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap( this , firstValue); } |
ThreadLocalMap是個靜態的內部類:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
|
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; } } /** * The initial capacity -- MUST be a power of two. */ private static final int INITIAL_CAPACITY = 16 ; /** * The table, resized as necessary. * table.length MUST always be a power of two. */ private Entry[] table; /** * The number of entries in the table. */ private int size = 0 ; /** * The next size value at which to resize. */ private int threshold; // Default to 0 /** * Set the resize threshold to maintain at worst a 2/3 load factor. */ private void setThreshold( int len) { threshold = len * 2 / 3 ; } /** * Increment i modulo len. */ private static int nextIndex( int i, int len) { return ((i + 1 < len) ? i + 1 : 0 ); } /** * Decrement i modulo len. */ private static int prevIndex( int i, int len) { return ((i - 1 >= 0 ) ? i - 1 : len - 1 ); } /** * Construct a new map initially containing (firstKey, firstValue). * ThreadLocalMaps are constructed lazily, so we only create * one when we have at least one entry to put in it. */ 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); } ... } |
最終的變量是放在了當前線程的 ThreadLocalMap
中,并不是存在 ThreadLocal
上,ThreadLocal
可以理解為只是ThreadLocalMap
的封裝,傳遞了變量值。
3、內存泄漏問題
實際上 ThreadLocalMap
中使用的 key 為 ThreadLocal
的弱引用,弱引用的特點是,如果這個對象只存在弱引用,那么在下一次垃圾回收的時候必然會被清理掉。
所以如果 ThreadLocal
沒有被外部強引用的情況下,在垃圾回收的時候會被清理掉的,這樣一來 ThreadLocalMap
中使用這個 ThreadLocal 的 key 也會被清理掉。但是,value 是強引用,不會被清理,這樣一來就會出現 key 為 null 的 value。
ThreadLocalMap
實現中已經考慮了這種情況,在調用 set()
、get()
、remove()
方法的時候,會清理掉 key 為 null 的記錄。如果說會出現內存泄漏,那只有在出現了 key 為 null 的記錄后,沒有手動調用 remove()
方法,并且之后也不再調用 get()
、set()
、remove()
方法的情況下。
4、使用場景
如上文所述,ThreadLocal
適用于如下兩種場景
每個線程需要有自己單獨的實例
實例需要在多個方法中共享,但不希望被多線程共享
對于第一點,每個線程擁有自己實例,實現它的方式很多。例如可以在線程內部構建一個單獨的實例。ThreadLoca
可以以非常方便的形式滿足該需求。
對于第二點,可以在滿足第一點(每個線程有自己的實例)的條件下,通過方法間引用傳遞的形式實現。ThreadLocal
使得代碼耦合度更低,且實現更優雅。
1)存儲用戶Session
一個簡單的用ThreadLocal來存儲Session的例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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; } |
2)解決線程安全的問題
比如Java7中的SimpleDateFormat不是線程安全的,可以用ThreadLocal來解決這個問題:
1
2
3
4
5
6
7
8
9
10
11
12
|
public class DateUtil { private static ThreadLocal<SimpleDateFormat> format1 = new ThreadLocal<SimpleDateFormat>() { @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat( "yyyy-MM-dd HH:mm:ss" ); } }; public static String formatDate(Date date) { return format1.get().format(date); } } |
這里的DateUtil.formatDate()就是線程安全的了。(Java8里的 [java.time.format.DateTimeFormatter]
(http://docs.oracle.com/javase/8/docs/api/java/time/format/DateTimeFormatter.html) 是線程安全的,Joda time里的DateTimeFormat也是線程安全的)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public class Context { private String name; private String cardId; public String getCardId() { return cardId; } public void setCardId(String cardId) { this .cardId = cardId; } public String getName() { return this .name; } public void setName(String name) { this .name = name; } } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public class ExecutionTask implements Runnable { private QueryFromDBAction queryAction = new QueryFromDBAction(); private QueryFromHttpAction httpAction = new QueryFromHttpAction(); @Override public void run() { final Context context = new Context(); queryAction.execute(context); System.out.println( "The name query successful" ); httpAction.execute(context); System.out.println( "The cardId query successful" ); System.out.println( "The Name is " + context.getName() + " and CardId " + context.getCardId()); } } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
public class QueryFromDBAction { public void execute(Context context) { try { Thread.sleep(1000L); String name = "Jack " + Thread.currentThread().getName(); context.setName(name); } catch (InterruptedException e) { e.printStackTrace(); } } } public void execute(Context context) { String name = context.getName(); String cardId = getCardId(name); context.setCardId(cardId); } private String getCardId(String name) { try { Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } return "444555" + Thread.currentThread().getId(); } } |
1
2
3
4
5
6
7
8
9
10
|
public class ContextTest { public static void main(String[] args) { IntStream.range( 1 , 5 ) .forEach(i -> new Thread( new ExecutionTask()).start() ); } } |
The name query successful
The name query successful
The name query successful
The name query successful
The cardId query successful
The Name is Jack Thread-0 and CardId 44455511
The cardId query successful
The Name is Jack Thread-1 and CardId 44455512
The cardId query successful
The Name is Jack Thread-2 and CardId 44455513
The cardId query successful
The Name is Jack Thread-3 and CardId 44455514
問題:需要在每個調用Context的方法中傳入進去
1
2
|
public void execute(Context context) { } |
3)使用ThreadLocal重新設計一個上下文設計模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
public final class ActionContext { private static final ThreadLocal<Context> threadLocal = new ThreadLocal() { @Override protected Object initialValue() { return new Context(); } }; public static ActionContext getActionContext() { return ContextHolder.actionContext; } public Context getContext() { return threadLocal.get(); } private static class ContextHolder { private final static ActionContext actionContext = new ActionContext(); } } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
public class Context { private String name; private String cardId; public String getCardId() { return cardId; } public void setCardId(String cardId) { this .cardId = cardId; } public String getName() { return this .name; } public void setName(String name) { this .name = name; } } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public class ExecutionTask implements Runnable { private QueryFromDBAction queryAction = new QueryFromDBAction(); private QueryFromHttpAction httpAction = new QueryFromHttpAction(); @Override public void run() { queryAction.execute(); System.out.println( "The name query successful" ); httpAction.execute(); System.out.println( "The cardId query successful" ); final Context context = ActionContext.getActionContext().getContext(); System.out.println( "The Name is " + context.getName() + " and CardId " + context.getCardId()); } } |
1
2
3
4
5
6
7
8
9
10
11
12
13
|
public class QueryFromDBAction { public void execute() { try { Thread.sleep(1000L); String name = "Jack " + Thread.currentThread().getName(); ActionContext.getActionContext().getContext().setName(name); } catch (InterruptedException e) { e.printStackTrace(); } } } |
public class QueryFromHttpAction {
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
public void execute() { Context context = ActionContext.getActionContext().getContext(); String name = context.getName(); String cardId = getCardId(name); context.setCardId(cardId); } private String getCardId(String name) { try { Thread.sleep(1000L); } catch (InterruptedException e) { e.printStackTrace(); } return "444555" + Thread.currentThread().getId(); } } |
1
2
3
4
5
6
7
8
9
10
|
public class ContextTest { public static void main(String[] args) { IntStream.range( 1 , 5 ) .forEach(i -> new Thread( new ExecutionTask()).start() ); } } |
The name query successful
The name query successful
The name query successful
The name query successful
The cardId query successful
The Name is Jack Thread-3 and CardId 44455514
The cardId query successful
The cardId query successful
The Name is Jack Thread-0 and CardId 44455511
The cardId query successful
The Name is Jack Thread-2 and CardId 44455513
The Name is Jack Thread-1 and CardId 44455512
這樣寫 執行過程中不會看到context的定義和聲明
注意:在使用之前記得將上個線程中context舊值清除調,否則會重復調用(比如線程池操作)
4)ThreadLocal注意事項
臟數據
線程復用會產生臟數據。由于結程池會重用Thread
對象,那么與Thread綁定的類的靜態屬性ThreadLocal
變量也會被重用。如果在實現的線程run()
方法體中不顯式地調用remove()
清理與線程相關的ThreadLocal
信息,那么倘若下一個結程不調用set()
設置初始值,就可能get()
到重用的線程信息,包括 ThreadLocal
所關聯的線程對象的value值。
內存泄漏
通常我們會使用使用static
關鍵字來修飾ThreadLocal
(這也是在源碼注釋中所推薦的)。在此場景下,其生命周期就不會隨著線程結束而結束,寄希望于ThreadLocal
對象失去引用后,觸發弱引用機制來回收Entry
的Value
就不現實了。如果不進行remove()
操作,那么這個線程執行完成后,通過ThreadLocal
對象持有的對象是不會被釋放的。
以上兩個問題的解決辦法很簡單,就是在每次用完ThreadLocal時, 必須要及時調用 remove()
方法清理。
父子線程共享線程變量
很多場景下通過ThreadLocal
來透傳全局上下文,會發現子線程的value和主線程不一致。比如用ThreadLocal
來存儲監控系統的某個標記位,暫且命名為traceId
。某次請求下所有的traceld
都是一致的,以獲得可以統一解析的日志文件。但在實際開發過程中,發現子線程里的traceld
為null,跟主線程的并不一致。這就需要使用InheritableThreadLocal
來解決父子線程之間共享線程變量的問題,使整個連接過程中的traceId
一致。
到此這篇關于Java多線程 ThreadLocal原理解析的文章就介紹到這了,更多相關Java多線程 ThreadLocal內容請搜索服務器之家以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持服務器之家!
原文鏈接:https://juejin.cn/post/7022879808967639070