面试官:看你简历上写着熟悉并发编程
死牛胖子:是的(毫不客气…)
面试官:有用过 Threadlocal
吗?
死牛胖子:用过(这不小KS吗,但还是要表现地谦虚一点,马上开始秀起来…)
死牛胖子:Threadlocal
是一个用于保障线程安全的类,我们可以将一些数据存放到 Threadlocal
中,之后再取出来使用,Threadlocal
可以保证这些数据在不同线程间互不干扰。比如,在我们之前的项目里,会将当前用户的登录信息存放在 ThreadLocal
中,每一个请求到达后端,首先需要做登录认证,认证成功,就会将认证的用户信息存放到 ThreadLocal
中,然后请求进入到 Controller
的方法中,执行业务处理,在整个的处理过程中,我们可以随时从 ThreadLocal
中取出当前登录的用户信息来使用。ThreadLocal
可以保证每个请求对应的用户信息是当前用户,不会因为多个请求并发造成数据被修改。
面试官:能把刚刚的场景使用伪代码实现一下吗?这里有纸笔。
死牛胖子:(卧槽,还要写代码,虽然心里有点抗拒,但手已经不自觉地开始活动起来…)
public class SecurityContext {
private static ThreadLocal<AuthInfo> context = new InheritableThreadLocal<>() {
@Override
protected AuthInfo initialValue() {
return new AuthInfo();
}
};
public static ThreadLocal<AuthInfo> getContext() {
return context;
}
public static ThreadLocal<AuthInfo> getAuthInfo() {
return context.get();
}
public static class AuthInfo {
// ...
}
}
public class AuthFilter implements Filter {
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) {
AuthInfo authInfo = checkAuthInfo(servletRequest);
SecurityContext.getContext().set(authInfo);
// ...
filterChain.doFilter(servletRequest, servletResponse);
}
}
死牛胖子:首先需要一个 ThreadLocal
的静态变量,然后声明一个 Filter
,拦截所有的请求,在 Filter
中对请求中的 Token
或者 Session
进行解释,获取当前用户信息,然后把用户信息设置到 ThreadLocal
中,这样在后续的操作中就可以取出来使用了。
面试官:不错,看来用的还是挺丝滑,那么我们再深入一点,说一下 ThreadLocal
是如何实现线程隔离的
死牛胖子:嗯…(思考一下,这是要说实现原理啊,还好看过源码,还可以秀…)
死牛胖子:这个问题,我们可以从源码的实现来进行分析(哥可是看过源码的淫)
首先看一下 Thread
类的实现,Thread
类中有一个 ThreadLocalMap
的成员变量,这个 Map
的 Key
就是 ThreadLocal
。
public class Thread implements Runnable {
ThreadLocalMap threadLocals = null;
}
然后看一下 ThreadLocal
类 set()
方法的实现,首先获取当前线程的 Thread
实例,然后获取到 Thread
类中的 ThreadLocalMap
变量,然后使用 ThreadLocal
自身作为 Key
将信息存入 ThreadLocalMap
。所以,我们通过 ThreadLocal
存储的信息实际上是存储在当前的线程的 Thread
实例中,而每个线程拥有自己唯一的 Thread
实例,在同一个线程内使用 Thread
实例的变量,就像使用本地变量一样,自然就实现了线程隔离。
public class ThreadLocal<T> {
public void set(T var1) {
Thread var2 = Thread.currentThread();
ThreadLocal.ThreadLocalMap var3 = this.getMap(var2);
if (var3 != null) {
var3.set(this, var1);
} else {
this.createMap(var2, var1);
}
}
public T get() {
Thread var1 = Thread.currentThread();
ThreadLocal.ThreadLocalMap var2 = this.getMap(var1);
if (var2 != null) {
ThreadLocal.ThreadLocalMap.Entry var3 = var2.getEntry(this);
if (var3 != null) {
Object var4 = var3.value;
return var4;
}
}
return this.setInitialValue();
}
public void remove() {
ThreadLocal.ThreadLocalMap var1 = this.getMap(Thread.currentThread());
if (var1 != null) {
var1.remove(this);
}
}
ThreadLocal.ThreadLocalMap getMap(Thread var1) {
return var1.threadLocals;
}
}
面试官:(又被你装到了)那这个 ThreadLocalMap
跟我们常用的 HashMap
有什么区别?
死牛胖子:这两个 Map
都是键值对的结构,但是底层实现是不一样的。HashMap
底层使用的数组 + 链表的结构实现,如果长度超过 8
,则会转换为红黑树。ThreadLocalMap
则是单纯使用数组实现,另外 ThreadLocalMap
的键前面说是 ThreadLocal
实例是不严谨的,正确地说应该是 ThreadLocal
的弱引用。
面试官:你刚说 ThreadLocalMap
是使用数组实现,如果出现 hash
冲突,如何解决?
死牛胖子:HashMap
本来也是数组,之所以加上链表是为了解决 hash
冲突的问题。ThreadLocalMap
既然没有链表来进行辅助,那么只能存储在数组上,它采用了一种开放定址法。比如,在往 ThreadLocalMap
插入数据时发生冲突,也就是说数组中当前位置的值与当前的 ThreadLocal
不匹配,那么继续寻找下一个位置,如果下一个位置为空或者正好就是当前的 ThreadLocal
,那么就将值设置进去,否则继续寻找下一个。所以看上去跟链表差不多,只不过是在同一个数组中通过一个查找位置的规则来实现。
面试官:不错,了解的还挺细,前面你提到 ThreadLocalMap
的键使用的是弱引用,可以细讲一下吗?
死牛胖子:我先说一下什么是弱引用吧。
面试官:好的
死牛胖子:JDK
为了更好地帮助 JAVA
虚拟机进行垃圾回收,在引用的基础上,增加了软引用,弱引用以及虚引用,原来的引用就改称为强引用。
- 强引用:强引用的意思就是只要引用在,就不能被回收,即便此时内存空间不足,抛
OOM
异常,程序中止也不能回收,这就是强引用,确实比较强。而软引用,弱引用,虚引用则是一级级递减。 - 软引用:如果一个对象只持有软引用,在内存空间充足时,垃圾回收器不会回收它,但是,如果内存空间不足了,这些对象就会被回收,这个常用于缓存。
- 弱引用:如果一个对象只持有弱引用,只要进行
GC
,不管当前内存空间足够与否,都会对其进行回收,所以弱引用是比较脆弱的。 - 虚引用:如果一个对象仅持有虚引用,在任何时候都可能被回收,跟没有引用效果差不多,形同虚设。
死牛胖子:(呼…这次背的还可以)
面试官:为什么 ThreadlocalMap
的 Key
要设置成弱引用呢?
死牛胖子:为了更好地释放内存,用一个简单的示例来说明一下
public class ThreadLocalDemo {
public void execute() {
test();
// ......
}
public void test() {
ThreadLocal<Integer> intLocal = new ThreadLocal<>();
intLocal.set(1);
}
}
intLocal
在声明时,拥有了一个强引用,当执行 intLocal.set(1)
时,会存储到 Thread
类中的 ThreadlocalMap
变量中,并以弱引用形式存储,intLocal
又拥有了一个弱引用,此后 test()
执行完毕,intLocal
的强引用失效,只剩下一个弱引用。此时,因为强引用不存在了,Thread
无法自行删除该键值对,而弱引用则让虚拟机可以自行回收。
放到实际应用中,就更加明显的,在实际应用中一般会使用线程池技术,线程在请求完成之后并不会消亡,而是被回收至线程池中,后续请求到时,还可以拿出来继续使用,如果使用强引用的话,那么 ThreadLocal
实例就会一直存在于 Thread
对象中,只要线程不消亡,ThreadLocal
实例就不会被回收,可能导致内存泄漏。
面试官:既然 ThreadlocalMap
的 Key
已经设置成弱引用了,是不是就杜绝了内存泄漏的风险?
死牛胖子:不然(开始装B…)
死牛胖子:通过前面的分析,ThreadlocalMap
的 Key
在 GC
时会被自动回收,但是 value
不会被回收啊,内存泄漏的风险依然存在。我们可以模拟一下 ThreadlocalMap
的实现
public class Test {
public static void main(String[] args) {
Object key = new Object();
Object value = new Object();
//弱引用
Entry entryReference = new Entry(key, value);
WeakReference<Object> valueReference = new WeakReference<>(value);
key = null;
value = null;
entryReference.value = null; // 重要一行
System.gc();
System.out.println(entryReference.get()); // null
System.out.println(valueReference.get()); // null
}
static class Entry extends WeakReference<Object> {
Object value;
Entry(Object var1, Object var2) {
super(var1);
this.value = var2;
}
}
}
执行结果是打印两个 null
,说明 key
跟 value
都被回收的,但如果 entryReference.value = null
注释掉,再执行,则结果就不一样了,valueReference
的值依然还在。
null
java.lang.Object@133314b
死牛胖子:(神马情况,怎么不自觉地又开始写上代码了…)
面试官:那怎么样才能彻底解决这个内存泄漏的问题呢?
死牛胖子:其实也简单,Threadlocal
类有提供一个 remove
方法,可以手动清理,只要在请求结束后手动清理一下,就可以防止内存泄漏了。
面试官:回答的不错,回家等消息吧,一年内我们的HR会联系你的。
死牛胖子:…