我们来聊聊ThreadLocal

We are all in the gutter, but some of us are looking at the stars
我们都深处阴沟,但依然有人仰望星空

ThreadLocal 在大多数面试中经常会被问到,让你聊聊对它的认识理解以及原理,今天我们就谈谈ThreadLocal,希望大家看完之后能够在面试中游刃有余,信手捏来,好了,进入正题


ThreadLocal 是一个线程内部的存储类,可以在指定线程内存储数据,数据存储以后,只有指定线程可以得到存储数据


下面的例子中,开启两个线程,在每个线程内部设置了本地变量的值,然后调用print方法打印当前本地变量的值。在打印之后调用本地变量的remove方法会删除本地内存中的变量,代码如下所示

public class ThreadLocalMain {    
    
    static ThreadLocal<String> threadLocal = new ThreadLocal<>();    
    
    static void print(String str){        
        System.out.println(str + ":"+threadLocal.get());        
        threadLocal.remove();    
    }    

    public static void main(String[] args){        
        
        Thread thread1 = new Thread(new Runnable() {            
        
        @Override            
        public void run() {                
            //设置线程1中本地变量值                
            threadLocal.set("thread1");                
            print("thread1");                
            System.out.println("after remove+"+threadLocal.get());            
        }        
    });  
      
    Thread thread2 = new Thread(new Runnable() {            
        @Override            
        public void run() {                
            //设置线程2中本地变量值                
            threadLocal.set("thread2");                
            print("thread2");                
            System.out.println("after remove+"+threadLocal.get());            
        }        
    }); 
       
    thread1.start();        
    thread2.start();    
}复制代码

输出结果:
thread1:thread1
thread2:thread2
after remove+null
after remove+null复制代码

ThreadLocal的实现原理:

先看threadlocal的几个方法:

public void set(T value) {
      //获取当前线程
      Thread t = Thread.currentThread();
      //实际存储的数据结构类型
      ThreadLocalMap map = getMap(t);
      //如果存在map就直接set,没有则创建map并set
      if (map != null)
          map.set(this, value);
      else
          createMap(t, value);
  }
 
public T get() {   
 //获取当前线程 
 Thread t = Thread.currentThread();    ThreadLocalMap map = getMap(t);    
    if (map != null) {        
        ThreadLocalMap.Entry e = map.getEntry(this);        
        if (e != null) {            
        @SuppressWarnings("unchecked")            
        T result = (T)e.value;            
        return result;        
        }    
    }    
    return setInitialValue();
} 
//getMap方法
ThreadLocalMap getMap(Thread t) {
      //获取线程自己的变量threadLocals,并绑定到当前调用线程的成员变量threadLocals上
      return t.threadLocals;
 }
 
//createMap
void createMap(Thread t, T firstValue) {
      //实例化一个新的ThreadLocalMap,并赋值给线程的成员变量threadLocals
      t.threadLocals = new ThreadLocalMap(this, firstValue);
}复制代码

从上面代码可以看出 每个线程持有一个ThreadLocalMap对象。每一个新的线程Thread都会实例化一个ThreadLocalMap并赋值给成员变量threadLocals,使用时若已经存在threadLocals则直接使用已经存在的对象。


如果调用getMap方法返回值不为null,就直接将value值设置到threadLocals中(key为当前线程引用,值为本地变量);如果getMap方法返回null说明是第一次调用set方法(前面说到过,threadLocals默认值为null,只有调用set方法的时候才会创建map),这个时候就需要调用createMap方法创建threadLocals

//createMap
void createMap(Thread t, T firstValue) {
      //实例化一个新的ThreadLocalMap,并赋值给线程的成员变量threadLocals
      t.threadLocals = new ThreadLocalMap(this, firstValue);
}复制代码

createMap方法不仅创建了threadLocals,同时也将要添加的本地变量值添加到了threadLocals中

get方法源码

在get方法的实现中,首先获取当前调用者线程,如果当前线程的threadLocals不为null,就直接返回当前线程绑定的本地变量值,否则执行setInitialValue方法初始化threadLocals变量。在setInitialValue方法中,类似于set方法的实现,都是判断当前线程的threadLocals变量是否为null,是则添加本地变量(这个时候由于是初始化,所以添加的值为null),否则创建threadLocals变量,同样添加的值为null。

public T get() {   
 //获取当前线程 
 Thread t = Thread.currentThread();  
 //获取当前线程的threadlocals变量  
 ThreadLocalMap map = getMap(t); 
    //如果threadlocals不为空,则再map中查找到本地变量的值   
    if (map != null) {        
        ThreadLocalMap.Entry e = map.getEntry(this);        
        if (e != null) {            
        @SuppressWarnings("unchecked")            
        T result = (T)e.value;            
        return result;        
        }    
    }    
    //执行到此处,threadLocals为null,调用该更改初始化当前线程的threadLocals变量
    return setInitialValue();
}复制代码

remove方法的实现

public void remove() {
     //获取当前线程绑定的threadLocals
     ThreadLocalMap m = getMap(Thread.currentThread());
     //如果map不为null,就移除当前线程中指定ThreadLocal实例的本地变量
     if (m != null)
         m.remove(this);
}复制代码
在使用ThreadLocal的时候,需要手动去remove掉Threadlocal中的ThreadlocalMap,避免内存泄漏

为什么每次用完后remove

当一个线程调用ThreadLocal的set方法设置变量的时候,当前线程的ThreadLocalMap里面就回存放一个记录,这个记录的key为ThreadLocal的引用,value则为设置的值。 如果当前线程一直存在而没有调用Threadlocal的remove方法,并且这时候其他地方还有对ThreadLocal的引用,则当前线程的ThreadLocalMap变量里面会存在ThreadLocal变量的引用和value对象的引用是不会被释放的,这就会造成内存泄露的。但是考虑如果这个ThreadLocal变量没有了其他强依赖,而当前线程还存在的情况下,由于线程的ThreadLocalMap里面的key是弱引用, 则当前线程的ThreadLocalMap里面的ThreadLocal变量的弱引用会被在gc的时候回收,但是对应value还是会造成内存泄露,这时候ThreadLocalMap里面就会存在key为null但是value不为null的entry项,so,一定要记得remove,避免内存泄漏

为什么使用弱引用


从表面上看,发生内存泄漏,是因为Key使用了弱引用类型。但其实是因为整个Entry的key为null后,没有主动清除value导致。

下面我们分两种情况讨论:
  • key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
  • key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key的value就会导致内存泄漏,而不是因为弱引用。

总结

在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。


猜你喜欢

转载自juejin.im/post/5e7a0d4d6fb9a07cbd01fbdb