ThreadLocal是什么

下图ThreadId类会在每个线程中生成唯一标识符。线程的id在第一次调用threadid.get()时被分配,在随后的调用中保持不变。
ThreadId类利用AtomicInteger原子方法getAndIncrement(CAS),为每个线程创建一个threadId变量,例如第一个线程是1,第二个线程是2…,并提供一个类静态get方法用以获取当前线程ID。

源码解析如下:

翻译之后:

ThreadLocal类用来提供线程内部的局部变量。
这种变量在多线程环境下访问(通过get或set方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量。
ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程的上下文。

可以总结为:
ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。

ThreadLocal如何设计

知道定义之后,如果让你来设计这个功能你会怎么设计,是不是会在每个ThreadLocal类创建一个Map,
然后用线程的ID作为Map的key,实例对象作为Map的value,这样就能达到各个线程的值隔离的效果。
废话不多说我们看下JDK1.8是如何设计的.

ThreadLocal#get方法

创建对下的时候,什么初始化都没有操作,通过定义可以猜到真正起作用的是get和set方法.

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
77
78
79
80
81
82

/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
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();
}

/**
* Variant of set() to establish initialValue. Used instead
* of set() in case user has overridden the set() method.
*
* @return the initial value
*/
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 the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

/**
* Get the entry associated with key. This method
* itself handles only the fast path: a direct hit of existing
* key. It otherwise relays to getEntryAfterMiss. This is
* designed to maximize performance for direct hits, in part
* by making this method readily inlinable.
*
* @param key the thread local object
* @return the entry associated with key, or null if no such
*/
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);
}


/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the map
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

ThreadLocalMap才是主角,其地位图示:

代码看到这里可以发现内部确实是有一个 Entry[]的数组进行存储,这个数组的内部结构是一个key-value的结构.并且该对象是一个弱引用。
总所周知:引用总共分为四种强引用,弱引用,软引用,虚引用。
StrongReference 是 Java 的默认引用实现, 它会尽可能长时间的存活于 JVM 内, 当没有任何对象指向它时 GC 执行后将会被回收。
WeakReference, 顾名思义, 是一个弱引用, 当所引用的对象在 JVM 内不再有强引用时, GC 后 weak reference 将会被自动回收。
SoftReference 于 WeakReference 的特性基本一致, 最大的区别在于 SoftReference 会尽可能长的保留引用直到 JVM 内存不足时才会被回收(虚拟机保证), 这一特性使得 SoftReference 非常适合缓存应用。
PhantomReference 主要用来跟踪对象被垃圾回收器回收的活动,用于对象被回收前的清理工作。他一般和ReferenceQueue配合使用,当PhantomReference指向的对象被jvm回收时,jvm就会把PhantomReference加入到ReferenceQueue中,之后你还要手动释放ReferenceQueue里的内存

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

/**
* 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;

简单解析一下,get方法的流程是这样的:

1:首先获取当前线程

2:根据当前线程获取一个Map

3:如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应entry的value,否则转到5

4:如果entry不为null,则返回e.value,否则转到5

5:Map为空或者e为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map

根据上面Entry方法的源码,我们知道ThreadLocalMap是使用ThreadLocal的弱引用作为Key的。下图是本文介绍到的一些对象之间的引用关系图,实线表示强引用,虚线表示弱引用:

ThreadLocalMap#Entry弱引用危害

如上图,ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永远无法回收,造成内存泄露。

ThreadLocal内存泄露解决方案:

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

/**
* Expunge a stale entry by rehashing any possibly colliding entries
* lying between staleSlot and the next null slot. This also expunges
* any other stale entries encountered before the trailing null. See
* Knuth, Section 6.4
*
* @param staleSlot index of slot known to have null key
* @return the index of the next null slot after staleSlot
* (all between staleSlot and this slot will have been checked
* for expunging).
*/
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;

// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;

// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;

// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}

ThreadLocalMap 也是采用的散列表(Hash)思想来实现的,但是实现方式和 HashMap 不太一样。我们首先看下散列表的相关知识

首先从ThreadLocal的直接索引位置(通过ThreadLocal.threadLocalHashCode & (table.length-1)运算得到)获取Entry e,如果e不为null并且key相同则返回e;

如果e为null或者key不一致则向下一个位置查询,如果下一个位置的key和当前需要查询的key相等,则返回对应的Entry。

否则,如果key值为null,则擦除该位置的Entry,并继续向下一个位置查询。

在这个过程中遇到的key为null的Entry都会被擦除,那么Entry内的value也就没有强引用链,自然会被回收。

仔细研究代码可以发现,set操作也有类似的思想,将key为null的这些Entry都删除,防止内存泄露。

