ThreadLocal使用场景
ThreadLocal的作用主要是做数据隔离,填充的数据只属于当前线程,变量的数据对别的线程而言是相对隔离的,在多线程环境下,如何防止自己的变量被其它线程篡改。
-
Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。
-
Spring框架里面就是用的ThreadLocal来实现这种隔离,主要是在TransactionSynchronizationManager这个类里面,代码如下所示:
private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);
private static final ThreadLocal<Map<Object, Object>> resources =
new NamedThreadLocal<>("Transactional resources");
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
new NamedThreadLocal<>("Transaction synchronizations");
private static final ThreadLocal<String> currentTransactionName =
new NamedThreadLocal<>("Current transaction name");
- 很多场景的cookie,session等数据隔离都是通过ThreadLocal去做实现的。
ThreadLocal的使用和底层原理
ThreadLocal<String> localName = new ThreadLocal();
localName.set("张三");
String name = localName.get();
localName.remove();
其实使用真的很简单,线程进来之后初始化一个可以泛型的ThreadLocal对象,之后这个线程只要在remove之前去get,都能拿到之前set的值,注意这里我说的是remove之前。
他是能做到线程间数据隔离的,所以别的线程使用get()方法是没办法拿到其他线程的值的,但是有办法可以做到,我后面会说。
set的源码:
public void set(T value) {
Thread t = Thread.currentThread();// 获取当前线程
ThreadLocalMap map = getMap(t);// 获取ThreadLocalMap对象
if (map != null) // 校验对象是否为空
map.set(this, value); // 不为空set
else
createMap(t, value); // 为空创建一个map对象
}
ThreadLocalMap是从当前线程Thread一个叫threadLocals的变量中获取的。
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
Thread类中有关ThreadLocal的变量
public class Thread implements Runnable {
……
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
这里我们基本上可以找到ThreadLocal数据隔离的真相了,每个线程Thread都维护了自己的threadLocals变量,所以在每个线程创建ThreadLocal的时候,实际上数据是存在自己线程Thread的threadLocals变量里面的,别人没办法拿到,从而实现了隔离。
ThreadLocalMap底层结构
它的数据结构其实是很像HashMap的,但是看源码可以发现,它并未实现Map接口,而且它的Entry是继承WeakReference(弱引用)的,也没有看到HashMap中的next,所以不存在链表了。
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
结构大概长这样:
为什么需要数组呢?没有了链表怎么解决Hash冲突呢?
用数组原因
用数组是因为,我们开发过程中可以一个线程可以有多个TreadLocal来存放不同类型的对象的,但是他们都将放到你当前线程的ThreadLocalMap里,所以肯定要数组来存。
处理Hash源码:
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();
}
-
从源码里面看到ThreadLocalMap在存储的时候会给每一个ThreadLocal对象一个threadLocalHashCode,在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置i,
int i = key.threadLocalHashCode & (len-1)
。 -
然后会判断一下:如果当前位置的key是空的,就替换一个Entry对象放在位置i上;
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
- 如果位置i不为空,如果这个Entry对象的key正好是即将设置的key,那么就刷新Entry中的value;
if (k == key) {
e.value = value;
return;
}
- 如果位置i的不为空,而且key不等于entry,那就找下一个空位置,直到为空为止。如果到最后为空,则初始化一个Entry对象放在位置i上:
tab[i] = new Entry(key, value);
- 这样的话,在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该位置Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置,set和get如果冲突严重的话,效率还是很低的。
get的源码:
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);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
// get的时候一样是根据ThreadLocal获取到table的i值,然后查找数据拿到后会对比key是否相等 if (e != null && e.get() == key)。
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;
}
ThreadLocal的实例存在哪?
其实不是的,因为ThreadLocal实例实际上也是被其创建的类持有(更顶端应该是被线程持有),而ThreadLocal的值其实也是被线程实例持有,它们都是位于堆上,只是通过一些技巧将可见性修改成了线程可见。
如何共享线程的ThreadLocal数据
- 使用InheritableThreadLocal可以实现多个线程访问ThreadLocal的值,我们在主线程中创建一个InheritableThreadLocal的实例,然后在子线程中得到这个InheritableThreadLocal实例设置的值。
private void test() {
final ThreadLocal threadLocal = new InheritableThreadLocal();
threadLocal.set("帅得一匹");
Thread t = new Thread() {
@Override
public void run() {
super.run();
Log.i( "张三帅么 =" + threadLocal.get());
}
};
t.start();
}
在子线程中是能够正常输出那一行日志的,怎么传递的呀?
Thread源码中,我们看看Thread.init初始化创建的时候做了什么:
public class Thread implements Runnable {
...
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals=ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
...
}
- 截取了部分代码,如果线程的inheritThreadLocals变量不为空,比如我们上面的例子,而且父线程的inheritThreadLocals也存在,那么我就把父线程的inheritThreadLocals给当前线程的inheritThreadLocals。
如何解决内存泄露问题
- ThreadLocal在保存的时候会把自己当做Key存在ThreadLocalMap中,正常情况应该是key和value都应该被外界强引用才对,但是现在key被设计成WeakReference弱引用了。
弱引用:
-
只具有弱引用的对象拥有更短暂的生命周期,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
-
不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
-
这就导致了一个问题,ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
那怎么解决?
- 在代码的最后使用remove就好了,我们只要记得在使用的最后用remove把值清空就好了。
ThreadLocal<String> localName = new ThreadLocal();
try {
localName.set("张三");
……
} finally {
localName.remove();
}
- remove的源码很简单,找到对应的值全部置空,这样在垃圾回收器回收的时候,会自动把他们回收掉。
那为什么ThreadLocalMap的key要设计成弱引用?
- key不设置成弱引用的话就会造成和entry中value一样内存泄漏的场景。
- 首先,ThreadLocal对象有着两个引用,一个是new 出来的时候创建的引用,假设为a,一个是Entry的引用,假设为b,如果a被手动释放了,那么b是不是也没有存在的意义了?但是如果Entry保持对threadLocal的强引用,那么Thread->ThreadMap->Entry的引用链会导致它的生命周期跟线程是一致的,那么如果ThreadLocal里面保存的是大对象,那么在多线程的情况下会有内存泄漏的风险。那么回到正题,如果Entry是对ThreadLocal的弱引用,那么引用a在手动释放以后,是不是这个对象的引用只剩b了,那么在下一次垃圾回收的时候,这个threadLocal对象是不是就可以被回收了。