目录
一、什么是ThreadLocal
二、ThreadLocal的使用
三、ThreadLocal源码分析
四、ThreadLocal导致内存泄漏
ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。当线程结束后,每个线程所拥有的那个本地值会被释放。在多线程并发操作“线程本地变量"的时候,线程操作自己的变量副本,从而规避了线程安全问题,是一种空间换时间的思想。
public class ThreadLocalDemo {
// private ThreadLocal threadLocal= ThreadLocal.withInitial(()->{
// return "test threadlocal"; //在定义ThreadLocal的时候设置一个获取初始值的回调函数。
// });
private static ThreadLocal threadLocal=new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
ThreadLocalDemo threadLocalDemo = new ThreadLocalDemo();
Thread thread = new Thread(() -> {
threadLocal.set("test");
System.out.println(threadLocal.get());
}, "t1");
thread.start();
thread.join();
String s = threadLocal.get();
System.out.println("主线程获取不到在thread线程设置的值:"+s);
}
}
使用场景
set()方法解读
public void set(T value) {
//获取当前线程
Thread t = Thread.currentThread();
//获取当前线程的ThreadLocalMap
ThreadLocal.ThreadLocalMap map = getMap(t);
if (map != null) {
//value被绑定到threadLocal实例
map.set(this, value);
} else {
//没有ThreadLocalMap则创建一个ThreadLocalMap实例,关联到thread实例
createMap(t, value);
}
}
小结:set()步骤
get()方法解读
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获取ThreadLocalMap
ThreadLocal.ThreadLocalMap map = getMap(t);
if (map != null) {
//如果不为空,以threadlocal实例为key获取值
ThreadLocal.ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
//值不为空 返回值
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果为空 初始化一个值
return setInitialValue();
}
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocal.ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);//绑定值
} else {
createMap(t, value);//创建threallocalmap
}
if (this instanceof TerminatingThreadLocal) {
//当线程终止并且已在终止线程中初始化时(即使已使用空值初始化)会收到通知。
TerminatingThreadLocal.register((TerminatingThreadLocal>) this);
}
return value;
}
小结:get()大致步骤
ThreadLocal实现线程隔离的原理其实就是用了Map的数据结构给当前线程缓存了变量的值, 要使用的时候就从本线程的threadLocals对象中获取就可以了, key就是当前线程。当然在当前线程下获取当前线程里面的Map里面的对象并操作肯定没有线程并发问题了, 当然能做到变量的线程间隔离了。
remove()方法解读
remove()方法用于在当前线程的ThreadLocalMap中移除线程本地变量所对应的值
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this);
}
}
private void remove(ThreadLocal> key) {
ThreadLocal.ThreadLocalMap.Entry[] tab = table;
int len = tab.length; //entry的长度
int i = key.threadLocalHashCode & (len-1);//key在数组上的槽点
for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.refersTo(key)) {
//如果e等于key值就删除该值,这是一个native方法,方便垃圾回收
e.clear();
expungeStaleEntry(i);
return;
}
}
}
ThreadLocalMap解读
ThreadLocalMap内部静态类Entry实现
内部使用table数组存储Entry,默认大小INITIAL_CAPACITY(16)
参数说明:
size:table中元素的数量。
threshold:table大小的2/3,当size >= threshold时,遍历table并删除key为null的元素,
如果删除后size >= threshold*3/4时,需要对table进行扩容。
static class Entry extends WeakReference> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal> k, Object v) {
super(k);
value = v;
}
}
可以看到
Entry对ThreadLocal使用了弱引用(WeakReference,弱引用指向的对象只能生存到下一次垃圾回收之前,也就是说当发生GC回收时,不管内存够不够弱引用的对象都会被回收)。
为什么要使用弱引用呢?如下代码
public void a (){
ThreadLocal threadLocal = new ThreadLocal<>();
threadLocal.set("test");//设置值
threadLocal.get();//获取值
//结束
}
线程执行方法a()时,会创建一个ThreadLocal实例,这个是强引用,在调用set("test")之后,ThreadLocalMap会新建一个Entry实例,这个key是以弱引用的方式指向ThreadLocal的,执行完方法之后,栈帧被销毁,强引用的值也就没有了,但是ThreadLocal的实例还有Entry对应的引用,如果是强引用那么ThreadLocal和value值都不能会GC回收。从而会导致内存泄露问题。
set()方法解读
private void set(ThreadLocal> key, Object value) {
ThreadLocal.ThreadLocalMap.Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
if (e.refersTo(key)) {
//如果key等于条目的值,那就直接替换掉旧值
e.value = value;
return;
}
if (e.refersTo(null)) {
//如果不相等,调用replaceStaleEntry方法创建新值
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new ThreadLocal.ThreadLocalMap.Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
//如果清理完无用条目(ThreadLocal被回收的条目)、并且数组中的数据大小 >= 阈值的时候对当前的Table进行重新哈希
rehash();
}
getEntry()方法解读
private Entry getEntry(ThreadLocal> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.refersTo(key))
//如果相等直接返回值
return e;
else
//如果没有匹配的值,就去后面的条目中查找
return getEntryAfterMiss(key, i, e);
}
内存泄漏就是不再使用的内存不能得到回收,引发的最终结果是内存溢出。
在什么情况下ThreadLocal会引发内存泄漏呢?
1、如果一个线程长时间运行而不被销毁,比如线程池。因为对于线程池里面不会销毁的线程, 里面总会存在着threadlocal的强引用, 因为final static 修饰的 ThreadLocal 并不会释放, 而ThreadLocalMap 对于 Key 虽然是弱引用, 但是强引用不会释放, 弱引用当然也会一直有值, 同时创建的value对象也不会释放, 就造成了内存泄露。
2、ThreadLocal应用被设置为null之后,且后续在同一Thread实例执行期间,没有发生对其他threadLocal实例的get(),set(),remove()操作(ThreadLocalMap 在执行这些方法时会清空key为null的Entry)。
如何避免ThreadLocal导致的内存泄漏?
1、尽量使用private static final修饰ThreadLocal实例,使用private和final主要是尽可能不让其他的类修改ThreadLocal的引用,static保证ThreadLocal实例的全局唯一。
2、ThreadLocal 使用完之后务必调用remove()方法。
参考
《JAVA高并发核心编程(卷2):多线程、锁、JMM、JUC、高并发设计》-尼恩编著
Java 并发 - ThreadLocal详解 | Java 全栈知识体系