面试题14解析-ThreadLocal原理、应用场景及内存泄漏
题目:描述一下ThreadLocal的实现原理、应用场景和内存泄漏的问题?
本文阅读大概需要18分钟。
这个题主要考查ThreadLocal的实现原理、应用场景和引起内存泄漏的问题。
是什么?
ThreadLocal主要为线程内部提供局部变量,这种变量在线程的生命周期内起作用。它并不能解决多线程访问共享变量,只为每个线程创建一个单独的变量副本。
见栗子:
private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return 0;
}
};
public static int getNextNum() {
seqNum.set(seqNum.get() + 1);
return seqNum.get();
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 3; i++) {
new Thread() {
@Override
public void run() {
for (int i = 0; i < 3; i++) {
System.out.println("thread[" + Thread.currentThread().getName() + "] --> sn["
+ getNextNum() + "]");
}
}
}.start();
}
}
执行结果:
thread[Thread-0] --> sn[1]
thread[Thread-0] --> sn[2]
thread[Thread-0] --> sn[3]
thread[Thread-1] --> sn[1]
thread[Thread-1] --> sn[2]
thread[Thread-2] --> sn[1]
thread[Thread-1] --> sn[3]
thread[Thread-2] --> sn[2]
thread[Thread-2] --> sn[3]
通过栗子,我们可以看出,ThreadLocal只为每个线程创建一个单独的变量副本。
实现原理
每个Thread维护一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal实例本身,value是真正需要存储的Object。ThreadLocalMap是使用ThreadLocal的弱引用作为Key的,弱引用的对象在GC时会被回收。它的强引用与弱引用关系细节见下图。
提供了4个对外的方法。
void set(Object value):设置当前线程的线程局部变量的值。
Object get():返回当前线程所对应的线程局部变量。
void remove():将当前线程局部变量的值删除,目的是为了减少内存的占用,调用该方法并不是必须的操作,但调用它可以加快内存回收的速度。
Object initialValue():返回该线程局部变量的初始值。
应用场景
它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。一般来说,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且对并发性处理效果更好。例如:在并发环境下,服务器为每个用户开一个线程创建一个ThreadLocal变量来存放用户信息;对于数据库的并发操作,我们可以用一个ThreadLocal变量来存放Connection;在spring中也经常出现,如Bean、事务管理、任务调度、AOP等。
四谁引起内存泄漏?
症结是由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
栗子如下:
class MyCounter {
private int count = 0;
public void increment() {
count++;
}
public int getCount() {
return count;
}
}
class MyThreadLocal extends ThreadLocal<MyCounter> {
}
public class HelloServlet extends HttpServlet {
private static MyThreadLocal myThreadLocal = new MyThreadLocal();
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
MyCounter counter = myThreadLocal.get();
if (counter == null) {
counter = new MyCounter();
myThreadLocal.set(counter);
}
}
}
这个栗子会导致内存泄露,咱们分析代码可以看出,使用static的ThreadLocal,延长了ThreadLocal的生命周期,导致myThreadLocal的生命周期跟Servlet类的生命周期一样长,意味着myThreadLocal不会被回收,弱引用形同虚设,所以当前线程无法通过ThreadLocalMap的防护措施清除counter的强引用。
提示ThreadLocal好的使用习惯,是每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
记住,这里全部都是干货!!!