但是光这样还是不够的,上面的设计思路依赖一个前提条件:要调用ThreadLocalMap的getEntry函数或者set函数。

这当然是不可能任何情况都成立的,所以很多情况下需要使用者手动调用ThreadLocal的remove函数,手动删除不再需要的ThreadLocal,防止内存泄露。

ThreadLocal#set方法

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112

/**
* Set the value associated with key.
*
* @param key the thread local object
* @param value the value to be set
*/
private void set(ThreadLocal<?> key, Object value) {

// We don't use a fast path as with get() because it is at
// least as common to use set() to create new entries as
// it is to replace existing ones, in which case, a fast
// path would fail more often than not.

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)]) {
ThreadLocal<?> k = e.get();

if (k == key) {
e.value = value;
return;
}

if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}

tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}

/**
* Replace a stale entry encountered during a set operation
* with an entry for the specified key. The value passed in
* the value parameter is stored in the entry, whether or not
* an entry already exists for the specified key.
*
* As a side effect, this method expunges all stale entries in the
* "run" containing the stale entry. (A run is a sequence of entries
* between two null slots.)
*
* @param key the key
* @param value the value to be associated with key
* @param staleSlot index of the first stale entry encountered while
* searching for key.
*/
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;

// Back up to check for prior stale entry in current run.
// We clean out whole runs at a time to avoid continual
// incremental rehashing due to garbage collector freeing
// up refs in bunches (i.e., whenever the collector runs).
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;

// Find either the key or trailing null slot of run, whichever
// occurs first
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();

// If we find key, then we need to swap it
// with the stale entry to maintain hash table order.
// The newly stale slot, or any other stale slot
// encountered above it, can then be sent to expungeStaleEntry
// to remove or rehash all of the other entries in run.
if (k == key) {
e.value = value;

tab[i] = tab[staleSlot];
tab[staleSlot] = e;

// Start expunge at preceding stale entry if it exists
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}

// If we didn't find stale entry on backward scan, the
// first stale entry seen while scanning for key is the
// first still present in the run.
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}

// If key not found, put new entry in stale slot
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);

// If there are any other stale entries in run, expunge them
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

总结set步骤:

1)根据哈希码和数组长度求元素放置的位置,即数组下标

2)从第一步得出的下标开始往后遍历,如果key相等,覆盖value,如果key为null,用新key、value覆盖,同时清理历史key=null的陈旧数据

3)如果超过阀值,就需要再哈希:

清理一遍陈旧数据 >= 3/4阀值,就执行扩容,把table扩容2倍==》注意这里3/4阀值就执行扩容,避免迟滞把老数据重新哈希散列进新table

ThreadLocal#remove方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

/**
* 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;
}
}
}

所以JDK建议将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,
所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。

ThreadLocal#resize方法

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

/**
* Double the capacity of the table.
*/
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;

for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1); //计算hash值
while (newTab[h] != null)//如果这个位置已使用
h = nextIndex(h, newLen); // 线性往后查下,直到找到一个没有使用的位置
newTab[h] = e;//在第一个空节点塞入entry e
count++;
}
}
}

setThreshold(newLen); //设置新的阈值
size = count;
table = newTab; //把新table赋值给
}

ThreadLocal#rehash方法

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

/**
* Re-pack and/or re-size the table. First scan the entire
* table removing stale entries. If this doesn't sufficiently
* shrink the size of the table, double the table size.
*/
private void rehash() {
expungeStaleEntries();//清理一次陈旧数据,保证数据及时GC

// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}


/**
* Expunge all stale entries in the table.
*/
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}
}

解决hash冲突

hash算法种类:
1:除法散列法 ->f(k) = k % p (p<=m)
2:平方散列法 ->f(k) = (k * k) >> 28 (32位)
3:斐波那契散列法 -> f(k) = (k * 2654435769) >> 28 (32位)
4:随机数法 -> f(k) = random(k)

解决hash冲突的方法主要有:
1:开放定址法
Hi=(H(key)+di)% m i=1,2,…,n
其中H(key)为哈希函数,m 为表长,di称为增量序列,增量序列的取值方式不同,相应的再散列方式也不同。主要有以下三种:
1):线性探测再散列 dii=1,2,3,…,m-1
这种方法的特点是:冲突发生时,顺序查看表中下一单元,直到找出一个空单元或查遍全表。
2):二次探测再散列 di=12,-12,22,-22,…,k2,-k2 ( k<=m/2 )
这种方法的特点是:冲突发生时,在表的左右进行跳跃式探测,比较灵活。
3):伪随机探测再散列 di=伪随机数序列。
具体实现时,应建立一个伪随机数发生器,(如i=(i+p) % m),并给定一个随机数做起点

