ThreadLocal是面试中经常问到的点,今天我们来讲解下ThreadLocal。
1、什么是ThreadLocal
Thread类有一个成员变量threadLocals:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
我们发现Thread类中并没有提供成员变量threadLocals的设置与访问的方法,那么每个线程的实例threadLocals参数我们如何操作呢?这时我们的主角:ThreadLocal就登场了。
ThreadLocal是线程Thread中属性threadLocals的管理者。也就是说我们对于ThreadLocal的get, set,remove的操作结果都是针对当前线程Thread实例的threadLocals存,取,删除操作。
ThreadLocal提供了线程独有的局部变量,可以在整个线程存活的过程中随时取用,极大地方便了一些逻辑的实现。常见的ThreadLocal用法有:
1)存储单个线程上下文信息。比如存储id等;
2)减少参数传递。比如做一个trace工具,能够输出工程从开始到结束的整个一次处理过程中所有的信息,从而方便debug。由于需要在工程各处随时取用,可放入ThreadLocal。
2、ThreadLocal使用实例
为了更直观的体会ThreadLocal的使用我们假设如下场景:
1)我们给每个线程生成一个ID
2)一旦设置,线程生命周期内不可变化
3)容器活动期间不可以生成重复的ID
package com.testthreadlocal;
import java.util.concurrent.atomic.AtomicInteger;
public class Test {
public static void main(String[] args)
{
// main Thread
incrementSameThreadId();
new Thread(new Runnable()
{
@Override
public void run()
{
incrementSameThreadId();
}
}).start();
new Thread(new Runnable()
{
@Override
public void run()
{
incrementSameThreadId();
}
}).start();
}
private static void incrementSameThreadId()
{
try
{
for (int i = 0; i < 5; i++)
{
System.out.println(Thread.currentThread() + "_" + i + ",threadId:" + ThreadLocalId.get());
}
}
finally
{
// 使用后请清除
ThreadLocalId.remove();
}
}
}
/**
* inner class.
*/
class ThreadLocalId
{
// Atomic integer containing the next thread ID to be assigned
private static final AtomicInteger nextId = new AtomicInteger(0);
// Thread local variable containing each thread's ID
private static final ThreadLocal<Integer> threadId = new ThreadLocal<Integer>()
{
@Override
protected Integer initialValue()
{
return nextId.getAndIncrement();
}
};
// Returns the current thread's unique ID, assigning it if necessary
public static int get()
{
return threadId.get();
}
// remove currentid
public static void remove()
{
threadId.remove();
}
}
输出结果如下,不同线程间id不同,相同线程id相同。
Thread[main,5,main]_0,threadId:0
Thread[main,5,main]_1,threadId:0
Thread[main,5,main]_2,threadId:0
Thread[main,5,main]_3,threadId:0
Thread[main,5,main]_4,threadId:0
Thread[Thread-0,5,main]_0,threadId:1
Thread[Thread-0,5,main]_1,threadId:1
Thread[Thread-0,5,main]_2,threadId:1
Thread[Thread-0,5,main]_3,threadId:1
Thread[Thread-0,5,main]_4,threadId:1
Thread[Thread-1,5,main]_0,threadId:2
Thread[Thread-1,5,main]_1,threadId:2
Thread[Thread-1,5,main]_2,threadId:2
Thread[Thread-1,5,main]_3,threadId:2
Thread[Thread-1,5,main]_4,threadId:2
3、ThreadLocal源码解析
ThreadLocal类结构及方法解析:
ThreadLocal三个方法get, set , remove以及内部类ThreadLocalMap、Entry。
我们以get方法为例:
其中getMap(t)返回的是当前线程的threadLocals,如下图,然后根据当前ThreadLocal实例对象作为key获取ThreadLocalMap中的value,如果首次进来这调用setInitialValue()。
getEntry方法:当前ThreadLocal实例对象作为key,key的hashCode与table数组的长度做位与运算,得出table数组的索引下标,取出对应的Entry。
Entry类是ThreadLocalMap的静态内部类,继承了WeakReference,从它的构造方法可以看出,ThreadLocal k是一个弱引用,指向了当前ThreadLocal实例。
set方法类似,不再赘述。
梳理完源代码后,可以得到这样一个关系图:
4、ThreadLocal导致的内存泄漏
什么叫内存泄漏,对应的什么叫内存溢出?
① Memory overflow:内存溢出,没有足够的内存提供申请者使用。
② Memory leak:内存泄漏,程序申请内存后,无法释放已申请的内存空间,内存泄漏的堆积终将导致内存溢出。
显然是TreadLocal在不规范使用的情况下导致了内存没有释放。
Entry继承了WeakReference类,我们来回顾下引用的知识:
既然WeakReference在下一次gc即将被回收,那么我们的程序为什么没有出问题呢?我们测试下弱引用的回收机制:
import java.lang.ref.WeakReference;
public class Test {
public static void main(String[] args) {
String str = new String("hello world");
WeakReference<String> weakReference = new WeakReference<String>(str);
System.gc();
if (weakReference.get() == null)
{
System.out.println("weakReference已经被GC回收");
}
else
{
System.out.println(weakReference.get());
}
while (true)
{
}
}
}
输出结果:hello world,没有被回收,这是因为这个String对象有一个强引用和一个弱引用同时指向了它。
我们改下代码:
WeakReference<String> weakReference = new WeakReference<String>(new String("hello world"));
输出结果为:weakReference已经被GC回收,只有一个弱引用被回收了。
上面演示了弱引用的回收情况,下面我们看下ThreadLocal的弱引用回收情况。
如上图所示,我们在作为key的ThreadLocal对象没有外部强引用,下一次gc必将产生key值为null的数据,若线程没有及时结束必然出现,一条强引用链
Threadref–>Thread–>ThreadLocalMap–>Entry-> value,导致value对应的Object一直无法被回收,产生内存泄露。
查看源码我们可以看到,ThreadLocal的get、set和remove方法都实现了对所有key为null的value的清除,但仍可能会发生内存泄露,因为可能使用了ThreadLocal的get或set方法后发生GC,此后不调用get、set或remove方法,为null的value就不会被清除。
5、总结
使用ThreadLocal时会发生内存泄漏的前提条件:
① ThreadLocal引用被设置为null,且后面没有set,get,remove操作
② 线程一直运行,不停止。(线程池)
③ 触发了垃圾回收。(Minor GC或Full GC)
我们看到ThreadLocal出现内存泄漏条件还是很苛刻的,所以我们只要破坏其中一个条件就可以避免内存泄漏,但为了更好的避免这种情况的发生我们使用ThreadLocal时遵守以下两个原则:
① ThreadLocal申明为private static final。Private与final 尽可能不让他人修改变更引用, Static 表示为类属性,只有在程序结束才会被回收。
② ThreadLocal使用后务必调用remove方法,最简单有效的方法是使用后将其移除。
欢迎小伙伴们留言交流~~
浏览更多文章可关注微信公众号:diggkr