深入浅出ThreadLocal

1,887 阅读6分钟

ThreadLocal相信大家都有用过的,一般用作存取一些全局的信息。比如用户信息,流程信息,甚至在Spring框架里面通过事务注解Transactional去获取数据库连接的实现上,也有它的一份功劳。

ThreadLocal作为一个进阶必会知识点,而且还是面试高频考点。网上博客对它的解读也必然不会少,但是网上博客解读水平良莠不齐,看多了难免会绕。不如自己亲自再梳理一遍,顺便记录下自己的解读。

ThreadLocal的线程隔离性Demo

先来看一个小的demo

static ThreadLocal<Student> threadLocal = new ThreadLocal<Student>();

public static void main(String[] args) {
    threadLocalTest1();
}

private static void threadLocalTest1() {
    new Thread(new Runnable() {
        public void run() {
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        System.out.println(threadLocal.get());
        }
    }).start();


    new Thread(new Runnable() {
        public void run() {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            threadLocal.set(new Student("zhangsan"));
        }
    }).start();
}

如代码所示,开启两个线程。

第一个线程3秒之后去取在静态变量threadLocal里的变量。

第二个线程1秒之后去设置threadLocal里的变量。

这段代码运行的结果就是,第一个线程永远获取不到第二个线程给静态变量threadLocal里设置的变量。

结论:不同的线程操作同一个threadLocal对象,可以实现线程信息之间的隔离。

猜想:看到set方法和get方法,大胆猜想threadLocal对象里面有个Map,key为当前线程,value为ThreadLocal泛型里的对象,这样就实现了在空间上的线程安全性。

但事实并不是这样,答案不在猜想中,而在源码中。

ThreadLocal.set()方法源码解读

public void set(T value) {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // 把当前ThreadLocal对象作为key,调用set方法的入参对象作为value,放入当前线程的ThreadLocalMap
        map.set(this, value);
    else
        // 通过当前线程和调用set方法的入参对象去构造Map
        createMap(t, value);
}

ThreadLocal.get()方法源码解读

public T get() {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取当前线程的ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    if (map != null) {
	// 通过当前ThreadLocal对象去取Entry
	ThreadLocalMap.Entry e = map.getEntry(this);
	if (e != null) {
	    @SuppressWarnings("unchecked")
	    // 获取Entry的value返回
	    T result = (T)e.value;
	    return result;
	}
    }
    return setInitialValue();
}

那么ThreadLocalMap又是什么呢?

再点进源码,你会发现,ThreadLocalMap是ThreadLocal的一个静态内部类,同时在Thread类下有一个成员变量ThreadLocals

ThreadLocal.ThreadLocalMap threadLocals = null

那我们顺着逻辑再下去看看ThreadLocalMap的set方法用来干嘛的。

ThreadLocalMap.set()方法源码解读

private void set(ThreadLocal<?> key, Object value) {
	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;
		}
	}
	// 设置Entry
	tab[i] = new Entry(key, value);
	int sz = ++size;
	if (!cleanSomeSlots(i, sz) && sz >= threshold)
		rehash();
}

其实我们在知道了HashMap的底层原理实现的基础上去理解上述代码并不难,取Entry数组长度,哈希,与运算取模,设置Entry一气呵成。

但是这里需要注意的是,也是ThreadLocal最有特色的一点,是这个Entry并不是普通理解里的Entry,而是ThreadLocalMap里面的一个静态内部类并且继承了WeakReference

static class Entry extends WeakReference<ThreadLocal<?>> {
	Object value;

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

到此,是不是对几个带有Thread的名词弄的有点晕,休息一下,我们先来看张图梳理一下ThreadLocal,ThreadLocalMap,与Thread的关系,如图:

层层深入,这时候问题又来了,弱引用是啥?啥是弱引用?弱什么?什么引用?

WeakReference演示

public static void main(String[] args) {
    wrTest();
}

private static void wrTest() {
    WeakReference<Student> weakReference = new WeakReference<Student>(new Student("aaaa"));
    System.out.println(weakReference.get());
    System.gc();
    System.out.println(weakReference.get());
}

输出的结果为:

Student@1b6d3586
null

在这个demo里面,我们可以很清晰的看到弱引用的特性:当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。

当垃圾回收器执行一次之后,原来的弱引用关联的对象就为null了,它拥有这样的特质,又有什么用呢?

这就得引出内存泄漏的问题了。

内存泄漏

使用ThreadLocal哪些情况会产生内存泄漏?

调用ThreadLocal的set方法设置某个对象进去,后来这个对象回收不了。久而久之,影响程序运行速度,最终造成OOM。

为什么会回收不了?是因为垃圾回收器执行之后,CurrentThread依然运行的前提下,Entry[]一直存在,但是其中有些key由于是继承了WeakReference,在GC之后其get方法返回值就是null了,导致取不到Entry里面key.get()为null的KEY所对应的value,而这块value永远也访问不到了。如图:

使用ThreadLocal如何避免内存泄漏?

把ThreadLocal定义为static,保持单例,不被回收。

用完ThreadLocal,需要手动擦除对应的Entry节点信息,记得调用ThreadLocal的remove方法。

特别是在实际项目的场景下,大多数情况下线程都是交给线程池在管理。一个线程任务跑完,通常不会立即销毁,而是放在线程池里面等待下次任务的来临(有种说法说是在把线程放回线程池的过程中会擦除Thread下的ThreadLocal.ThreadLocalMap threadLocals信息,当然,这是线程池帮我们做的)无论线程池是否帮我们擦除,我们用完ThreadLocal手动remove总是安全的。

附上remove的源码。

public void remove() {
	// 获取当前线程的ThreadLocalMap
	ThreadLocalMap m = getMap(Thread.currentThread());
	if (m != null){
		m.remove(this);
	}
}
	 
private void remove(ThreadLocal<?> key) {
    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)]) {
         // 匹配key
         if (e.get() == key) {
    	 // 清除一个Entry元素
    	 e.clear();
    	 expungeStaleEntry(i);
    	 return;
        }
    }
}

总结

解读完ThreadLocal的源码,再回归到它的命名,理解又深了一个层次:

Thread + Local = 线程 + 本地 = 线程本地变量 = 把某个对象放在了线程本地。

文理不分家,不妨借用文科的思维打比方去理解它:

ThreadLocal对象就像一个具体的客观的对象,可以是某个话题,某部电影,某本书,甚至某个人。

而每个Thread就像一个人,读者,旁观者。

Thread对ThreadLocal的set操作和get操作,就分别对应是一个人对某个客观的对象进行设置主观印象和获取主观印象。即便是同一个对象,不同的人会对其有不同的主观印象并记录在自己的脑海里,在每个人来看这些印象都是合理的,无论你处在哪个上下文,总能快速获取到这个印象信息,而不会错乱。

在空间上保留对象的副本,通过空间换时间的思想,也就实现了ThreadLocal的线程安全性。