2:再哈希法
Hi=RH1(key) i=1,2,…,k
当哈希地址Hi=RH1(key)发生冲突时,再计算Hi=RH2(key)……,直到冲突不再产生。这种方法不易产生聚集,但增加了计算时间。

3:链地址法
这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。
链地址法适用于经常进行插入和删除的情况。

4:建立公共溢出区
将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素,一律填入溢出表

thradLocalMap中使用了斐波那契散列法,来保证哈希表的离散度。而它选用的乘数值即是2^32 * 黄金分割比。
使用“开放寻址法”中最简单的“线性探测法”解决散列冲突问题
int h = k.threadLocalHashCode & (len - 1);

ThreadLocal的子类InheritableThreadLocal

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

/**
* This class extends <tt>ThreadLocal</tt> to provide inheritance of values
* from parent thread to child thread: when a child thread is created, the
* child receives initial values for all inheritable thread-local variables
* for which the parent has values. Normally the child's values will be
* identical to the parent's; however, the child's value can be made an
* arbitrary function of the parent's by overriding the <tt>childValue</tt>
* method in this class.
*
* <p>Inheritable thread-local variables are used in preference to
* ordinary thread-local variables when the per-thread-attribute being
* maintained in the variable (e.g., User ID, Transaction ID) must be
* automatically transmitted to any child threads that are created.
*
* @author Josh Bloch and Doug Lea
* @see ThreadLocal
* @since 1.2
*/

public class InheritableThreadLocal<T> extends ThreadLocal<T> {

}

这个类扩展ThreadLocal,以提供从父线程到子线程的值的继承:当创建子线程时,子线程会接收父元素所具有值的所有可继承线程局部变量的初始值。正常情况下,子线程的变量值与父线程的相同;然而,子线程可复写childValue方法来自定义获取父类变量。
当变量(例如,用户ID、事务ID)中维护的每个线程属性必须自动传输到创建的任何子线程时,使用InheritableThreadLocal优于ThreadLocal。
InheritableThreadLocal对比ThreadLocal唯一不同是子线程会继承父线程变量,并自定义赋值函数。

ThreadLocal传递上下文case

背景:AssetContext这个上下文在多个方法中调用,但是并不是每个发放都有该参数,其中也存在某个函数对AssetContext转换处理,但是最后的一个函数需要用到该对象。

解决方案:采用Threadlocal+注解

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
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174

@Component
public class OpContextHolder {

//申明为list<AssetContext>是因为方法中多个地方调用过assetContext
private static final ThreadLocal<List<AssetContext>> ASSET_CONTEXT_THREAD_LOCAL = new ThreadLocal<>();


public AssetContext current() {
List<AssetContext> assetContexts = ASSET_CONTEXT_THREAD_LOCAL.get();
if (CollectionUtils.isEmpty(assetContexts)) {
throw new IllegalStateException("缺少上下文Context定义");
}

return ASSET_CONTEXT_THREAD_LOCAL.get().get(0);
}

/**
* 执行压栈操作
*
* @param assetContext 操作上下文
*/
public void pushStack(AssetContext assetContext) {
List<AssetContext> assetContexts = ASSET_CONTEXT_THREAD_LOCAL.get();
if (CollectionUtils.isEmpty(assetContexts)) {
ASSET_CONTEXT_THREAD_LOCAL.set(Lists.newArrayList(assetContext));
} else {
assetContexts.add(assetContext);
}
}

/**
* 执行出栈操作
*/
public void popStack() {
List<AssetContext> assetContexts = ASSET_CONTEXT_THREAD_LOCAL.get();
if (CollectionUtils.isNotEmpty(assetContexts) && assetContexts.size() > 1) {
assetContexts.remove(0);
} else {
ASSET_CONTEXT_THREAD_LOCAL.remove();
}
}
}



import javax.annotation.Resource;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

/**
* AssetContext的栈管理切面,当执行 doAction 前会进行压栈操作,执行结束后出栈
*
*/
@Component
@Aspect
public class ContextAspect implements ApplicationListener<ContextRefreshedEvent> {

private final Map<Class<?>, ContextConvert> converterMap = new HashMap<>();

@Resource
private OpContextHolder opContextHolder;

@Override
public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
//
// 在bean初始化结束之后将所有的上下文的参数存入map中.
//
ApplicationContext applicationContext = contextRefreshedEvent.getApplicationContext();
Map<String, ContextConvert> beanMap = applicationContext.getBeansOfType(ContextConvert.class);
beanMap.values().forEach(converter ->
converterMap.put(converter.convertType(), converter)
);
}

