• ThreadLocal源码分析


    一、概述

    ThreadLocal应用场景很广,很多主流框架都使用到它。例如,Spring用它来管理数据库连接,每个线程获取的都是自己的数据库连接对象。

    通常,我们使用ThreadLocal有两个目的:

    1.用来隔离不同线程的变量,避免线程间互相干扰。

    比如,我们系统每秒钟同时会有很多用户请求,每个请求都带有用户信息。通常我们都是一个线程处理一个用户请求,所以我们可以把用户信息放到Threadlocal里,让每个线程处理自己的用户信息,线程之间互不干扰。

    2.使用ThreadLocal来传递数据。

    比如,一个典型的用户请求需要经过拦截器-->controller-->service-->dao。如果想在这条请求链上传递数据,可以使用参数的方式一直传递下去,但这样做不太优雅。这时就可以使用ThreadLocal来存数据,由于一次请求只会对应一个线程,数据是与线程绑定的 。后续可以再通过ThreadLocal取出数据。

    下面是jdk文档对ThreadLocal的描述。

    ThreadLocal类提供了线程局部变量,这些变量不同于通常的变量,每个线程访问(通过get/set方法)的都是线程各自独有的变量副本。
    ThreadLocal实例通常都是类中私有的静态的(private static)成员变量。这些类希望将状态与线程关联(例如,用户id或事物id)。
    例如,下面的类将会为每个线程产生一个独有的标识符(id)。线程id在首次调用ThreadId.get()方法时分配,并且在随后的调用中保持不变。

    官网也提供的一个ThreadLocal的例子。

    当多个线程同时调用ThreadId.get()时,会轮流将共享的原子变量nextId加1后的值作为各个线程的线程id,例如线程A为0,则线程B为1,线程C初为2……。

       public class ThreadId {
           // Atomic integer containing the next thread ID to be assigned
           private static final AtomicInteger nextId = new AtomicInteger(0);
      
           // Thread local variable containing each thread's ID
           private static final ThreadLocal<Integer> threadId = new ThreadLocal<Integer>() {
               @Override protected Integer initialValue() {
                   return nextId.getAndIncrement();
               }
           };
      
           // Returns the current thread's unique ID, assigning it if necessary
           public static int get() {
               return threadId.get();
           }
       }

    二、源码分析

    ThreadLocal内部使用ThreadLocalMap来持有元素,它是ThreadLocal的核心,我们可以把ThreadLocalMap看做是一个定制化的HashMap。它的数据结构如下图所示。

    1.ThreadLocalMap

    我们知道HashMap是由数组加链表组成的,但ThreadLocalMap只使用到了数组,并且数组是首尾相连的环形结构,后面会解释原因。

    由源码可知,ThreadLocalMap的初始容量为16,负载因子为2/3。

        /**
         * 初始容量。必须是2的次方。
         */
        private static final int INITIAL_CAPACITY = 16;
    
        /**
         * table数组,必要时会扩容。数组长度必须是2的次方。
         */
        private Entry[] table;
    
        /**
         * 数组中元素个数
         */
        private int size = 0;
    
        /**
         * The next size value at which to resize.
         * 下次扩容时使用的容量值。
         */
        private int threshold; // Default to 0
    
        /**
         * Set the resize threshold to maintain at worst a 2/3 load factor.
         * 设置扩容的阈值,以维持负载因子至少为2/3.
         */
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }/**
         *  构造器。ThreadLocalMap使用的【懒初始化】,当有多个Entry要存放时,只会先创建一个Entry。
         */
        ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
            //初始化table数组
            table = new Entry[INITIAL_CAPACITY];
            //根据key的hash值定位其在数组中位置
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            //在数组中创建首个节点
            table[i] = new Entry(firstKey, firstValue);
            //设置初始化元素个数
            size = 1;
            //设置初始阈值
            setThreshold(INITIAL_CAPACITY);
        }

    ThreadLocalMap使用的懒初始化,当有多个Entry节点要存储时,也只能先通过构造器来先初始化一个Entry节点。而ThreadLocalMap初始化的时机是在ThreadLocal中首次调用set或get方法时。

    另外,节点存放的位置需要通过hash函数计算来定位。

    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); 

    说明一下,下面两种hash方式的结果都是一样的,做个测试就知道了。

    • x % n
    • x & (n-1)

    2.set操作

    ThreadLocal内部使用ThreadLocalMap来持有元素,它是ThreadLocal的核心,ThreadLocalMap可以看做是一个定制化的HashMap。查看Thread类的源码,可以发现Thread类有一个threaLocals属性,类型为ThreadLocalMap类型,所以可以理解为每个线程都会绑定一个ThreadLocalMap。

    public class Thread implements Runnable {
        ThreadLocal.ThreadLocalMap threadLocals = null; //本地变量 
    }

    存放数据时,我们会调用ThreadLocal.set(value)方法

        public void set(T value) {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null)
                map.set(this, value);
            else
                createMap(t, value);
        }
    
        ThreadLocalMap getMap(Thread t) {
            return t.threadLocals;
        }

    set方法会先获取当前线程,然后通过getMap获取当前线程绑定的ThreadLocalMap。

    • getMap结果非空,则进行保存
    • getMap结果为空,说明是第一次调用set方法,需要先实例化ThreadLocalMap。

    如果getMap方法返回为空,说明是第一次调用set方法,需要先实例化ThreadLocalMap,前面已经讲过,不再赘述。

        void createMap(Thread t, T firstValue) {
            t.threadLocals = new ThreadLocalMap(this, firstValue);
        }

    如果getMap方法返回非空,将<ThreadLocal实例,value>作为键值对保存到ThreadLocalMap中。

    保存的具体步骤是怎么样的呢,我们继续跟踪。set(value)-->set(key, value)

        private void set(ThreadLocal<?> key, Object value) {
    
            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.
    
            Entry[] tab = table;
            int len = tab.length;
            //通过hash定位
            int i = key.threadLocalHashCode & (len-1);
    
            //从当前节点开始,往后线性探测
            for (Entry e = tab[i];//当前节点
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {//下一个探测节点
                //探测节点的key
                ThreadLocal<?> k = e.get();
                
                //要存入的key与探测节点的key相等,直接覆盖
                if (k == key) {
                    e.value = value;
                    return;
                }
                //探测节点key为空,说明被回收了【弱引用原因】
                //说明可以使用该位置,用新k-v将其替换。
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            
            //执行到这里,说明探测到了空位置,可以插入
            tab[i] = new Entry(key, value);//创建节点插入
            int sz = ++size;
            
            //插入后需要检测一下容量
            //先尝试启发式清理,如果无法回收且容量又达到阈值,则需要扩容
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

    1)hash定位

    首先,使用hash算法来定位到数组的下标,使用的hash算法与上面讲解初始化时一样,不再赘述。

    通过hash算法得到的位置,并不一定就是最终value将要存放的位置。ThreadLocalMap同HashMap一样也存在hash冲突问题。

    2)使用线性探测,解决hash冲突

    我们知道HashMap是使用链地址法来解决hash冲突的,而ThreadLocalMap则是使用的另一种解决hash冲突的方法:开放地址法。所谓的开放定址法就是一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。

    开放地址法可以用公式表示为 ( hash(key) + di ) % m,这里di是个变量,表示每次移动的步数。开放地址法在进行探测时,di有下列几种取法:

    • 线性探测再散列:di=1,2,3,……m-1  。这种探测最简单。
    • 二次探测再散列:di=21,-21,22,-22,……k2  (k<2/m)  
    • 随机探测再散列:di取伪随机数列

    TheadLocalMap使用的是线性探测法,每次探测都是通过nextIndex()往后挪动一步。如果当前已经是最后一个节点,则探测第一个节点。这就说明了ThreadLocalMap中的数组是环形结构。但这里暂时还不能看出它是一个双向的,需要结合后面的prevIndex方法才能确定,后面再讲。

        /**
         * Increment i modulo len.
         */
        private static int nextIndex(int i, int len) {
            //下一个节点的位置。如果当前是最后一个节点,则下一个节点是第一个节点。
            return ((i + 1 < len) ? i + 1 : 0);
        }

    在探测的过程,对探测的节点进行判断

    • 如果探测节点的key与要存入的key相等,则直接覆盖。停止探测。
    • 如果探测节点的key为空,说明由于弱引用的原因被回收了。说明该位置可以重新利用,直接替换掉即可。停止探测。

    替换失效节点的方法:replaceStaleEntry()

            private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                           int staleSlot) {
                Entry[] tab = table;
                int len = tab.length;
                Entry e;
    
                // Back up to check for prior stale entry in current run.
                // We clean out whole runs at a time to avoid continual
                // incremental rehashing due to garbage collector freeing
                // up refs in bunches (i.e., whenever the collector runs).
                int slotToExpunge = staleSlot;
                for (int i = prevIndex(staleSlot, len);
                     (e = tab[i]) != null;
                     i = prevIndex(i, len))
                    if (e.get() == null)
                        slotToExpunge = i;
    
                // Find either the key or trailing null slot of run, whichever
                // occurs first
                for (int i = nextIndex(staleSlot, len);
                     (e = tab[i]) != null;
                     i = nextIndex(i, len)) {
                    ThreadLocal<?> k = e.get();
    
                    // If we find key, then we need to swap it
                    // with the stale entry to maintain hash table order.
                    // The newly stale slot, or any other stale slot
                    // encountered above it, can then be sent to expungeStaleEntry
                    // to remove or rehash all of the other entries in run.
                    if (k == key) {
                        e.value = value;
    
                        tab[i] = tab[staleSlot];
                        tab[staleSlot] = e;
    
                        // Start expunge at preceding stale entry if it exists
                        if (slotToExpunge == staleSlot)
                            slotToExpunge = i;
                        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                        return;
                    }
    
                    // If we didn't find stale entry on backward scan, the
                    // first stale entry seen while scanning for key is the
                    // first still present in the run.
                    if (k == null && slotToExpunge == staleSlot)
                        slotToExpunge = i;
                }
    
                // If key not found, put new entry in stale slot
                tab[staleSlot].value = null;
                tab[staleSlot] = new Entry(key, value);
    
                // If there are any other stale entries in run, expunge them
                if (slotToExpunge != staleSlot)
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            }

    3)启发式清理

    如果上面的两种方式无法找到插入点,就只能找空节点来插入了。一旦遇到了空节点,就停止探测,准备在此处插入。插入前先做一次启发式清理操作。

    原因就是,ThreadLocalMap同HashMap一样也是有负载因子的,当到达一定容量后就会进行rehash。rehash毕竟是一个耗性能的操作,应该尽量避免。所以,如果先通过启发式清理,能找到已经失效(key=null)的空间可以重复利用,这样就能避免rehash。

        private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do {
                i = nextIndex(i, len);
                Entry e = tab[i];
                if (e != null && e.get() == null) {
                    n = len;
                    removed = true;
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0);
            return removed;
        }
    
        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
    
            // expunge entry at staleSlot
            //清理失效的节点
            tab[staleSlot].value = null; //value设为null
            tab[staleSlot] = null;       //整个Entry设为null
            size--;
    
            // Rehash until we encounter null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {//k为null表示引用的对象已经被gc回收了
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    //hash不相等,说明这个元素之前发生过hash冲突(本应放在这却没放在这),
                    //现在因为有元素被移除了,很有可能原来冲突的位置空出来了,重试一次
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;
    
                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        //继续采用链地址法存放元素
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

    4)rehash

    如果启发式清理没能清理出过期空间(key==null),而容量又达到了阈值,就只能rehash了。

        private void rehash() {
            //全量清理
            expungeStaleEntries();
    
            // Use lower threshold for doubling to avoid hysteresis
            if (size >= threshold - threshold / 4)
                //扩容
                resize();
        }
        
    
        /**
         * 做一次全量清理失效节点的操作
         */
        private void expungeStaleEntries() {
            Entry[] tab = table;
            int len = tab.length;
            for (int j = 0; j < len; j++) {
                Entry e = tab[j];
                if (e != null && e.get() == null)
                    expungeStaleEntry(j);
            }
        }
        
    
        /**
         * Double the capacity of the table.
         */
        private void resize() {
            Entry[] oldTab = table;
            int oldLen = oldTab.length;
            int newLen = oldLen * 2;//扩容后的容量为原来的2倍。因为要为2的次幂。
            Entry[] newTab = new Entry[newLen];
            int count = 0;
    
            for (int j = 0; j < oldLen; ++j) {
                Entry e = oldTab[j];
                if (e != null) {
                    ThreadLocal<?> k = e.get();
                    if (k == null) {
                        e.value = null; // Help the GC
                    } else {
                        int h = k.threadLocalHashCode & (newLen - 1);
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                        newTab[h] = e;
                        count++;
                    }
                }
            }
    
            setThreshold(newLen);
            size = count;
            table = newTab;
        }

    3.get操作

    取数据时,我们会调用ThreadLocal.get()方法

        public T get() {
            // 获取当前线程
            Thread t = Thread.currentThread();
            // 获取当前线程绑定的ThreadLocalMap
            ThreadLocalMap map = getMap(t);
            if (map != null) {
                ThreadLocalMap.Entry e = map.getEntry(this);
                if (e != null)
                    return (T)e.value;
            }
            // map为空,则自动为其设置一个初始值并返回。
            return setInitialValue();
        }
    ThreadLocalMap getMap(Thread t) { return t.threadLocals; }

    同样会先获取执行该方法的当前线程,然后再获取该线程绑定的ThreadLocalMap,将当前ThreadLocal实例作为key来获取Map中保存的value值。

    get()-->getEntry()

        /**
         * Get the entry associated with key.
         * This method itself handles only the fast path: a direct hit of existing key.
         * It otherwise relays to getEntryAfterMiss.
         * This is designed to maximize performance for direct hits,in part by making this method readily inlinable.
         *
         * 根据key获取Entry节点。
         */
        private Entry getEntry(ThreadLocal key) {
            //根据key的hash值定位在数组中位置i
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            //找到目标
            if (e != null && e.get() == key)
                return e;
            else
                //找不到,则可能是因为碰撞而存到别处了。
                return getEntryAfterMiss(key, i, e);
        }
    
        /**
         * getEntry未直接命中的时候调用此方法
         */
        private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;
            // 向后不断进行线性探测
            while (e != null) {
                ThreadLocal k = e.get();
                //找到目标
                if (k == key)
                    return e;
                //key为null,清除失效的Entry
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                //探测下一个
                e = tab[i];
            }
            return null;
        }

    4.remove操作

         public void remove() {
             ThreadLocalMap m = getMap(Thread.currentThread());
             if (m != null)
                 m.remove(this);
         }
         
        /**
         * Remove the entry for key.
         */
        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)]) {
                if (e.get() == key) {
                    //直接将弱引用设为null,断开对对象的引用。
                    e.clear();
                    //清理无效节点
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

    三、弱引用与内存泄露

    ThreadLocalMap中Entry节点的定义如下。类上的注释信息已经解释的很清楚了,Entry类继承于WeakReference类,key是ThreadLocal类型。当key为null时,也就是entry.get() == null,意味着key将不再被引用,因此就可以将Entry从table数组中清除了。这样的Entry在后续的代码中被认为是过期失效的Entry。

       /**
         * Entry类继承于WeakReference,使用其主引用域(中括号中的类类型)作为key(通常总是一个ThreadLocal对象)。
         * 注意:key为null(也就是entry.get() == null)意味着key将不再被引用,因此可以将Entry从table数组中清除。
         * 这样的Entry在后续的代码中被认为是过期失效的Entry。
         */
        static class Entry extends WeakReference<ThreadLocal> {
            /**
             * The value associated with this ThreadLocal.
             */
            Object value;
    
            Entry(ThreadLocal k, Object v) {
                super(k);
                value = v;
            }
        }

    需要说明的是,这里的弱引用指的既不是Entry也不是value,而是key,也就是key对ThreadLocal实例存在弱引用

    当调用ThreadLocal.set(value)时,引用关系如下图所示。

    弱引用解决ThreadLocal实例内存泄露

    那么问题来了,为什么要将ThreadLocalMap的key设计为弱引用类型呢? 

    我们先看看下面的代码。

    public class ThreadLocalDemo {
    
        public static void main(String[] args) throws InterruptedException {
            print();
            ……
        }
    
        private static void print() {
            ThreadLocalWrapper wrapper = new ThreadLocalWrapper();
            wrapper.set("hello");
            System.out.println(wrapper.get());
        }//作用域结束,wrapper实例不再被使用会被gc,其所持有的local也会被gc。
    
    }
    
    class ThreadLocalWrapper {
        //1.使用ThreadLocal时通常都会在某个类中。
        private ThreadLocal<String> local = new ThreadLocal<>();
    
        public String get() {
            return local.get();
        }
    
        public void set(String str) {
            local.set(str);
        }
    }

    上面代码print方法结束后,wrapper作用域结束会被gc,其持有的ThreadLocal实例也就是local也理应会被gc。

      假设key不是弱引用,而是强引用,由于强引用的存在,local是不会被gc的。只有等待线程结束,Thread-->ThreadLocalMap-->Entry这条强引用链消失,entry不可达被gc,最终Threadlocal对象也会不可达才会被gc。如果是这样的话,岂不是ThreadLocal对象的回收要看线程的执行时间了,如果线程生命周期较长,比如线程池中的线程,ThreadLocal对象就发生内存泄露了。

      ThreadLocal的设计者显然要考虑这个问题。线程的执行时间是由开发者的业务决定的,对于threadlocal引用出了作用域范围或者threadlocal=null后ThreadLocal对象的回收这个问题,肯定要在设计层面就解决掉,而不能依赖业务线程的终止,所以设计者就将key设计为弱引用类型。弱引用能保证Threadlocal对象一定活不过下次gc,一定会被回收掉。所以说,将ThreadLocalMap的key设计为弱引用,能在一定程度上防止内存泄露,这里的泄露指的是ThreadLocal对象的泄露。

    remove()解决value内存泄露

    那上面为什么说是一定程度上防止内存泄露,而不是说最终保证不会发生内存泄露?

      别忘了还有value对象。由于ThreadLocalMap和线程的生命周期是一致的,当线程资源长期不释放,即使ThreadLocal本身由于弱引用机制已经被回收掉了,但还存在Thread-->ThreadLocalMap-->Entry-->value这样的强引用链,value在留存在线程的ThreadLocalMap的Entry中。即存在key为null而value却有值的无效Entry,导致内存泄漏。(由于value只能通过ThreadLocal的set/get/remove方法来访问,当ThreadLocal对象因为弱引用的原因被回收后,value自然也就无法再被访问到,成了无用资源了。)  

      所以,ThreadLocal采取了一定的措施来尽量避免内存泄露的发生。每次调用ThreadLocal的get/set/remove方法时,都会触发执行expungeStaleEntry方法,对失效(key为null)的Entry的做清理工作擦除Entry(置为null),同时检测整个Entry数组将key为null的Entry一并擦除,然后重新调整索引。但是,只有在调用这三个方法才会触发清理,而实际上很可能我们在使用完ThreadLocal之后就不再做任何操作了,这样就不会触发ThreadLocal的清理工作。所以,当我们使用完ThreadLocal后,尽量手动调用一下remove方法,尽早地将value清理掉

    三、继承性

    InheritableThreadLocal

      在一些场景中,子线程需要可以获取父线程的本地变量,比如子线程要获取父线程中保存的用户的信息,或者使用一个统一的traceId来进行链路追踪。但是ThreadLocal不支持继承性,即子线程无法从父线程中获取父线程的本地变量。原因很简单,因为操作ThreadLocal时每次都是获取的当前线程。因此,JDK提供了InheritableThreadLocal来解决继承性问题

      InheritableThreadLocal 继承了ThreadLocal,并且重写了createMap等三个方法。当首次调用set方法时,创建的是当前线程的inheritableThreadLocals变量,而不再是threadLocals。同样在调用get方法时,获取当前线程的内部map时,获取的是inheritableThreadLocals而不再是threadLocals。总的来说,在InheritableThreadLocal中,变量inheritableThreadLocals替代了threadLocals。

      当父线程创建子线程时,构造函数会将父线程中的inheritableThreadLocals变量里的本地变量赋值到子线程的inheritableThreadLocals里面。

    // new Thread()-->init()
    
    init(){
        ……
        //如果父线程inheritableThreadLocals变量不为null
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            //设置子线程中的inheritableThreadLocals变量
            this.inheritableThreadLocals =ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        ……
    }
    
    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        return new ThreadLocalMap(parentMap);
    }

    TransmittableThreadLocal(淘宝开源)

      但是InheritableThreadLocal提供的方案并不彻底。因为对于使用线程池等会池化复用线程的情况,线程由线程池创建好,并且线程是池化起来反复使用的;这时父子线程关系的ThreadLocal值传递已经没有意义,应用需要的实际上是把任务提交给线程池时的ThreadLocal值传递到任务执行时。

      淘宝技术部哲良在github上开源了一个TransmittableThreadLocal,完美解决了线程变量继承问题,github地址为:https://github.com/alibaba/transmittable-thread-local。 

    总结

    1.ThreadLocal的作用?

    主要解决线程变量隔离问题。在实际使用中,我们有两种典型用法

    ①用来隔离线程变量。比如在数据库连接池中,可以将数据库连接Connection对象放在ThreadLocal中,每个线程获取各自的数据库连接,线程间不会互相干扰。

    ②用来在同一个线程的调用链中传递数据。

    2.ThreadLocal的工作原理?(如何实现线程变量隔离的?)hash冲突解决方法?

    工作原理略。每个线程都持有了一个线程本地变量,每个线程只操作自己的线程本地变量,线程间避免了相互干扰。

    ThreadLocal使用开发地址法中的线性探测来解决hash冲突。 

    3.ThreadLocalMap为什么要定义在ThreadLocal中,而不是Thread中?

    Thread类有个ThreadlocalMap类型的成员变量,但ThreadlocalMap的定义却在Threadlocal 中。

    将ThreadLocalMap定义在Thread中似乎看起来更符合逻辑,但是实际上并不需要在Thread中操作ThreadLocalMap,定义在Thread类中只会增加一些不必要的开销。

    定义在ThreadLocal类中的原因是ThreadLocal类负责ThreadLocalMap的创建,仅当线程中设置第一个ThreadLocal时,才为当前线程创建ThreadLocalMap,之后所有其他ThreadLocal变量将使用一个ThreadLocalMap。

    总的来说就是,ThreadLocalMap不是必需品,定义在Thread中增加了成本,定义在ThreadLocal中按需创建。

    4.既然是线程局部变量,那为什么不用线程(Thread)对象作为key,获取变量时通过线程作为key来获取,这样不是更清晰?

    这样设计就存在数据覆盖的问题。如果用线程对象作为key,假设已经存入了用户信息,存入<线程,用户信息>。这时需要新增加用户地理位置信息,需要存入<线程,用户地理信息>。由于key都是同一个线程,不就覆盖了嘛。

    5.那使用ThreadLocal新增信息应该怎么做呢?

    为当前线程再绑定一个Threadlocal对象不就好了。比如已经存入了用户信息,要新增加用户的地理信息,直接Threadlocal<Geo> geo = new Threadlocal<> (); geo.set(地理信息);

    这样线程的ThreadlocalMap里面就会有二个元素,一个是用户信息,一个是地理位置。(一个线程绑定一个ThreadLocalMap,一个ThreadLocalMap存多个ThreadLocal实例。)

    6.如果有多个变量都要塞到ThreadlocalMap中,那岂不是要申明多个Threadlocal 对象?有没有好的解决办法。

    可以再封装一下,把这些变量打包成一个Map不就好了,整个Map作为value存入,这样就只需要一个Threadlocal 对象。

    7.为什么ThreadLocalMap中key被设计成弱引用类型?

    key设计为弱引用是为了尽最大努力避免内存泄漏,解决的是ThreadLocal对象的内存泄露问题。

    ThreadLocal的设计者考虑到了某些线程的生命周期较长,比如线程池中的线程。由于存在Thread -> ThreadLocalMap -> Entry这样一条强引用链,如果key不设计成弱引用类型,是强引用的话,key就一直不会被GC回收,一直不会是null,Entry就不会被清理。

    (ThreadLocalMap根据key是否为null来判断是否清理Entry。因为key为null时,引用的ThreadLocal实例不可达会被回收。value又只能通过ThreadLocal的方法来访问,此时相当于value也没用处了。所以,可以根据key是否为null来判断是否清理Entry。)

    8.ThreadLocal内存泄露的原因?要如何避免?

    弱引用解决的是ThreadLocal对象的内存泄露问题,但value还存在内存泄露的风险。

    内存泄露的原因:

    由于ThreadLocalMap和线程的生命周期是一致的,当线程资源长期不释放,即使ThreadLocal本身由于弱引用机制已经被回收掉了,但value还是驻留在线程的ThreadLocalMap的Entry中。即存在key为null,但value却有值的无效Entry,导致内存泄漏。

    ThreadLocal自身采取的措施:

    但实际上,ThreadLocal内部已经为我们做了一定的防止内存泄漏的工作。ThreadLocalMap提供了一个expungeStaleEntry方法,该方法在每次调用ThreadLocal的get、set、remove方法时都会执行,即ThreadLocal内部已经帮我们做了对key为null的Entry的清理工作:擦除Entry(置为null),同时检测整个Entry数组将key为null的Entry一并擦除,然后重新调整索引。

    但是必须需要调用这三个方法才会触发清理,很可能我们使用完之后就不再做任何操作了(set/get/remove),这样就不会触发内部的清理工作。

    开发人员需要注意:

    所以,通常建议每次使用完ThreadLocal后,立即调用remove方法。

    9.为什么使用ThreadLocal时通常定义为static?

    ThreadLocal 对象建议使用 static 修饰。这个变量是针对一个线程内所有操作共享的,所以设置为静态变量,所有此类实例共享 此静态变量 ,也就是说在类第一次被使用时装载,只分配一块存储空间,所有此类的对象(只 要是这个线程内定义的)都可以操控这个变量。

    10.ThreadLocal继承性问题?如何解决?

    ThreadLocal不支持子线程继承,可以使用JDK中的InheritableThreadLocal来解决继承性问题。对于线程池等场景,可以使用淘宝技术部哲良实现的TransmittableThreadLocal

    参考:

    ThreadLocal源码解读

    一个ThreadLocal和面试官大战30个回合

    不积跬步,无以至千里。不积小流,无以成江海!
  • 相关阅读:
    Native Boot 从一个 VHD 引导系统的相关说明
    bind()函数的深入理解及两种兼容方法分析
    四、CentOS 6.5 上传和安装Nginx
    jQuery 常见操作实现方式
    “贷券” 信贷系统
    注册 Ironic 裸金属节点并部署裸金属实例
    hover()方法
    Uncaught SyntaxError: Inline Babel script: Unexpected token
    Uncaught Error: The `style` prop expects a mapping from style properties to values, not a string
    jquery bind事件
  • 原文地址:https://www.cnblogs.com/rouqinglangzi/p/9106301.html
Copyright © 2020-2023  润新知