• Java并发编程之ThreadLocal


    早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。ThreadLocal是指作用域为Thread的局部变量,也许把它命名为ThreadLocalVariable更容易让人理解一些。

    介绍

    多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,为了保证线程安全,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法,当我们在创建一个变量后,如果每个线程对其进行访问的时候访问的都是线程自己的变量这样就不会存在线程不安全问题。 ThreadLocal是JDK包提供的,它提供线程本地变量,如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,在实际多线程操作的时候,操作的是自己本地内存中的变量,从而规避了线程安全问题,如下图所示:

    ThreadLocal示例图

    ThreadLocal使用示例

    下面的例子中,开启两个线程,在每个线程内部设置了本地变量的值,然后调用print方法打印当前本地变量的值。如果在打印之后调用本地变量的remove方法会删除本地内存中的变量,代码如下所示:

    package test;
    
    public class ThreadLocalTest {
    
        static ThreadLocal<String> localVar = new ThreadLocal<>();
    
        static void print(String str) {
            //打印当前线程中本地内存中本地变量的值
            System.out.println(str + " :" + localVar.get());
            //清除本地内存中的本地变量
            localVar.remove();
        }
    
        public static void main(String[] args) {
            Thread t1  = new Thread(new Runnable() {
                @Override
                public void run() {
                    //设置线程1中本地变量的值
                    localVar.set("localVar1");
                    //调用打印方法
                    print("thread1");
                    //打印本地变量
                    System.out.println("after remove : " + localVar.get());
                }
            });
    
            Thread t2  = new Thread(new Runnable() {
                @Override
                public void run() {
                    //设置线程1中本地变量的值
                    localVar.set("localVar2");
                    //调用打印方法
                    print("thread2");
                    //打印本地变量
                    System.out.println("after remove : " + localVar.get());
                }
            });
    
            t1.start();
            t2.start();
        }
    }
    

    ThreadLocal的实现原理

    从上一节中我们可以看出,ThreadLocal主要有set和get方法,用于设置和获取线程中的变量,那么ThreadLocal是怎么实现这个功能的呢?和ThreadLocal实现相关的类主要有三个:ThreadLocal、Thread、ThreadLocalMap,三者之间的关系同样如下图所示:

    1. ThreadLocalMap:名字上看是Map,实际上是一个数组,不过它的功能和Map类似,可以按照key查找数据。
    2. Thread:线程大家应该都知道,那么在ThreadLocal中他起什么作用呢?一个Thread中会包含两个ThreadLocalMap,分别用于存储本线程和父线程的ThreadLocal数据。每一个ThreadLocal变量会在线程中对应一条ThreadLocalMap的key-value,其中key是ThreadLocal的唯一Hash值。
    3. ThreadLocal:每个ThreadLocal都会有一个唯一的Hash值,用于查找这个ThreadLocal在ThreadLocalMap中的值;ThreadLocal提供了方法用于获取当前线程的ThreadLocal数据。

    ThreadLocal示例图

    数据存放的位置

    ThreadLocal只是一层访问线程数据的壳,ThreadLocal get和set的数据不会在ThreadLocal的实例中存放,而是存放在线程Thread中的ThreadLocalMap,ThreadLocal只是提供了一个访问这些数据的途径。

    ThreadLoca的set方法将value添加到调用线程的ThreadLocalMap中,当调用线程调用get方法时候能够从它的ThreadLocalMap中取出变量。如果调用线程一直不终止,那么这个本地变量将会一直存放在他的ThreadLocalMap中,所以不使用本地变量的时候需要调用remove方法将ThreadLocalMap中删除不用的本地变量。

    set方法存放数据

    ThreadLocal方法的set可以向当前线程的ThreadLocalMap中放入数据,存放数据的源码如下所示,Set过程分为以下步骤:

    1. 获取当前线程。
    2. 从当前线程中获取ThreadLocalMap变量。
    3. 如果当前线程的ThreadLocalMap不为空,用当前的ThreadLocal为Key,需要存放的数据为Value,存放数据。
    4. 如果当前线程的ThreadLocalMap为空,创建ThreadLocalMap并存放数据。
    public void set(T value) {
        //(1)获取当前线程(调用者线程)
        Thread t = Thread.currentThread();
        //(2)以当前线程作为key值,去查找对应的线程变量,找到对应的map
        ThreadLocalMap map = getMap(t);
        //(3)如果map不为null,就直接添加本地变量,key为当前定义的ThreadLocal变量的this引用,值为添加的本地变量值
        if (map != null)
            map.set(this, value);
        //(4)如果map为null,说明首次添加,需要首先创建出对应的map
        else
            createMap(t, value);
    }
    

    get方法获取数据

    ThreadLocal方法的get可以获取当前线程ThreadLocalMap中存放的数据,获取存放数据的源码如下所示,get过程分为以下步骤:

    1. 获取当前线程
    2. 从当前线程中获取ThreadLocalMap变量。
    3. 如果ThreadLocalMap变量不为null,就可以在map中查找到本地变量的值。
    4. 如果ThreadLocalMap变量为null,那么就初始化当前线程的ThreadLocalMap。
    public T get() {
        //(1)获取当前线程
        Thread t = Thread.currentThread();
        //(2)获取当前线程的threadLocals变量
        ThreadLocalMap map = getMap(t);
        //(3)如果threadLocals变量不为null,就可以在map中查找到本地变量的值
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //(4)执行到此处,threadLocals为null,调用该更改初始化当前线程的threadLocals变量
        return setInitialValue();
    }
    
    private T setInitialValue() {
        //protected T initialValue() {return null;}
        T value = initialValue();
        //获取当前线程
        Thread t = Thread.currentThread();
        //以当前线程作为key值,去查找对应的线程变量,找到对应的map
        ThreadLocalMap map = getMap(t);
        //如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值
        if (map != null)
            map.set(this, value);
        //如果map为null,说明首次添加,需要首先创建出对应的map
        else
            createMap(t, value);
        return value;
    }
    

    ThreadLocal不支持继承性

    同一个ThreadLocal变量在父线程中被设置值后,在子线程中是获取不到的。(threadLocals中为当前调用线程对应的本地变量,所以二者自然是不能共享的)。

    package test;
    
    public class ThreadLocalTest2 {
    
        //(1)创建ThreadLocal变量
        public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    
        public static void main(String[] args) {
            //在main线程中添加main线程的本地变量
            threadLocal.set("mainVal");
            //新创建一个子线程
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("子线程中的本地变量值:"+threadLocal.get());
                }
            });
            thread.start();
            //输出main线程中的本地变量值
            System.out.println("mainx线程中的本地变量值:"+threadLocal.get());
        }
    }
    

    InheritableThreadLocal类

    在上面说到的ThreadLocal类是不能提供子线程访问父线程的本地变量的,而InheritableThreadLocal类则可以做到这个功能,下面是该类的源码,InheritableThreadLocal类继承了ThreadLocal类,并重写了childValue、getMap、createMap三个方法。我们接下来分别介绍一下三种方法的用处。

    1. createMap:当线程中不存在ThreadLocalMap变量,但是调用set或者get方法设置值的时候,需要初始化ThreadLocalMap变量时调用该方法。
    2. getMap:需要获取线程的ThreadLocalMap时调用该方法,这里返回的ThreadLocalMap始终为InheritableThreadLocalMap。
    3. childValue:在创建新线程的时候,如果父线程有ThreadLocalMap变量并且允许inherite ThreadLocalMap,那么程序会复制父线程的InheritableThreadLocal到子线程中,childValue表示在复制过程中如何根据父线程中得到数据生成线程中的数据。

    InteritableThreadLocal示例图

    
    public class InheritableThreadLocal<T> extends ThreadLocal<T> {
        /**
         * Creates an inheritable thread local variable.
         */
        public InheritableThreadLocal() {}
    
        /**
         * Computes the child's initial value for this inheritable thread-local
         * variable as a function of the parent's value at the time the child
         * thread is created.  This method is called from within the parent
         * thread before the child is started.
         * <p>
         * This method merely returns its input argument, and should be overridden
         * if a different behavior is desired.
         */
        protected T childValue(T parentValue) {
            return parentValue;
        }
    
        /**
         * Get the map associated with a ThreadLocal.
         */
        ThreadLocalMap getMap(Thread t) {
           return t.inheritableThreadLocals;
        }
    
        /**
         * Create the map associated with a ThreadLocal.
         */
        void createMap(Thread t, T firstValue) {
            t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
        }
    }
    
    

    总结:Thread会在构造函数中将父线程的inheritableThreadLocals成员变量的值赋值到新的ThreadLocalMap对象中。返回之后赋值给子线程的inheritableThreadLocals。InheritableThreadLocals类通过重写getMap和createMap两个方法将本地变量保存到了具体线程的inheritableThreadLocals变量中,当线程通过InheritableThreadLocals实例的set或者get方法设置变量的时候,就会创建当前线程的inheritableThreadLocals变量。而父线程创建子线程的时候,ThreadLocalMap中的构造函数会将父线程的inheritableThreadLocals中的变量复制一份到子线程的inheritableThreadLocals变量中。

    ThreadLocal内存泄漏

    通过前面的分析我们知道,ThreadLocal的线程数据是存放在ThreadLocalMap中的,所以如果ThreadLocal出现内存泄漏,那么肯定是ThreadLocalMap中存储的数据出现了泄露,我们需要看看ThreadLocalMap中的数据结构。ThreadLocalMap的数据结构如下所示,ThreadLocalMap中的数据存储在一个Entry数组中,Entry中有对ThreadLocal的WeakReference。

    什么情况下会出现内存泄露呢?

    1. 当一个线程调用ThreadLocal的set方法设置变量的时候,当前线程的ThreadLocalMap就会存放一个记录,这个记录的key值为ThreadLocal的弱引用,value就是通过set设置的值。
    2. 如果当前线程一直存在且没有调用该ThreadLocal的remove方法,如果这个时候别的地方还有对ThreadLocal的引用,那么当前线程中的ThreadLocalMap中会存在对ThreadLocal变量的引用和value对象的引用,是不会释放的,就会造成内存泄漏。
    3. 考虑这个ThreadLocal变量没有其他强依赖,如果当前线程还存在,由于线程的ThreadLocalMap里面的key是弱引用,所以当前线程的ThreadLocalMap里面的ThreadLocal变量的弱引用在gc的时候就被回收,但是对应的value还是存在的这就可能造成内存泄漏(因为这个时候ThreadLocalMap会存在key为null但是value不为null的entry项)。

    总结:THreadLocalMap中的Entry的key使用的是ThreadLocal对象的弱引用,在没有其他地方对ThreadLoca依赖,ThreadLocalMap中的ThreadLocal对象就会被回收掉,但是对应的不会被回收,这个时候Map中就可能存在key为null但是value不为null的项,这需要实际的时候使用完毕及时调用remove方法避免内存泄漏。

    Java中的四种引用类型

    上文中我们说到了WeakReference,Java中有四种引用类型:

    1. 强引用:Java中默认的引用类型,一个对象如果具有强引用那么只要这种引用还存在就不会被GC。
    2. 软引用:简言之,如果一个对象具有弱引用,在JVM发生OOM之前(即内存充足够使用),是不会GC这个对象的;只有到JVM内存不足的时候才会GC掉这个对象。软引用和一个引用队列联合使用,如果软引用所引用的对象被回收之后,该引用就会加入到与之关联的引用队列中。
    3. 弱引用(这里讨论ThreadLocalMap中的Entry类的重点):如果一个对象只具有弱引用,那么这个对象就会被垃圾回收器GC掉(被弱引用所引用的对象只能生存到下一次GC之前,当发生GC时候,无论当前内存是否足够,弱引用所引用的对象都会被回收掉)。弱引用也是和一个引用队列联合使用,如果弱引用的对象被垃圾回收期回收掉,JVM会将这个引用加入到与之关联的引用队列中。若引用的对象可以通过弱引用的get方法得到,当引用的对象呗回收掉之后,再调用get方法就会返回null;
    4. 虚引用:虚引用是所有引用中最弱的一种引用,其存在就是为了将关联虚引用的对象在被GC掉之后收到一个通知。(不能通过get方法获得其指向的对象)。
  • 相关阅读:
    sql-lib闯关秘籍之1-10关
    简单的SQL注入
    五分钟带你读懂 TCP全连接队列(图文并茂)
    Ambari HDP集群搭建全攻略
    Spring Cloud Security OAuth2.0 认证授权系列(入门篇)
    敲黑板:InnoDB的Double Write,你必须知道
    重要,知识点:InnoDB的插入缓冲
    你不知道的内存知识
    每日一个知识点:关于磁盘的一些事儿
    Spring Boot 系列:日志动态配置详解
  • 原文地址:https://www.cnblogs.com/johnvwan/p/15571477.html
Copyright © 2020-2023  润新知