ThreadLocal是什么?
线程本地/局部变量,可以缓存数据到线程上,使数据在线程其他任何地方进行使用。如下:
ThreadLocal tl = new ThreadLocal();
tl.set("this is value in threadlocal");
String val = tl.get(); // val = "this is value in threadlocal";
ThreadLocal的原理?
1,如果没有ThreadLocal,可以怎么做?
ThreadLocal
的原理其实特别简单,考虑一下,如果不用ThreadLocal
,要怎么用Java实现类似的功能?
- 其实就是用一个线程安全的
Map
来存储数据 - 存储多个
k-v
。这个Map
的key
是线程,value
存储的是k-v
的Map
结构(一个线程可以定义多个ThreadLocal
) - 在线程退出时移除这个线程的
key
。这样就实现了一个类似的功能。
即: 最简单来说,结构为 Map<Thread,Map<ThreadLocal,Object>>
2,Java中ThreadLocal的结构
那么,其实在ThreadLocal
的表现也是类似的:Thread
维护了一个 ThreadLocalMap
- 在
Thread
上,实现了和线程绑一起,在当前线程使用的时候可拿到线程的Map
,按上面的结构来说,就是少了一层最外层的Map
- 使用
ThreadLocalMap
,以支撑多个ThreadLocal,当ThreadLocal
初始化后设置值时,就将ThreadLocal
作为key
和以及设置的相应值value
加进该ThreadLocalMap
中。如下代码:
//......
ThreadLocal.ThreadLocalMap threadLocals = null; /*在Thread类里的成员变量*/
//......
ThreadLocal
的部分源码
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t); //获取当前线程的ThreadLocalMap
if (map != null) //获取到不为空,直接把ThreadLocal和value添加到map中
map.set(this, value);
else
createMap(t, value); //map空,进行创建并把ThreadLocal和value添加到map中
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this/*firstKey*/, firstValue);
}
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();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
可以看到,源码之中也是比较简单的。
ThreadLocal可能存在的问题
问题说明
首先,ThreadLocal
可能出现的问题是内存泄露。但是为什么呢?
我们先用我们上面一种自定义的结构即Map<Thread,Map<ThreadLocal,Object>>
来考虑——因为这个问题其实不是因为ThreadLocal
的问题。而是在某些场景可能会出现的情况。
-
既然是用线程作为
key
,那么来说,线程在退出的时候,remove
这个key
,之后,gc垃圾回收,这种方式正常的方式,应该是没有问题,既然如此,按这种方式就不应该出现内存的问题。 -
但是,有没有一些情况,在
remove
这里出现问题的呢?——线程存在的时间比较长,或者一直都是在的情况呢?——这也很明显,Tomcat容器里的请求线程、线程池等是有这种情况的. -
在线程存在的情况下,如果一直
put
数据进去,但是却没有把数据移除掉,那么这个线程对应的value
部分,即Map<ThreadLocal,Object>
部分,不就一直膨胀了吗? -
那很清晰了,在线程不退出的情况,
Map
一直膨胀,就会产生内存泄露。对应到ThreadLocal
的就是,线程里不断产生新的ThreadLocal
,即new ThreadLocal()
出来。
所以,既然是存在Thread
上的ThreadLocalMap
,那么就会跟着线程的生命周期,因此如果在存活比较久(导致同时间线程数过多)或者一直存活,并且set
的对象比较大的情况下,如果一直产生ThreadLocal来set值,但是没有remove掉,那么就有可能会造成内存泄露。
Java从源码上如何减少问题出现的概率。
- 上面说到,在线程存活比较久(导致同时间线程数过多)或者一直存活的情况下,产生ThreadLocal来set值,但是没有remove掉,那么就有可能会造成内存泄露,就比较容易产生内存泄露。
- 很不巧,
ThreadLocal
在多线程环境中使用,上面的线程情况出现的场景的概率还是比较多的。所以,Java又从源码上降低出现内存的概率(这也是和我们自己考虑的简单结构很不同的地方)。 - Java使用的方式是,使用弱引用,即。
WeakReference
,什么是弱引用?——源码上解释为Weak reference objects, which do not prevent their referents from being made finalizable, finalized, and then reclaimed. 弱引用对象,不阻止其引用对象可终结、终结和回收。
即,弱引用,垃圾回收会对弱引用进行回收。如下代码:ThreadLocalMap
的Entry
的key
使用了弱引用
//.....
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;
}
}
//.....
}
- 怎么起作用的呢?如下,
threadLocal
直接指向了new ThreadLocal()
,设值的时候放进了Map
里,但是在run
代码作用范围里,是一个强引用,因此gc的时候不会对这个entry的key进行回收
,但是下一次这个线程被重用了,threadLocal
就没有指向上次的ThreadLocal
对象,那么这时候就变成了存在Map
里的entry
,key
在某个时刻就会被gc回收。
ExecutorService threadPoolExecutor = Executors.newFixedThreadPool(10);
threadPoolExecutor.submit(new Runnable() {
@Override
public void run() {
ThreadLocal<Object> threadLocal = new ThreadLocal<>();
threadLocal.set(new Object());
....
threadLocal.get();
}
});
- 等等,还没结束!gc回收了
key
之后,entry
持有value
其实还存在map
里面。但是可以看到,在ThreadLocal
进行set
的时候,用的是map的set
,这个方法会检查这样的entry
,并进行移除。如下代码:
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();
}
ThreadLocal的使用场景
也比较好理解,就是一些在线程内只需要这个数据的场景,例如一次会话。
常见的有 web
会话传递一些信息,例如身份或数据;
数据库使用中同一个线程使用同一个连接等。