深入理解并发编程之ThreadLocal原理分析
文章目录
一、什么是ThreadLocal
ThreadLocal提供了线程本地变量,它可以保证访问到的变量属于当前线程,每个线程都保存有一个变量副本,每个线程的变量都不同。ThreadLocal相当于提供了一种线程隔离,将变量与线程相绑定。简单的说就是ThreadLocal为多个线程提供了互不干扰的本地变量。
Threadlocal适用于在多线程的情况下,可以实现传递数据,实现线程隔离。
Thread有三个核心方法:
- set(): 设置当前线程绑定的局部变量。
- get(): 获取当前线程绑定的局部变量。
- remove(): 移除当前线程绑定的变量。
简单的来看一个ThreadLocal的使用Demo:
public class Test001 {
private static ThreadLocal threadLocal = new ThreadLocal<String>();
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 5; i++) {
new Thread(() -> {
threadLocal.set("这是线程"+Thread.currentThread().getName());
System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
}).start();
}
threadLocal.set("这里是主线程");
System.out.println(Thread.currentThread().getName() + ":" + threadLocal.get());
}
}
通过打印结果我们可以看出来,线程之间并不会出现打印错乱的并发问题,没有出现例如线程1可能打印到线程2的情况:
ThreadLocal的使用场景是比较多的,例如:
- Spring的数据库事务模板用来缓存对应的数据库连接。
- SpringMVC获取HttpRequest对象,Spring将HttpRequest对象都缓存到对应的线程中就是使用的ThreadLocal。
二、ThreadLocal原理分析
ThreadLocalMap
Thread(注意这里是Thread而不是ThreadLocal)有个核心属性ThreadLocalMap threadLocals,这是每个线程都有一份的,他们之间相互独立隔离。
下面是ThreadLocalMap的UML图:
看图其实和Map差不多的,这里主要是分析ThreadLocal的,对于ThreadLocalMap具体是怎么存储的不做深究了,知道是通过ThreadLocal的哈希值来计算数组存放位置来存放获取变量的就行,可以看出是通过Entry数组来保存局部变量的,再根据我们对Map的了解,看一下ThreadLocalMap的get()和set()相关的方法可以看出,它的key是ThreadLocal,Value是我们实际存放的变量。其实这里曾经有过一个面试题:为什么线程缓存的是ThreadLocalMap对象而不是ThreadLocal?,其实真正使用过ThreadLocal就知道答案非常简单,因为一个线程有使用的不止有一个ThreadLocal,所以我们要把它存放在Map中,通过ThreadLocal来存取对应的值。
set()方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value); //this就是指的当前的ThreadLocal
else
createMap(t, value);
}
看到ThreadLocal的set()源码非常简单,首先获取当前线程,然后获取当前线程自己的Map,最后使用当前ThreadLocal作为Key往Map里面赋值
get()方法
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();
}
get()方法也是非常简单的,就是获取当前线程,然后获取当前线程的ThreadLocalMap,最后获取ThreadLocalMap中Key为当前ThreadLocal的value值
三、ThreadLocal涉及的问题
ThreadLocal的内存泄漏问题
先说几个词:内存泄漏、内存溢出、强引用、 软引用、 弱引用和虚引用。
内存泄漏:指程序在申请内存后,无法释放已申请的内存空间,内存泄露堆积会导致内存被占光,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
内存溢出:指程序在申请内存时,没有足够的内存空间供其使用,出现 out of memory, OOM溢出,程序在申请内存时,没有足够的内存空间使用,一般解决办法:加内存。
强引用:被引用关联的对象永远不会被垃圾收集器回收,Object obj = new Object()。
软引用:软引用关联的对象,只有当系统内存溢出时,才会回收软引用的对象,SoftReference objRef = new SoftReference(obj)。
弱引用:只被弱引用关联的对象,当垃圾回收机制触发的时候就会被回收,WeakReference objRef = new WeakReference(obj),ThreadLocal就是使用的弱引用。
虚引用:它就和没有任何引用一样,在任何时候都可能被垃圾回收,通过PhantomReference来实现。
为什么ThreadLocal会有内存泄漏问题?
先看下面一段测试代码:
public class Test001 {
private static ThreadLocal threadLocal = new ThreadLocal<String>();
public static void main(String[] args) throws InterruptedException {
threadLocal.set("测试");
threadLocal = null;
System.gc();
Thread thread = Thread.currentThread(); //在这一行打断点看一下
System.out.println("打印断点");
}
}
看一下断点打印,我们可以看到Thread里面的ThreadLocal并没有被清理:
出现这个现象的原因很简单
ThreadLocalMap使用ThreadLocal的弱引用作为key,上面我们说了弱引用会在垃圾回收的时候清除掉,虽然ThreadLocalMap中key为null,但是而value还存在着强引用,只有thead线程退出以后,value的强引用链条才会断掉。我们看上面的打印结果就看到了ref已经为null了,但是value还是存在的。
解决办法:
- 可以自己调用remove()方法将不要的数据移除避免内存泄漏的问题。
- 每次在做set方法的时候会清除之前key为null,ThreadLocalMap因为每次调用set方法的时候,会判断如果key空的情况下,直接删除。
弱引用存在内存泄漏问题为啥还要用弱引用
首先,如果使用强引用的话,即使我们把ThreadLocal设置为null了,ThreadLocalMap的ThreadLocal还会继续存在,这样是肯定会存在内存泄漏问题的,而弱引用是可能出现内存泄漏问题,我们处理得当可以避免的;而软引用更不可能了,软引用得内存溢出才会回收的。
内容来源:蚂蚁课堂