ThreadLocal

参考

ThreadLocal 用于提供线程局部变量,在多线程环境可以保证各个线程里的变量独立于其它线程里的变量,保证了多线程环境下数据的独立性

使用

注释写的:

image-20210110224621414

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 简单版
public class ThreadLocalTest {

private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();

public static void main(String[] args) {
threadLocal.set("sss");

new Thread(() -> {
threadLocal.set("hhh");
System.out.println("new Thread:" + threadLocal.get());
}).start();

System.out.println("main:" + threadLocal.get());
}
}

// 输出:
// main:sss
// new Thread:hhh

原理

每个线程维护了一个 ThreadLocalMap 类型的 threadLocals,存着自己定义的 ThreadLocal 的副本( 看 set 那里 )

1
ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMap

用一个 Entry 数组来存储 key 和 value

1
2
3
4
5
6
7
8
9
10
private Entry[] table;

private static final int INITIAL_CAPACITY = 16;

// 阈值,超过了就扩容
private int threshold;

private void setThreshold(int len) {
threshold = len * 2 / 3;
}

set

  • 如果当前位置上的 key 相同,直接将该位置上面的 Entry 的 value 替换成最新的
  • 如果当前位置上面的 Entry 的 key 为空, 说明 ThreadLocal 对象已经被回收了, 那么就调用 replaceStaleEntry
  • 如果清理完无用条目( ThreadLocal 被回收的条目 ),并且数组中的数据大小 > 阈值的时候,对当前的 Table 进行重新哈希
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
private void set(ThreadLocal<?> key, Object value) {

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();
}


// 首先扫描,删除陈旧的条目
// 如果这样不能充分缩小大小,则将大小加倍
private void rehash() {
expungeStaleEntries();

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

// 大小加倍
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);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}

setThreshold(newLen);
size = count;
table = newTab;
}

set

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void set(T value) {
// 当前线程
Thread t = Thread.currentThread();
// 拿到当前线程的 threadLocals
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
// 如果为 null
// 就 new 一个存有变量的 ThreadLocal 的 ThreadLocalMap
// 赋给此线程的 threadLocals
createMap(t, value);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

get

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public T get() {
Thread t = Thread.currentThread();
// 拿到当前线程的 threadLocals
ThreadLocalMap map = getMap(t);
if (map != null) {
// 拿到你定义的 ThreadLocal 的变量返回
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}

setInitialValue

如果 get 的时候 threadLocals 为空,或者 threadLocals 不为空,但存的变量为空,就会返回初始值

如果不重写 initialValue,那默认就返回 null

1
2
3
4
5
6
7
8
9
10
11
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
// 和 set 差不多
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}

应用场景

应用最多的是 session 管理和数据库链接管理

我们希望通过某个类将状态(例如用户ID、事务ID)与线程关联起来,那么通常在这个类中定义private static类型的ThreadLocal 实例

由于在每个线程中都创建了副本,所以要考虑它对资源的消耗,比如内存的占用会比不使用 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
public class ThreadLocalDemo {
static class LocalVariable {
private Long[] a = new Long[1024 * 1024];
}

// (1)
final static ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, 5, 1, TimeUnit.MINUTES,
new LinkedBlockingQueue<>());
// (2)
final static ThreadLocal<LocalVariable> localVariable = new ThreadLocal<LocalVariable>();

public static void main(String[] args) throws InterruptedException {
// (3)
Thread.sleep(5000 * 4);
for (int i = 0; i < 50; ++i) {
poolExecutor.execute(new Runnable() {
public void run() {
// (4)
localVariable.set(new LocalVariable());
// (5)
System.out.println("use local varaible" + localVariable.get());
localVariable.remove();
}
});
}
// (6)
System.out.println("pool execute over");
}
}

对于线程池里面不会销毁的线程,里面总会存在着 <ThreadLocal, LocalVariable> 的强引用,因为 final static 修饰的 ThreadLocal 并不会释放,而ThreadLocalMap 对于 Key 虽然是弱引用,但是强引用不会释放,弱引用当然也会一直有值,同时创建的 LocalVariable 对象也不会释放,就造成了内存泄露

ThreadLocal提供了一个清除线程中对象的方法,即 remove,内部实现就是调用 ThreadLocalMap 的 remove

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
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;
}
}
}

找到 key 对应的 Entry,并且清除 Entry 的 key( ThreadLocal )置空,随后清除过期的 Entry 即可避免内存泄露