@SuppressWarnings("unchecked")
@Around("execution(public * com.action.AssetAction.doAction(..))")
public Object aspect(ProceedingJoinPoint joinPoint) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();

//
// 取 action 方法的第一个参数,将其转化为context
//
Class<?> paramType = method.getParameterTypes()[0];
ContextConvert contextConvert = converterMap.get(paramType);
Object obj = joinPoint.getArgs()[0];
AssetContext assetContext = (AssetContext) contextConvert.apply(obj);

opContextHolder.pushStack(assetContext);
try {
return joinPoint.proceed();
} finally {
opContextHolder.popStack();
}
}
}



import org.springframework.stereotype.Component;

@Component
public class TestAction implements AssetAction<Void, AssetContext> {

@Autowired
private OpContextHolder contextHolder;

@Override
public AssetContext doAction(AssetOpContext assetOpContext, Void param) {
return contextHolder.current();
}
}



@RunWith(SpringRunner.class)
@ContextConfiguration("classpath:META-INF/spring/assets-stable.xml")
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class ContextAspectTest {

private static final ILog LOGGER = LogFactory.getLog(ContextAspectTest.class);

@Autowired
private TestAction testAction;

@Autowired
private OpContextHolder contextHolder;

@Test(expected = IllegalStateException.class)
public void test01_beforeAction() {
LOGGER.info("调用前应该不存在上下文, thread=" + Thread.currentThread().getId() + ", thread-name=" + Thread.currentThread().getName());
//
// 调用之前,应该没有上下文
//
AssetContext afterInvoke = contextHolder.current();
Assert.assertNull(afterInvoke);
}

@Test
public void test02_runDoAction() {
AssetOpContext expected = new AssetOpContext();
RandomFieldInjector.inject(expected);

LOGGER.info("expected=" + expected);

AssetContext inAction = testAction.doAction(expected, null);
LOGGER.info("inAction=" + inAction);

Assert.assertNotNull(inAction);
AssetOpContext actual = convert(inAction);
LOGGER.info("actual=" + expected);

Asserts.assertFieldsEquals(expected, actual, AssetOpContext.class);
}

@Test(expected = IllegalStateException.class)
public void test03_afterAction() {
LOGGER.info("调用后应该不存在上下文, thread=" + Thread.currentThread().getId() + ", thread-name=" + Thread.currentThread().getName());
//
// 调用之后,上下文有收回
//
AssetContext afterInvoke = contextHolder.current();
Assert.assertNull(afterInvoke);
}

private AssetOpContext convert(AssetContext assetContext) {
AssetOpContext context = new AssetOpContext();
context.setActionType(assetContext.getEventType());
context.setEventTime(assetContext.getEventTime());
return context;
}
}

##ThreadLocal的使用场景

ThreadLocal 不是为了解决线程间的共享变量问题的,如果是多线程都需要访问的数据,那需要用全局变量加同步机制。

举几个例子说明一下:

1、比如线程中处理一个非常复杂的业务,可能方法有很多,那么,使用 ThreadLocal 可以代替一些参数的显式传递;

2、比如用来存储用户 Session。Session 的特性很适合 ThreadLocal ,因为 Session 之前当前会话周期内有效,会话结束便销毁。我们先笼统但不正确的分析一次 web 请求的过程:

用户在浏览器中访问 web 页面;
浏览器向服务器发起请求;
服务器上的服务处理程序(例如tomcat)接收请求,并开启一个线程处理请求,期间会使用到 Session ;
最后服务器将请求结果返回给客户端浏览器。
从这个简单的访问过程我们看到正好这个 Session 是在处理一个用户会话过程中产生并使用的,如果单纯的理解一个用户的一次会话对应服务端一个独立的处理线程,那用 ThreadLocal 在存储 Session ,简直是再合适不过了。但是例如 tomcat 这类的服务器软件都是采用了线程池技术的,并不是严格意义上的一个会话对应一个线程。并不是说这种情况就不适合 ThreadLocal 了,而是要在每次请求进来时先清理掉之前的 Session ,一般可以用拦截器、过滤器来实现。

3、在一些多线程的情况下,如果用线程同步的方式,当并发比较高的时候会影响性能,可以改为 ThreadLocal 的方式,例如高性能序列化框架 Kyro 就要用 ThreadLocal 来保证高性能和线程安全;

4、还有像线程内上线文管理器、数据库连接等可以用到 ThreadLocal;

5.项目如果使用了线程池,那么小心线程回收后ThreadLocal、InheritableThreadLocal变量要remove,否则线程池回收后,变量还在内存中,后果不堪设想!