ThreadLocal源码分析详解
在实际开发过程中用到ThreadLocal的地方不是很多,ThreadLocal除了系统源码的使用,很少在项目中用到,不过也还是有的。
ThreadLocal的作用就是做数据隔离,填充的数据只属于当前线程,变量的数据对其他线程而言是相对隔离的,在多线程的环境下,就是如何防止自己的变量被其他线程篡改
隔离的作用 使用场景
Spring事务的隔离级别
Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。
Spring 的事务主要是ThreadLocal和AOP实现的,每个线程自己的链接是靠ThreadLocal保存就好
底层实现原理
使用:
ThreadLocal<String> localName = new ThreadLocal();
localName.set("李四");
String name = LocalName.get();
localName.remove();
线程进来后先初始化一个可以泛型的ThreadLocal对象,之后这个线程只要在remove之前get,就可以拿到之前set的值
他是能做到线程间的数据隔离的,所以别的线程去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,调用他的getMap方法,就是获取当前线程的threadLocals变量
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;
……
这里就可以知道他实现线程隔离的原理,每个线程都维护了自己的threadLocals变量,每个线程创建的时候,实际上数据是存储在自己线程的threadLocals变量里面,从而实现隔离
ThreadLocals的底层数据结构
通过源码可以发现,他并没有实现Map接口,而他的Entry是继承WeakReference(弱引用),不存在链表,底层是数组
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;
}
}
……
}
结构图示:
在开发过程中,一个线程是可以有多个ThreadLocal来存放多个不同类型的对象的,但是他们都将放到当前线程的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中的位置然后判断一下:如果当前位置为空,就初始化一个Entry对象放在该位置
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
如果不为空,这个Entry对象的key正好是即将设置的key,那么就刷新Entry中的value
if (k == key) {
e.value = value;
return;
}
如果不为空,key的值不等于Entry,就找下一个空位置
这样,在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该位置Entry对象的key是否和get的key一样,不一致就判断下一个位置
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对象的存储位置
在Java中,栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存,而堆内存中的对象对所有线程可见,堆内存中的对象可以被所有线程访问。ThreadLocal实例实际上也是被其创建的类持有(更顶端应该是被线程持有),而ThreadLocal的值其实也是被线程实例持有,它们都是位于堆上,只是通过一些技巧将可见性修改成了线程可见。
ThreadLocal实现共享数据
使用InheritableThreadLocal可以实现多个线程访问ThreadLocal的值,在主线程中创建一个InheritableThreadLocal的对象,然后在子线程中得到这个InheritableThreadLocal实例设置的值,也就是父子线程数据传递的问题
Thread源码中,Thread.init初始化创建的时候,如果线程的InheritableThreadLocal变量不为空,但是父线程的InheritableThreadLocal存在,那么就把父线程的InheritableThreadLocal给当前线程的InheritableThreadLocal
ThreadLocal存在的问题:内存泄漏
ThreadLocal在保护的时候会把自己的key存储在ThreadLocalMap中,正常情况下,key和value应该被外界强引用才对,但是现在key被设计成WeakReference弱引用
弱引用
他有很短暂的生命周期,在垃圾回收器线程扫描他所管辖的内存区域的时候,一旦发现有弱引用对象,不管内存空间足够与否,都会回收
如果ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。就比如线程池里面的线程,线程都是复用的,那么之前的线程实例处理完之后,出于复用的目的线程依然存活,所以,ThreadLocal设定的value值被持有,导致内存泄露。
解决:
配合try、finally块,在使用完成后用remove值清空就好了