ThreadLocal
ThreadLocal的作用主要是做数据隔离,填充的数据只属于当前线程,变量的数据对别的线程而言是相对隔离的。
应用场景
- spring事务隔离级别的源码,Spring采用ThreadLocal的方式来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,管理多个事务配置之间的切换,挂起和恢复。主要是在TransactionSynchronizationManager。Spring的事务主要是ThreadLocal和AOP去做实现的
- 多线程场景下日期不对,排查下来是SimpleDataFormat的锅,当时我们使用的是SimpleDataFormat的parse()方法,其内部有一个Calendar对象,调用SimpleDataFormat的parse()方法会先调用Calendar.clear(),然后调用Calendar.add(),如果一个线程先调用add()然后另一个线程又调用了clear(),这时候parse()方法解析的时间就不对了。
当时我们使用了线程池加上ThreadLocal包装SimpleDataFormat,再调用initialValue让每个线程有一个SimpleDataFormat的副本,从而解决了线程安全的问题,也提高了性能。
private static ThreadLocal<SimpleDateFormat> local =new ThreadLocal<SimpleDateFormat>(){
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
}
};
static {
SimpleDateFormat simpleDateFormat = local.get();
String format = simpleDateFormat.format(new Date());
System.out.println(format);
}
- 用户信息参考,方法之间相互调用使用用户信息很麻烦(1,通过接口传参,2,再次调用Feign)。所以使用ThreadLocal存放用户信息。
底层实现原理
好的面试官,我先说一下他的使用:
ThreadLocal<String> localName = new ThreadLocal();
localName.set("张三");
String name = localName.get();
localName.remove();
线程进来之后初始化一个可以泛型的ThreadLocal对象,之后这个线程只要在remove之前去get,都能拿到之前set值,注意这里提到remove之前。
首先看他的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对象
}
set源码很简单。主要是ThreadLocalMap关注一下,ThreadLocalMap是当前线程Thread一个叫threadLocals的变量中获取的。
ThreadLocalMap getMap(Thread t){
return t.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;
……
这里可以发现ThreadLocal数据隔离的真相了,每个线程Thread都维护了自己的threadLocals变量,所以每个线程创建ThreadLocal的时候,实际上数据是存在自己线程Thread的ThreadLocals变量里面的,其他现成无法拿到从而实现了隔离。
ThreadLocalMap底层结构是怎么样子的呢
查看ThreadLocalMap接口发现其并未实现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冲突呢?
用数组是因为开发过程中一个线程可能有多个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中的位置i。
然后判断一下:如果当前位置为空,就初始化一个Enrty对象放在位置i上。如果当前位置不为空,如果key相等那么就刷新value的值。如果位置i的不为空,而且key不等于entry,那就找下一个空位置,直到为空为止。
这样的话,在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该位置Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置,set和get如果冲突严重的话,效率还是很低的。
对象存放在哪里。
在java中,栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存,而对内存中的对象对所有线程可见,堆内存中的对象可以被所有线程访问。
那么是不是说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变量不为空,且父线程InheritableThreadLocals也存在,那么就把父线程的InheritableThreadLocals给当前线程的InheritableThreadLocals。
内存泄漏
ThreadLocal在保存的时候会把自己当做Key存在ThreadLocalMap中,正常情况应该是Key和value都应该被外界强引用才对,但是现在key被设计称了WeakReference弱引用了。
key是ThreadLocal,value是用户自定的value值。
弱引用:只具有弱引用的对象拥有更短暂的生命周期,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。
这就导致了一个问题,ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
就比如线程池里面的线程,线程都是复用的,那么之前的线程实例处理完之后,出于复用的目的线程依然存活,所以,ThreadLocal设定的value值被持有,导致内存泄露。
按照道理一个线程使用完,ThreadLocalMap是应该要被清空的,但是现在线程被复用了。
怎么解决value造成的内存泄漏
在代码的最后使用remove就好了,我们只要记得在使用的最后用remove把值清空就好了。
ThreadLocal<String> localName = new ThreadLocal();
try {
localName.set("张三");
……
} finally {
localName.remove();
}
remove的源码很简单,找到对应的值全部置空,这样在垃圾回收器回收的时候,会自动把它们回收掉。
那为什么ThreadLocalMap的key要设计成弱引用?
key不设置成弱引用的话就会造成和entry中value一样内存泄漏的场景。