ThreadLocal原理分析及内存泄漏问题

ThreadLocal 的定义及使用:

ThreadLocal类 位于 java.lang 包下

ThreadLocal, 翻译成中文,应该叫 线程局部变量。作用是存储线程本地变量,意思是说,在ThreadLocal中保存的变量,都归当前线程所有,不同线程之间相互隔离,互不影响,避免和主内存通信,从而提高效率。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

先看一下使用效果:

public class TestThreadLocal {
    private static ThreadLocal<String> tl = new ThreadLocal<>();
    
    public static void main(String[] args) {
        new Thread(() -> {
            tl.set("thread-1 set String");
            try {
                TimeUnit.SECONDS.sleep(3);
                System.out.println(tl.get());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();

        new Thread(() -> {
            System.out.println(tl.get();
        }).start();
    }
}

以上代码中,定义了两个线程,线程1 启动后,往ThreadLocal中设置了一个字符串,然后休眠3秒钟,在这之间,线程2 往ThreadLocal中去获取值并输出,3秒钟后,线程1再次获取ThreadLocal中的值并输出。
执行结果:

null
thread-1 set String

可以看出,在线程1往ThreadLocal中保存了一个String字符串后,线程2获取结果为null,说明线程2并不能得到线程1所保存的数据,而线程1本身则可以拿到。由此可以得出答案:

各个线程在ThreadLocal中保存的数据,只对当前线程可见,多个线程对ThreadLocal的操作,其实就是操作各自线程中的数据。

ThreadLocal 的原理:

既然ThreadLocal可以做到线程隔离,那么他是怎么做到的呢?说到原理,就一定离不开源码。首先我们就要看看JDK源码对ThreadLocal这个类的定义。在对ThreadLocal的操作中,我们着重看set()和get()这两个函数。暂时忽略类中的其他属性和方法。

先看以下ThreadLocal中的set()函数,该函数是往ThreadLocal中保存数据。

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) map.set(this, value);
        else createMap(t, value);
}

分析代码。大致可以得出基本步骤:

获取当前线程
通过当前线程获取ThreadLocalMap,通过ThreadLocalMap保存数据。
如果没有获取到ThreadLocalMap,就新建一个ThreadLocalMap,然后通过ThreadLocalMap保存数据。

我们发现,在这个函数中,涉及到了一个叫ThreadLocalMap的类,这是个什么类,我们看一下,点进去发现该类是ThreadLocal的内部类。

static class ThreadLocalMap {
	
	private static final int INITIAL_CAPACITY = 16;
	private Entry[] table;
	private int size = 0;
	private int threshold;
	
	static class Entry extends WeakReference<ThreadLocal<?>> {
            
		Object value;

		Entry(ThreadLocal<?> k, Object v) {
			super(k);
			value = v;
		}
    }
}

先不看类中的其他方法,只看定义的变量和内部类。得出以下结果:

ThreadLocalMap 是 ThreadLocal 的静态内部类。
ThreadLocalMap 中定义了一个Entry[] table  ,而Entry 是他的内部类。
Entry类中定义了一个 Object value 变量,这个value就是我们存入的数据。后面再分析。

类的定义先看到这里,我们大致可以猜到,我们往ThreadLocal中保存的变量,最终是将一个Entry对象,存到了ThreadLocalMap的table数组中, 而Entry对象的value属性,就是我们要存入的数据。我们带着疑问,继续往下看。
这里返回到上面的set()方法,我们看到set()方法中,是通过getMap(t) 来获取ThreadLocalMap的,参数 t 代表当前线程。接下来我们看一下getMap(t)这个函数。

 ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
}

可以看到,在获取ThreadMap的时候,他返回的是t.threadLocals 这个变量, 而 t 是调用该方法时所传入的参数,即:当前线程。它是Thread类型,说明在Thread类中有一个变量 threadLocals。 看到这里,我们不用多想,肯定要到Thread类中一看究竟。
我们到Thread类中可以看到,在Thread类中,定义了这样一个变量:

public class Thread implements Runnable {
	
	// 省略其他定义
	...
	...
	ThreadLocal.ThreadLocalMap threadLocals = null;
}

看到这里,ThreadLocal为什么线程隔离的原因,大概就是弄明白了。分析原因如下:

