持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情
基本功能
同一个ThreadLocal
对象在不同线程中可以存储不同的值,线程之间的ThreadLocal值不会相互影响。基本使用如下所示:
@Test
public void test() throws InterruptedException {
ThreadLocal<Integer> intLocal = new ThreadLocal<>();
Thread thread1 = new Thread(() -> {
intLocal.set(1);
System.out.println("thread1 set val 1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread1 val: " + intLocal.get());
});
Thread thread2 = new Thread(() -> {
intLocal.set(2);
System.out.println("thread2 set val 2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("thread2 val: " + intLocal.get());
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
}
// 输出如下:
// thread2 set val 2
// thread1 set val 1
// thread1 val: 1
// thread2 val: 2
上例中thread1设置的intLocal和thread设置的intLocal为同一个对象,但是读取的值各不相同。
ThreadLocal实现方式
查看实现方式可以从ThreadLocal的源码入手 ThreadLocal类的set方法如下所示
public void set(T value) {
Thread t = Thread.currentThread(); //拿到当前线程对象
ThreadLocalMap map = getMap(t); //获取当前线程对象中存储的threadLocalMap,其中key在上例中的intLocal,而value存储的就是intLocal存的值。
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
其中getMap源码如下所示
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
这样不同线程使用相同的ThreadLocal对象可以存储不同的值的原因就找到了,因为每个线程对象中都存储着自己的一份threadLocalMap,而ThreadLocal对象和ThreadLocal对象set的值是以key-value的形式存储到threadLocalMap中的,不同线程有各自的threadLocalMap,所以即使key相同,value也可以存储不同的值。
内存泄漏
因为点啥
Thread内存泄漏的原因是面试中问的频率比较高的,那我们来分析一下出现这种情况的原因
查看源码可以看到,ThreadLocalMap中实现key,value是使用内部类Entry来实现的,而Entry的构造方法源码如下所示:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
可以看到key是存储到WeakReference中的,即弱引用。而弱引用在gc后会立即清理,这样就导致threadLocalMap中的key如果没有变量对其强引用的情况下,那么在GC时就会将其回收,而value是在Entry类对象中强引用指向的,所以value不会在gc时被回收。这样就导致value值一直存在,其实在线程中已经访问不到该value值了,导致内存泄漏。
举个栗子
模拟内存泄漏代码如下:
@AllArgsConstructor
public static class TestVal {
@Getter
String val;
}
@Test
public void gcTest() throws InterruptedException {
for (int i = 1; i <= 100; i ++) {
ThreadLocal<TestVal> intThread = new ThreadLocal<>();
TestVal testVal = new TestVal("the obj " + i);
intThread.set(testVal);
}
System.gc(); //触发gc
Thread.sleep(3600000);
}
使用工具查看TestVal类对象数量:
TestVal类对象已经访问不到了,而堆中依然存在。就会发生内存泄漏的现象。
解决方式
对于这种内存泄漏的问题,只需要在不使用ThreadLocal时调用remove方法即可,示例代码如下所示:
@Test
public void gcTest2() throws InterruptedException {
for (int i = 1; i <= 100; i ++) {
ThreadLocal<TestVal> intThread = new ThreadLocal<>();
TestVal testVal = new TestVal("the obj " + i);
intThread.set(testVal);
intThread.remove();
}
Thread.sleep(3600000);
}
上述代码在threadLocal失效前调用remove方法,执行后使用工具查看堆中TestVal类对象的数量如下
可以看到在gc后TestVal类对象都已经被回收了,这样就可以避免出现内存泄漏了。