ThreadLocal讲解(包含使用不当导致的内存泄漏问题)

  • Post author:
  • Post category:其他

ThreadLocal对于每个线程都创建一个ThreadLocalMap副本,相当于是以空间换取时间实现的线程安全策略,而synchronized(加锁)相当于以时间换取空间实现线程安全。
ThreadLocal.set();
源码

    public void set(T value) {
    //获取当前线程
        Thread t = Thread.currentThread();
        //根据thread获取ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null)
        	//map已经生成的话,则替换值。
            map.set(this, value);
        else
        	//map未生成,则创建map并放入value值。
            createMap(t, value);
    }

从set方法我们可以看出来,实际上先根据线程来获取到ThreadLocalMap,然后再根据ThreadLocal来获取或者设置具体的value值置于ThreadLocalMap中。
createMap方法

    void createMap(Thread t, T firstValue) {
    //实际上threadlocalMap是Thread类中的threadLocals字段属性。我们后续可以直接通过反射获取ThreadLocalMap。
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

new ThreadLocalMap(this, firstValue)方法

        private Entry[] table;
        private static final int INITIAL_CAPACITY = 16;
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            //同HashMap获取数组下标相同。
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            //Entry数组
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            //设置扩容阈值
            setThreshold(INITIAL_CAPACITY);
        }
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }
		//Entry对象
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

结合以上可知,ThreadLocal实际上将值存放在ThreadLocalMap中,而ThreadLocalMap实际上是将值存放在Entry数组中,而Entry对象实际上是WeakReference弱引用的子类,而Entry实际上同ThreadLocal关联起来,并且将值存放在其中的value字段属性中。
ThreadLocal.get()

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
        //根据ThreadLocal获取Entry
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                //返回Entry中存放的value
                T result = (T)e.value;
                return result;
            }
        }
        //实际上是返回null,方法会判断ThreadLocalMap是否生成,未生成则初始化ThreadLocalMap,并注入null值。
        return setInitialValue();
    }

ThreadLocalMap.getEntry()

        private Entry getEntry(ThreadLocal<?> key) {
        //获取位于对应数组的下标
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            //如果ThreadLocal相同,则返回该Entry。
            if (e != null && e.get() == key)
                return e;
            else
            	//同HashMap中可能产生相同的数组下标对应,如果ThreadLocalMap不相同,则以该下标为起点从前往后找(直到为null为止)。
                return getEntryAfterMiss(key, i, e);
        }

Entry.get实际上是Reference.get,Reference是个抽象类,由WeakReference类(弱引用)实现。

    public T get() {
    	//即referent存放的就是ThreadLocal的对象地址
        return this.referent;
    }

即get方法是获取弱引用的ThreadLocal,判断是否相同,相同则取Entry中的value值返回。
总结get和set方法,即ThreadLocal实际上是将值存放在ThreadLocalMap中,而ThreadLocalMap实际上是Thread类的字段,而ThreadLocalMap将值存放在Entry数组中,而Entry数组又是以ThreadLocal关联上value,这也是为何Threadlocal是与Thread相关联的了。

下面就可以解释下为何ThreadLocal推荐设置成static,因为它本身就是线程安全的类,而且Entry的value值的获取与ThreadLocal有关。因为目前Spring框架默认的Bean是单例,因此ThreadLocal不设置成static可能没问题,但是一旦设置成原型,那么就会发生很多ThreadLocal的创建与回收,这是可以避免的。而千万要注意的是:不要在局部方法中创建使用ThreadLocal,举例如下:

public class Test {


    public static void main(String[] args) throws Exception {
        Test test = new Test();
        test.loop();
        //建议jvm进行gc操作
        System.gc();
        System.out.println("--触发GC操作--");
        test.printEntryInfo();

    }

    public void loop() throws Exception {
            ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
            threadLocal.set(new Integer(555));
            printEntryInfo();

    }

    private void printEntryInfo() throws Exception {
        Thread currentThread = Thread.currentThread();
        Class<? extends Thread> clz = currentThread.getClass();
        //上面讲述源码时介绍过,实际上ThreadLocal将值存放在Thread的threadLocals字段中。
        Field field = clz.getDeclaredField("threadLocals");
        field.setAccessible(true);
        Object threadLocalMap = field.get(currentThread);
        Class<?> tlmClass = threadLocalMap.getClass();
        Field tableField = tlmClass.getDeclaredField("table");
        tableField.setAccessible(true);
        //即获取Entry[]数组。
        Object[] arr = (Object[]) tableField.get(threadLocalMap);
        for (Object o : arr) {
            if (o != null) {
                Class<?> entryClass = o.getClass();
                //获取Entry中的value
                Field valueField = entryClass.getDeclaredField("value");
                //获取referent,实际上referent存放ThreadLocal对象地址。
                Field referenceField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent");
                valueField.setAccessible(true);
                referenceField.setAccessible(true);
                System.out.println(String.format("key:%s,值:%s", referenceField.get(o), valueField.get(o)));
            }
        }

    }
}

打印如下
在这里插入图片描述

可以看出,如果在局部方法中创建并使用ThreadLocal,一旦局部方法的生命周期结束后,那么在此阶段如果触发了GC操作,那么ThreadLocal对象会被回收(因为Entry中的ThreadLocal为弱引用),即Entry的referent会为null,但是由于value被Entry的value引用,而ThreadLocalMap被Thread引用,那么会导致该value一直无法被回收,直到Thread即线程结束为止。如果框架中的线程是采用线程池技术的话,那么可能会导致不同会话实际是复用一个线程,即第二个会话可能会获取到第一个会话的信息(这可能是不正确的,如果使用ThreadLocal存放用户信息的话),因此推荐ThreadLocal在使用完成后,都调用ThreadLocal.remove方法,并且将ThreadLocal设置成static。
ThreadLocal.remove()方法

     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
         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) {
 		         //将Entry中的referent置空。
                    e.clear();
                    //将Entry的value置空,并且将引用该Entry的地方置空。
                    expungeStaleEntry(i);
                    return;
                }
            }
        }
       private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            //主要逻辑在这两行代码                    
            //将Entry的value置空,并且将引用该Entry的地方置空。
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            //调整Entry数组
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    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;
        }    
    public void clear() {
 		//将Entry中的referent置空。
        this.referent = null;
    }

版权声明:本文为qq_43527397原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。