【多线程】ThreadLocal是什么?有哪些使用场景?使用ThreadLocal需要注意些什么?

前言

一、ThreadLocal 是什么?

ThreadLocal 是线程本地存储,在每个线程中都创建了一个 ThreadLocalMap 对象,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value。

二、有哪些使用场景?

经典的使用场景是为每个线程分配一个 JDBC 连接 Connection。这样就可以保证每个线程的都在各自的 Connection 上进行数据库的操作,不会出现 A 线程关了 B线程正在使用的 Connection; 还有 Session管理等问题。

ThreadLocal 使用例子:

public class TestThreadLocal {
    
    
	
	//线程本地存储变量
	private static final ThreadLocal<Integer> THREAD_LOCAL_NUM = new ThreadLocal<Integer>() {
    
    
		@Override
		protected Integer initialValue() {
    
    
			return 0;
		}
	};
 
	public static void main(String[] args) {
    
    
		for (int i = 0; i < 3; i++) {
    
    //启动三个线程
			Thread t = new Thread() {
    
    
				@Override
				public void run() {
    
    
					add10ByThreadLocal();
				}
			};
			t.start();
		}
	}
	
	/**
	 * 线程本地存储变量加 5
	 */
	private static void add10ByThreadLocal() {
    
    
		for (int i = 0; i < 5; i++) {
    
    
			Integer n = THREAD_LOCAL_NUM.get();
			n += 1;
			THREAD_LOCAL_NUM.set(n);
			System.out.println(Thread.currentThread().getName() + " : ThreadLocal num=" + n);
		}
	}
	
}

打印结果:启动了 3 个线程,每个线程最后都打印到 “ThreadLocal num=5”,而不是 num 一直在累加直到值等于 15

Thread-0 : ThreadLocal num=1
Thread-1 : ThreadLocal num=1
Thread-0 : ThreadLocal num=2
Thread-0 : ThreadLocal num=3
Thread-1 : ThreadLocal num=2
Thread-2 : ThreadLocal num=1
Thread-0 : ThreadLocal num=4
Thread-2 : ThreadLocal num=2
Thread-1 : ThreadLocal num=3
Thread-1 : ThreadLocal num=4
Thread-2 : ThreadLocal num=3
Thread-0 : ThreadLocal num=5
Thread-2 : ThreadLocal num=4
Thread-2 : ThreadLocal num=5
Thread-1 : ThreadLocal num=5

三、实现原理

按照我们第一直觉,感觉 ThreadLocal 内部肯定是有个 Map 结构,key 存了 Thread,value 存了 本地变量 V 的值。每次通过 ThreadLocal 对象的 get() 和 set(T value) 方法获取当前线程里存的本地变量、设置当前线程里的本地变量。

在这里插入图片描述

而 JDK 的实现里面这个 Map 是属于 Thread,而非属于 ThreadLocal。ThreadLocal 仅是一个代理工具类,内部并不持有任何与线程相关的数据,所有和线程相关的数据都存储在 Thread 里面。ThreadLocalMap 属于 Thread 也更加合理。

还有一个更加深层次的原因,这样设计不容易产生内存泄露。
ThreadLocal 持有的 Map 会持有 Thread 对象的引用,只要 ThreadLocal 对象存在,那么 Map 中的 Thread 对象就永远不会被回收。ThreadLocal 的生命周期往往比线程要长,所以这种设计方案很容易导致内存泄露。

JDK 的实现中 Thread 持有 ThreadLocalMap,而且 ThreadLocalMap 里对 ThreadLocal 的引用还是弱引用(WeakReference),所以只要 Thread 对象可以被回收,那么 ThreadLocalMap 就能被回收。JDK 的这种实现方案复杂但更安全。

在这里插入图片描述

四、在线程池中使用 ThreadLocal 为什么可能导致内存泄露呢?

在线程池中线程的存活时间太长,往往都是和程序同生共死的,这样 Thread 持有的 ThreadLocalMap 一直都不会被回收,再加上 ThreadLocalMap 中的 Entry 对 ThreadLocal 是弱引用(WeakReference),所以只要 ThreadLocal 结束了自己的生命周期是可以被回收掉的。
Entry 中的 Value 是被 Entry 强引用的,即便 value 的生命周期结束了,value 也是无法被回收的,导致内存泄露。

五、线程池中,如何正确使用 ThreadLocal?

在 finally 代码块中手动清理 ThreadLocal 中的 value,调用 ThreadLocal 的 remove()方法。

六、ThreadLocal 核心方法

  • 设置 Thread 对应的 Value 值,首次会创建一个 ThreadLocalMap ,添加 ThreadLocal - Value 到 ThreadLocalMap 中,并且绑定 ThreadLocalMap 到当前线程。
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,绑定到当前线程。
void createMap(Thread t, T firstValue) {
    
    
	t.threadLocals = new ThreadLocalMap(this, firstValue);
}
 
  • 通过 ThreadLocalMap 获取当前线程的存储的 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();
}
 
  • 设置 ThreadLocal 的初始化值,未 set(T value) 初次获取 Thread 对应的 Value 值时会调用,即被 setInitialValue 方法调用。需要重写该方法。
protected T initialValue() {
    
    
	return null;
}
  • 移除当前线程存储的 Value 值。当 ThreadLocal 不在使用,最好在 finally 语句块中,调用 remove() 方法,释放去 Value 的引用,避免内存泄露。
public void remove() {
    
    
	ThreadLocalMap m = getMap(Thread.currentThread());
	if (m != null)
		m.remove(this);
}

猜你喜欢

转载自blog.csdn.net/u011397981/article/details/132757499