ThreadLocal实现原理以及内存泄漏问题

下班抽空写个博客记录下:

一、概述

ThreadLocal官网解释:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable.  {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g.,a user ID or Transaction ID)

 翻译过来的大概意思就是:ThreadLocal类用来提供线程内部的局部变量。这些变量在多线程环境下访问(通过get或set方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量,ThreadLocal实例通常来说都是private static类型

总结:ThreadLocal不是为了解决多线程访问共享变量,而是提供了保持对象的方法(使用TheadLoca对象的get、set,remove,initialValue来操作当前线程内部的局部变量)和避免参数传递的复杂性

二、实现原理

ThreadLocal类提供了四个对外开放的接口方法,这也是用户操作ThreadLocal类的基本方法:
(1) void set(Object value)设置当前线程的线程局部变量的值。
(2) public Object get()该方法返回当前线程所对应的线程局部变量。
(3) public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
(4) protected Object initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次,ThreadLocal中的缺省实现直接返回一个null。 

/**
    返回当前线程副本中的值
    线程局部变量。如果该变量没有当前线程的值,则首先将其初始化为{@link #initialValue}方法调用                    
    返回的值。
    @return 返回当前线程的这个线程局部值
 */
public T get() {
    Thread t = Thread.currentThread();//当前线程
    ThreadLocalMap map = getMap(t);//获取当前线程对应的ThreadLocalMap
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);//获取对应ThreadLocal的变量值
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();//若当前线程还未创建ThreadLocalMap,则返回调用此方法并在其中调用createMap方法进行创建并返回初始值。
}
//设置变量的值
public void set(T value) {
   Thread t = Thread.currentThread();
   ThreadLocalMap map = getMap(t);
   if (map != null)
       map.set(this, value);
   else
       createMap(t, value);
}
private T setInitialValue() {
   T value = initialValue();
   Thread t = Thread.currentThread();
   ThreadLocalMap map = getMap(t);
   if (map != null)
       map.set(this, value);
   else
       createMap(t, value);
   return value;
}
/**
为当前线程创建一个ThreadLocalMap的threadlocals,并将第一个值存入到当前map中
@param t the current thread
@param firstValue value for the initial entry of the map
*/
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
//删除当前线程中ThreadLocalMap对应的ThreadLocal
public void remove() {
       ThreadLocalMap m = getMap(Thread.currentThread());
       if (m != null)
           m.remove(this);
}

其实在ThreadLocal类中有一个静态内部类ThreadLocalMap(其类似于Map),用键值对的形式存储每一个线程的变量副本,ThreadLocalMap中元素的key为当前ThreadLocal对象,而value对应线程的变量副本,每个线程可能存在多个ThreadLocal。

static class ThreadLocalMap {
  //map中的每个节点Entry,其键key是ThreadLocal并且还是弱引用,这也导致了后续会产生内存泄漏问题的原因。
 static class Entry extends WeakReference<ThreadLocal<?>> {
           Object value;
           Entry(ThreadLocal<?> k, Object v) {
               super(k);
               value = v;
   }
    /**
     * 初始化容量为16,以为对其扩充也必须是2的指数 
     */
    private static final int INITIAL_CAPACITY = 16;
    /**
     * 真正用于存储线程的每个ThreadLocal的数组,将ThreadLocal和其对应的值包装为一个Entry。
     */
    private Entry[] table;
    ///....其他的方法和操作都和map的类似
}

看上面ThreadLocalMap的源代码,我们发现Entry 继承了WeakReference<ThreadLocal<?>>(弱引用)

先简单解释其中需要用到的弱引用

如果一个对象只具有弱引用,那么垃圾回收器在扫描到该对象时,无论内存充足与否,都会回收该对象的内存

三、内存泄漏问题

先看个简单的例子

public class UserContext {		
    private static final ThreadLocal<UserInfo> userInfoLocal = new ThreadLocal<UserInfo>(); 		
    public static UserInfo getUserInfo() {			
		return userInfoLocal.get();		
	} 		
	public static void setUserInfo(UserInfo userInfo) {			
		userInfoLocal.set(userInfo);		
	} 		
	public static void clear() {			
		userInfoLocal.remove();		
	}	
}

Entry中的key是弱引用并指向,ThreadLocal<UserInfo> 对象,value是userInfo对象。

当我显示的把userInfoLocal = null 时就只剩下了key这一个弱引用,GC时也就会回收掉ThreadLocal<UserInfo> 对象。

但是我们最好避免threadLocal=null的操作,尽量用threadLocal.remove()来清除。因为前者中的userInfo对象还是存在强引用在当前线程中,只有当前thread结束以后, current thread就不会存在栈中,强引用断开, 会被GC回收。但是如果用的是线程池,那么的话线程就不会结束,只会放在线程池中等待下一个任务,但是这个线程的 map 还是没有被回收,它里面存在value的强引用,所以会导致内存溢出。

最后用一个内存图来理解:

参考博文: https://blog.csdn.net/LHQJ1992/article/details/52451136

                     https://blog.csdn.net/vicoqi/article/details/79743112

猜你喜欢

转载自blog.csdn.net/lwang_IT/article/details/82989751
今日推荐