java中的ThreadLocal

1. ThreadLocal的作用:

ThreadLocal的作用主要是做数据隔离,填充的数据只属于当前线程,变量的数据对别的线程而言是相对隔离的,在多线程环境下,如何防止自己的变量被其它线程篡改。

2. ThreadLocal使用到的场景:

  1. Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。

3. ThreadLocal的原理:

其实使用真的很简单,线程进来之后初始化一个可以泛型的ThreadLocal对象,之后这个线程只要在remove之前去get,都能拿到之前set的值,注意这里我说的是remove之前。

他是能做到线程间数据隔离的,所以别的线程使用get()方法是没办法拿到其他线程的值的

  • 以下是set时的源码
1
2
3
4
ThreadLocal<String> localName = new ThreadLocal();
localName.set("张三");
String name = localName.get();
localName.remove();
1
2
3
4
5
6
7
8
public void set(T value) {
Thread t = Thread.currentThread(); //获取当前线程
ThreadLocalMap map = getMap(t); //在当前线程中获取ThreadLocalMap对象
if (map != null) //当前线程中存在ThreadLocalMap对象,直接设置值
map.set(this, value);
else
createMap(t, value); // 不存在则创建一个为空的map对象
}

这里我们基本上可以找到ThreadLocal数据隔离的真相了,每个线程Thread都维护了自己的threadLocals变量,所以在每个线程创建ThreadLocal的时候,实际上数据是存在自己线程Thread的threadLocals变量里面的,别人没办法拿到,从而实现了隔离。

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
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table; //其实它是把值放到了数组中
int len = tab.length;
int i = key.threadLocalHashCode & (len-1); //根据ThreadLocal对象的hash值,计算到table中的位置
//拿到ThreadLocal对象中的Entry,如果ThreadLocal对象的Entry不存在,就会在i位置直接 new Entry()
//如果位置i的不为空,而且key不等于entry,那就找下一个空位置,直到为空为止。
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();

if (k == key) { // 如果值相等就刷新Entry中的value;
e.value = value;
return;
}

if (k == null) { //如果当前位置是空的,就初始化一个Entry对象放在位置i上;
replaceStaleEntry(key, value, i);
return;
}
}

tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
  • 以下是get时的源码
1
2
3
4
5
6
7
8
9
10
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);
}

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

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;

while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}

4. ThreadLocal 数据共享

1
2
3
4
5
6
7
8
9
10
private static void test() throws InterruptedException {
final ThreadLocal threadLocal = new InheritableThreadLocal();
threadLocal.set("帅得一匹");
Thread t = new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 输出 " + threadLocal.get());
}, "t1");
t.start();
t.join();
System.out.println(Thread.currentThread().getName() + " 输出 " + threadLocal.get());
}

在子线程中我是能够正常输出那一行,这也就是父子线程数据传递的

**5. ThreadLocal内存溢出问题 **

1
2
3
4
5
6
7
8
9
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;

Entry(ThreadLocal<?> k, Object v) {
super(k); //key 为弱引用
value = v;
}
}

ThreadLocal在保存的时候会把自己当做Key存在ThreadLocalMap中,正常情况应该是key和value都应该被外界强引用才对,但是现在key被设计成WeakReference弱引用了。

这就导致了一个问题,ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。

就比如线程池里面的线程,线程都是复用的,那么之前的线程实例处理完之后,出于复用的目的线程依然存活,所以,ThreadLocal设定的value值被持有,导致内存泄露。

按照道理一个线程使用完,ThreadLocalMap是应该要被清空的,但是现在线程被复用了。

得在使用的最后用remove把值清空就好了

ThreadLocal为什么要把key设计为弱引用? 请看垃圾回收机制