zoukankan      html  css  js  c++  java
  • 并发——深入分析ThreadLocal的实现原理

    一、前言

      这篇博客来分析一下ThreadLocal的实现原理以及常见问题,由于现在时间比较晚了,我就不废话了,直接进入正题。


    二、正文

    2.1 ThreadLocal是什么

      在讲实现原理之前,我先来简单的说一说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变量存储值,但是输出结果中,两个线程的值却互不影响,线程11输出到10,而线程2100输出到91。这就是ThreadLocal的功能,即让每一个线程拥有自己独立的变量,多个线程之间互不影响。


    2.2 ThreadLocal的实现原理

      下面我就就来说一说ThreadLocal是如何做到线程之间相互独立的,也就是它的实现原理。这里我直接放出结论,后面再根据源码分析:每一个线程都有一个对应的Thread对象,而Thread类有一个成员变量,它是一个Map集合,这个Map集合的key就是ThreadLocal的引用,而value就是当前线程在key所对应的ThreadLocal中存储的值。当某个线程需要获取存储在ThreadLocal变量中的值时,ThreadLocal底层会获取当前线程的Thread对象中的Map集合,然后以ThreadLocal作为key,从Map集合中查找value值。这就是ThreadLocal实现线程独立的原理。也就是说,ThreadLocal能够做到线程独立,是因为值并不存在ThreadLocal中,而是存储在线程对象中。下面我们根据ThreadLocal中两个最重要的方法来确认这一点。


    2.3 ThreadLocal中的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中,而这个mapkey就是当前ThreadLocal的引用。而ThreadLocal中,获取线程的ThreadLocalMap的方法getMap的代码如下:

    ThreadLocalMap getMap(Thread t) {
        // 直接返回Thread对象的threadLocals成员变量
        return t.threadLocals;
    }
    

      我们再看看Thread类中的threadLocals变量:

    /** 可以看到,ThreadLocalMap是ThreadLocal的内部类 */
    ThreadLocal.ThreadLocalMap threadLocals = null;
    

    2.4 ThreadLocal中的set方法

      下面再来看一看ThreadLocalset方法的实现,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.5 ThreadLocalMap的中的弱引用

      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类了。从代码中,我们可以看到,Entrykey值,也就是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对象的ThreadLocalMapkey。我们之前反复说过,ThreadLocalMapkey就是ThreadLocal对象的引用,若这个引用是一个强引用,那么在当前线程执行完毕,被回收前,ThreadLocalMap不会被回收,而ThreadLocalMap不会被回收,它的key引用的ThreadLocal也就不会回收,这就是问题的所在。而使用弱引用就可以保证,在其他对ThreadLocal的强引用解除后,ThreadLocalMap对它的引用不会影响JVM对它进行垃圾回收。这就是使用弱引用的原因。


    2.6 ThreadLocal造成的内存溢出问题

      上面描述了对ThreadLocalMapkey使用弱引用,来避免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存在。我们前面说过,ThreadLocalMapvalue是强引用,这也就意味着虽然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
    

      做了上面的修改后,没有再发生内存溢出异常,程序正常执行完毕。这是为什么呢?ThreadLocalremove方法究竟有什么作用。其实remove方法的作用非常简单,执行remove方法时,会从当前线程的ThreadLocalMap中删除key为当前ThreadLocal的那一个记录,keyvalue都会被置为null,这样一来,就解除了ThreadLocalMapvalue的强引用,使得value可以正常地被JVM回收了。所以,今后如果我们确认不再使用的ThreadLocal对象,一定要记得调用它的remove方法。

      我们之前说过,如果我们没有调用remove方法,那就会导致ThreadLocal在使用完毕后,被正常回收,但是ThreadLocalMap中存放的value无法被回收,此时将会在ThreadLocalMap中出现keynull,而value不为null的元素。为了减少已经无用的对象依旧占用内存的现象,ThreadLocal底层实现中,在操作ThreadLocalMap的过程中,线程若检测到keynull的元素,会将此元素的value置为null,然后将这个元素从ThreadLocalMap中删除,占用的内存就可以让JVM将其回收。比如说在getEntry方法中,或者是Map扩容的方法中等。


    三、总结

      ThreadLocal实现线程独立的方式是直接将值存放在Thread对象的ThreadLocalMap中,Mapkey就是ThreadLocal的引用,且为了有助于JVM进行垃圾回收,key使用的是弱引用。在使用ThreadLocal后,一定要记得调用remove方法,有助于JVMvalue的回收。


    四、参考

  • 相关阅读:
    ABI与ARM,X86的概念
    数据库升级,如何操作
    shell脚本
    数据库设计范式
    jQuery基础教程
    git clone 失败 fatal: early EOF fatal: the remote end hung up unexpectedly fatal: index-pack failed
    windowserver中PowerShell禁止脚本执行的解决方法
    移动端延迟300ms的原因以及解决方案
    将伪数组转为真正的数组
    cnpm安装时候出现“Unexpected end of JSON input“的解决办法
  • 原文地址:https://www.cnblogs.com/tuyang1129/p/12713815.html
Copyright © 2011-2022 走看看