【转载】并发——深入分析ThreadLocal的实现原理
一、前言
这篇博客来分析一下ThreadLocal的实现原理以及常见问题,由于现在时间比较晚了,我就不废话了,直接进入正题。
二、正文
2.1ThreadLocal是什么
在讲实现原理之前,我先来简单的说一说ThreadLocal是什么。ThreadLocal被称作线程局部变量,当我们定义了一个ThreadLocal变量,所有的线程共同使用这个变量,但是对于每一个线程来说,实际操作的值是互相独立的。简单来说就是,ThreadLocal能让线程拥有自己内部独享的变量。举一个简单的例子:
// 定义一个线程共享的ThreadLocal变量 static ThreadLocal<Integer> tl = new ThreadLocal<>(); public static void main(String[] args) { // 创建第一个线程 Thread t1 = new Thread(() -> { // 设置ThreadLocal变量的初始值,为1 tl.set(1); // 循环打印ThreadLocal变量的值 for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + "----" + tl.get()); // 每次打印完让值 + 1 tl.set(tl.get() + 1); } }, "thread1"); // 创建第二个线程 Thread t2 = new Thread(() -> { // 设置ThreadLocal变量的初始值,为100,与上一个线程区别开 tl.set(100); // 循环打印ThreadLocal变量的值 for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName() + "----" + tl.get()); // 每次打印完让值 - 1 tl.set(tl.get() - 1); } }, "thread2"); // 开启两个线程 t1.start(); t2.start(); tl.remove(); }
上面的代码,运行结果如下(注:每次运行的结果可能不同):
thread1----1 thread2----100 thread1----2 thread2----99 thread1----3 thread2----98 thread1----4 thread2----97 thread1----5 thread2----96 thread1----6 thread2----95 thread1----7 thread2----94 thread1----8 thread2----93 thread1----9 thread2----92 thread1----10 thread2----91
通过上面的输出结果我们可以发现,线程1和线程2虽然使用的是同一个ThreadLocal变量存储值,但是输出结果中,两个线程的值却互不影响,线程1从1输出到10,而线程2从100输出到91。这就是ThreadLocal的功能,即让每一个线程拥有自己独立的变量,多个线程之间互不影响。
2.2ThreadLocal得实现原理
下面我就就来说一说ThreadLocal是如何做到线程之间相互独立的,也就是它的实现原理。这里我直接放出结论,后面再根据源码分析:每一个线程都有一个对应的Thread对象,而Thread类有一个成员变量,它是一个Map集合,这个Map集合的key就是ThreadLocal的引用,而value就是当前线程在key所对应的ThreadLocal中存储的值。当某个线程需要获取存储在ThreadLocal变量中的值时,ThreadLocal底层会获取当前线程的Thread对象中的Map集合,然后以ThreadLocal作为key,从Map集合中查找value值。这就是ThreadLocal实现线程独立的原理。也就是说,ThreadLocal能够做到线程独立,是因为值并不存在ThreadLocal中,而是存储在线程对象中。下面我们根据ThreadLocal中两个最重要的方法来确认这一点。
2.3ThreadLocal中得get方法
get方法的作用非常简单,就是线程向ThreadLocal中取值,下面我们来看看它的源码:
public T get() { // 获取当前线程的Thread对象 Thread t = Thread.currentThread(); // getMap方法传入Thread对象,此方法将返回Thread对象中存储的一个Map集合 // 这个Map集合的类型为ThreadLocalMap,这是ThreadLoacl的一个内部类 // 当前线程存放在ThreadLocal中的值,实际上存放在这个Map集合中 ThreadLocalMap map = getMap(t); // 如果当前Map集合已经初始化,则直接从Map集合中查找 if (map != null) { // ThreadLocalMap的key其实就是ThreadLoacl对象的引用 // 所以要找到线程在当前ThreadLoacl中存放的值,就需要以当前ThreadLoacl作为key // getEntry方法就是通过key获取map中的一个key-value,而这里使用的key就是this ThreadLocalMap.Entry e = map.getEntry(this); // 如果返回值不为空,表示查找成功 if (e != null) { @SuppressWarnings("unchecked") // 于是获取对应的value并返回 T result = (T)e.value; return result; } } // 若当前线程的ThreadLocalMap还未初始化,或者查找失败,则调用以下方法 return setInitialValue(); } private T setInitialValue() { // 此方法默认返回null,但是可以由子类进行重新,根据需求返回需要的值 T value = initialValue(); // 获取当前线程的Thread对象 Thread t = Thread.currentThread(); // 获取对应的ThreadLocalMap ThreadLocalMap map = getMap(t); // 如果Map已经初始化了,就直接往map中加入一个key-value // key就是当前ThreadLocal对象的引用,而value就是上面获取到的value,默认为null if (map != null) map.set(this, value); // 若还没有初始化,则调用createMap创建ThreadLocalMap对象 else createMap(t, value); // 返回initialValue方法返回的值,默认为null return value; } void createMap(Thread t, T firstValue) { // 创建ThreadLocalMap对象,构造方法传入的是第一对放入其中的key-value // 这个key也就是当前线程第一次调用get方法的ThreadLocal对象,也就是当前ThreadLocal对象 // 而firstValue则是initialValue方法的返回值,默认为null t.threadLocals = new ThreadLocalMap(this, firstValue); }
上面的代码非常直观的验证了我之前说过的ThreadLocal的实现原理。通过上面的代码,我们可以非常直观的看到,线程向ThreadLocal中存放的值,最后都放入了线程自己的ThreadLocalMap中,而这个map的key就是当前ThreadLocal的引用。而ThreadLocal中,获取线程的ThreadLocalMap的方法getMap的代码如下:
ThreadLocalMap getMap(Thread t) { // 直接返回Thread对象的threadLocals成员变量 return t.threadLocals; }
我们再看看Thread类中的threadLocals变量:
/** 可以看到,ThreadLocalMap是ThreadLocal的内部类 */ ThreadLocal.ThreadLocalMap threadLocals = null;
2.4ThreadLocal中的set方法
下面再来看一看ThreadLocal的set方法的实现,set方法用来使线程向ThreadLocal中存放值(实际上是存放在线程自己的Map中):
public void set(T value) { // 获取当前线程的Thread对象 Thread t = Thread.currentThread(); // 获取当前线程的ThreadLocalMap ThreadLocalMap map = getMap(t); // 若map已经初始化,则之际将value放入Map中,对应的key就是当前ThreadLocal的引用 if (map != null) map.set(this, value); // 若没有初始化,则调用createMap方法,为当前线程t创建ThreadLocalMap, // 然后将key-value放入(此方法已经在上面讲解get方法是看过) else createMap(t, value); }
这就是set方法的实现,比较简单。看完上面两个关键方法的实现,相信大家对ThreadLocal的实现已经有了一个比较清晰的认识,下面我们来更加深入的分析ThreadLocal,看看ThreadLocalMap的一些实现细节。
2.5ThreadLocalMap中的弱引用
ThreadLocalMap的实现其实就是一个比较普通的Map集合,它的实现和HashMap类似,所以具体的实现细节我们就不一一讲解了,这里我们只关注它最特别的一个地方,即它内部的节点Entry。我们先来看看Entry的代码:
// Entry是ThreadLocalMap的内部类,表示Map的节点 // 这里继承了WeakReference,这是java实现的弱引用类,泛型为ThreadLocal // 表示在这个Map中,作为key的ThreadLocal是弱引用 // (这里value是强引用,因为没用WeakReference) static class Entry extends WeakReference<ThreadLocal<?>> { /** 存储value */ Object value; Entry(ThreadLocal<?> k, Object v) { // 将key的值传入父类WeakReference的构造方法,用弱引用来引用key super(k); // value则直接使用上面的强引用 value = v; } }
可以看到,上面的Entry比较特殊,它继承自WeakReference类型,这是Java实现的弱引用。在具体讲解前,我们先来介绍一下不同类型的引用:
强引用:这是Java中最常见的引用,在没有使用特殊引用的情况下,都是强引用,比如Object o = new Object()就是典型的强引用。能让程序员通过强引用访问到的对象,不会被JVM垃圾回收,即使内存空间不够,JVM也不会回收这些对象,而是抛出内存溢出异常;
软引用:软引用描述的是一些还有用,但不是必须的对象。被软引用所引用的对象,也不会被垃圾回收,直到JVM将要发生内存溢出异常时,才会将这些对象列为回收对象,进行回收。在JDK1.2之后,提供了SoftReference类实现软引用;
弱引用:弱引用描述的是非必须的对象,被弱引用所引用的对象,只能生存到下一次垃圾回收前,下一次垃圾回收来临,此对象就会被回收。在JDK1.2之后,提供了WeakReference类实现弱引用(也就是上面Entry继承的类);
虚引用:这是最弱的一种引用关系,一个对象是否有虚引用,完全不会对其生存时间产生影响,我们也不能通过一个虚引用访问对象,使用虚引用的唯一目的就是,能在这个对象被回收时,受到一个系统的通知。JDK1.2之后,提供了PhantomReference实现虚引用;
介绍完各类引用的概念,我们就可以来分析一下Entry为什么需要继承WeakReference类了。从代码中,我们可以看到,Entry将key值,也就是ThreadLocal的引用传入到了WeakReference的构造方法中,也就是说在ThreadLocalMap中,key的引用是弱引用。这表明,当没有其他强引用指向key时,这个key将会在下一次垃圾回收时被JVM回收。
为什么需要这么做呢?这么做的目的自然是为了有利于垃圾回收了。如果了解过JVM的垃圾回收算法的应该知道,JVM判断一个对象是否需要被回收,判断的依据是这个对象还能否被我们所使用,举个简单的例子:
public static void main(String[] args) { Object o = new Object(); o = null; }
上面的代码中,我们创建了一个对象,并使用强引用o指向它,然后我们将o置为空,这个时候刚刚创建的对象就丢失了,因为我们无法通过任何引用找到这个对象,从而使用它,于是这个对象就需要被回收,这种判断依据被称为可达性分析。关于JVM的垃圾回收算法,可以参考这篇博客:Java中的垃圾回收算法详解。
好,回归正题,我们开始分析为什么ThreadLocalMap需要让key使用弱引用。假设我们创建了一个ThreadLocal,使用完之后没有用了,我们希望能够让它被JVM回收,于是有了下面这个过程:
// 创建ThreadLocal对象 ThreadLocal tl = new ThreadLocal(); // .....省略使用的过程... // 使用完成,希望被JVM回收,于是执行以下操作,解除强引用 tl = null;
我们在使用完ThreadLocal之后,解除对它的强引用,希望它被JVM回收。但是JVM无法回收它,因为我们虽然在此处释放了对它的强引用,但是它还有其它强引用,那就是Thread对象的ThreadLocalMap的key。我们之前反复说过,ThreadLocalMap的key就是ThreadLocal对象的引用,若这个引用是一个强引用,那么在当前线程执行完毕,被回收前,ThreadLocalMap不会被回收,而ThreadLocalMap不会被回收,它的key引用的ThreadLocal也就不会回收,这就是问题的所在。而使用弱引用就可以保证,在其他对ThreadLocal的强引用解除后,ThreadLocalMap对它的引用不会影响JVM对它进行垃圾回收。这就是使用弱引用的原因。
2.6ThreadLocal造成内存溢出的问题
上面描述了对ThreadLocalMap对key使用弱引用,来避免JVM无法回收ThreadLocal的问题,但是这里却还有另外一个问题。我们看上面Entry的代码发现,key值虽然使用的弱引用,但是value使用的却是强引用。这会造成一个什么问题?这会造成key被JVM回收,但是value却无法被收,key对应的ThreadLocal被回收后,key变为了null,但是value却还是原来的value,因为被ThreadLocalMap所引用,将无法被JVM回收。若value所占内存较大,线程较多的情况下,将持续占用大量内存,甚至造成内存溢出。我们通过一段代码演示这个问题:
public class Main { public static void main(String[] args) { // 循环创建多个TestClass for (int i = 0; i < 100; i++) { // 创建TestClass对象 TestClass t = new TestClass(i); // 调用反复 t.printId(); // *************注意此处,非常关键:为了帮助回收,将t置为null t = null; } } static class TestClass { int id; // 每个TestClass对象对应一个很大的数组 int[] arr = new int[100000000]; // 每个TestClass对象对应一个ThreadLocal对象 ThreadLocal<int[]> threadLocal = new ThreadLocal<>(); TestClass(int id) { this.id = id; // threadLocal存放的就是这个很大的数组 threadLocal.set(arr); } public void printId() { System.out.println(id); } } }
上面的代码多次创建所占内存非常大的对象,并在创建后,立即解除对象的强引用,让对象可以被JVM回收。按道理来说,上面的代码运行应该不会发生内存溢出,因为我们虽然创建了多个大对象,占用了大量空间,但是这些对象立即就用不到了,可以被垃圾回收,而这个对象被垃圾回收后,对象的id,数组,和threadLocal成员都会被回收,所以所占内存不会持续升高,但是实际运行结果如下:
0 1 2 Exception in thread "main" java.lang.OutOfMemoryError: Java heap space at Main$TestClass.<init>(Main.java:23) at Main.main(Main.java:10)
可以看到,很快就发生了内存溢出异常。为什么呢?需要注意到,在TestClass的构造方法中,我们将数组arr放入了ThreadLocal对象中,也就是被放进了当前线程的ThreadLocalMap中,作为value存在。我们前面说过,ThreadLocalMap的value是强引用,这也就意味着虽然ThreadLocal可以被正常回收,但是作为value的大数组无法被回收,因为它仍然被ThreadLocalMap的强引用所指向。于是TestClass对象的超大数组就一种在内存中,占据大量空间,我们连续创建了多个TestClass,内存很快就被占满了,于是发生了内存溢出。而JDK的开发人员自然发现了这个问题,于是有了下面这个解决方案:
public class Main { public static void main(String[] args) { for (int i = 0; i < 100; i++) { TestClass t = new TestClass(i); t.printId(); // **********注意,与上面的代码只有此处不同************ // 此处调用了ThreadLocal对象的remove方法 t.threadLocal.remove(); t = null; } } static class TestClass { int id; int[] arr; ThreadLocal<int[]> threadLocal; TestClass(int id) { this.id = id; arr = new int[100000000]; threadLocal = new ThreadLocal<>(); threadLocal.set(arr); } public void printId() { System.out.println(id); } } }
上面的代码中,我们在将t置为空时,先调用了ThreadLocal对象的remove方法,这样做了之后,再看看运行结果:
0 1 2 // ....神略中间部分 98 99
做了上面的修改后,没有再发生内存溢出异常,程序正常执行完毕。这是为什么呢?ThreadLocal的remove方法究竟有什么作用。其实remove方法的作用非常简单,执行remove方法时,会从当前线程的ThreadLocalMap中删除key为当前ThreadLocal的那一个记录,key和value都会被置为null,这样一来,就解除了ThreadLocalMap对value的强引用,使得value可以正常地被JVM回收了。所以,今后如果我们确认不再使用的ThreadLocal对象,一定要记得调用它的remove方法。
我们之前说过,如果我们没有调用remove方法,那就会导致ThreadLocal在使用完毕后,被正常回收,但是ThreadLocalMap中存放的value无法被回收,此时将会在ThreadLocalMap中出现key为null,而value不为null的元素。为了减少已经无用的对象依旧占用内存的现象,ThreadLocal底层实现中,在操作ThreadLocalMap的过程中,线程若检测到key为null的元素,会将此元素的value置为null,然后将这个元素从ThreadLocalMap中删除,占用的内存就可以让JVM将其回收。比如说在getEntry方法中,或者是Map扩容的方法中等。
三、总结
ThreadLocal实现线程独立的方式是直接将值存放在Thread对象的ThreadLocalMap中,Map的key就是ThreadLocal的引用,且为了有助于JVM进行垃圾回收,key使用的是弱引用。在使用ThreadLocal后,一定要记得调用remove方法,有助于JVM对value的回收。