 我们在使用ThreadLocal保存数据的时候,其实是将数据保存在了Entry[]中。
 该 Entry[] 是在ThreadLocalMap中定义的一个变量 tables
 既然tables是一个数组,说明我们可以保存多个数据。
 Entry 是 ThreadLocalMap的内部类,我们要保存的每一个数据就是会转化成Entry的方式保存。
 归根结底,我们的数据是保存在ThreadLocalMap这个类的一个变量 tables 中。
 而ThreadLocalMap同时又是Thread类中的一个变量 threadLocals
 因为每一个Thread实例代表着一个线程,所以我们保存在Thread实例中的数据,当然是各个线程各自维护的。

我们已经知道,要保存的变量是以Entry的方式保存,那么到底是怎样转化为Entry的呢?接下来我们来看看。
我们回到set()函数,再看一下。

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) map.set(this, value);
        else  createMap(t, value);
}

可以看到,在保存数据的时候,调用了map.set(this, value), 其中this代表当前ThreadLocal实例, value代表要保存的变量。我们看一下这个函数。该函数是ThreadLocalMap中的一个函数。

private void set(ThreadLocal<?> key, Object value) {

	// 先获取之前的数组,这个 table 就是ThreadLocalMap中的tale 变量
	Entry[] tab = table;
	// 获取长度
	int len = tab.length;
	// 计算 要存放在数组中的位置
	int i = key.threadLocalHashCode & (len-1);
	
	for (Entry e = tab[i];
		e != null;
		e = tab[i = nextIndex(i, len)]) {
		ThreadLocal<?> k = e.get();

		if (k == key) {
			e.value = value;
			return;
		}

		if (k == null) {
			replaceStaleEntry(key, value, i);
			return;
		}
	}
	
	// 保存数据
	tab[i] = new Entry(key, value);
	int sz = ++size;
	if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash();
}

上面的代码,大致就是获取原来的table数组,然后计算要添加的数据应该保存在数组的哪个位置,最后new 一个Entry实例,保存到该位置上去。在上面代码中可以找到这行代码:

tab[i] = new Entry(key, value);

这个key 就是传入的 当前ThreadLocal 的实例对象, value是要保存的变量数据。
我们再看一下Entry类的定义及该类的构造方法。

static class Entry extends WeakReference<ThreadLocal<?>> {

	Object value;

	Entry(ThreadLocal<?> k, Object v) {
		super(k);
		value = v;
	}
}

最终,我们要保存的数据,就是Entry中的value。

最后,简单总结:

    调用 ThreadLocal.set()保存数据时,实际是将数据 以Entry的形式 保存到 当前Thread实例中的 threadLocals 变量中的 tables 数组中。由于Thread代表线程实例,所以各个线程所保存的数据互不可见,互不影响,只由各自的线程独自进行维护。

细心的人可能会发现,Entry类 继承了一个 WeakReference<ThreadLocal<?>>的 弱引用类型

为什么Entry的key 是弱引用(WeakReference)类型的ThreadLocal实例?

    首先我们需要了解Java中的强、软、弱、虚这四种引用。这里不再做详细说明。而其中弱引用就是表示: 只要下次垃圾回收启动时,就回收当前实例

    试想一下,如果这里不使用弱引用而使用强引用,实质上就会造成该Entry与当前线程的生命周期绑定,只要线程没有销毁,那么节点在GC分析中一直处于可达状态,没办法被回收,而程序本身也无法判断是否可以清理节点,但是很有可能该Entry已经不会再被使用到。这就可能导致内存泄漏问题。

    当然了,即使这里Entry中的key为弱引用,也并不保证不会发生内存泄漏问题,因为Entry中的value仍然是强引用。即使在垃圾回收时,key被回收,这时该Entry的 key == null, 但是仍然有一个value是强引用状态而导致该Entry不能被回收。

怎样防止内存泄漏

最保险的方式,是在每次使用了ThreadLocal时,调用ThreadLocal中的**remove()**方法,即可避免出现内存泄漏问题。

对remove()源码的分析,参考:ThreadLocal源码分析:(三)remove()方法

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值