• ThreadLocal 从源码角度简单分析


    ThreadLcoal源码浅析

    我们知道ThreadLocal用于维护多个线程线程独立的变量副本,这些变量只在线程内共享,可跨方法、类等,如下是一个维护多个线程Integer变量的ThreadLocal:

    ThreadLocal<Integer> threadLocalNum = new ThreadLocal<>();
    

    每个使用threadLocalNum的线程,可以通过形如threadLocalNum.set(1)的方式创建了一个独立使用的Integer变量副本,那么它是怎么实现的呢?我们今天就来简单的分析一下。

    先看下ThreadLocal的set方法是如何实现的,源码如下:

    public void set(T value) {
            Thread t = Thread.currentThread();  //获取当前线程
            ThreadLocalMap map = getMap(t);     //获取当前线程的ThreadLocalMap
            if (map != null)
                map.set(this, value);           //当前线程的ThreadLocalMap不为空则直接设值
            else
                createMap(t, value);            //当前线程的ThreadLocalMap为空则创建一个来设置值
        }
    

    是的,你没有看错,是获取当前线程中的ThreadLocalMap来设置的值,我们来看一下getMap(t)是如何实现的:

    ThreadLocalMap getMap(Thread t) {
            return t.threadLocals;
        }
    

    然后我们看到Thread中包含了一个ThreadLocalMap类型的属性:

    ThreadLocal.ThreadLocalMap threadLocals = null;
    

    到这里我们可以得出一个结论:各个线程持有了一个ThreadLocalMap的属性,通过ThreadLocal设置变量时,直接设置到了对应线程的的ThreadLocalMap属性中

    那么不同的线程中通过ThreadLocal设置的值是如何关联定义的ThreadLocal变量和Thread中的ThreadLocalMap的呢?我们接着分析。

    前面写到当前线程的ThreadLocalMap为空则创建一个ThreadLocalMap来设值,我们来看下createMap(t, value)的具体实现:

    void createMap(Thread t, T firstValue) {
            t.threadLocals = new ThreadLocalMap(this, firstValue);
        }
    
    ///////////////////
    //ThreadLocalMap构造器定义如下
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
                table = new Entry[INITIAL_CAPACITY];
                int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);  //
                table[i] = new Entry(firstKey, firstValue);
                size = 1;
                setThreshold(INITIAL_CAPACITY);
            }
    private static final int INITIAL_CAPACITY = 16;
    
    

    线程中threadLocals是一个ThreadLocalMap变量,其默认值是null,该线程在首次使用threadLocal对象调用set的时候通过createMap(Thread t, T firstValue)实例化。

    先来看一下ThreadLocalMap,它是在ThreadLocal中定义的一个静态内部类,其内属性如下:

    		/**
             * The initial capacity -- MUST be a power of two.
             */
            private static final int INITIAL_CAPACITY = 16;
    
            /**
             * The table, resized as necessary.
             * table.length MUST always be a power of two.
             */
            private Entry[] table;
    
            /**
             * The number of entries in the table.
             */
            private int size = 0;
    
            /**
             * The next size value at which to resize.
             */
            private int threshold; // Default to 0
    

    其中属性private Entry[] table,用于存储通过threadLocal set 进来的变量,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继承了WeakReference<ThreadLocal<?>>,ThreadLocal在构造器中被指定为弱引用super(k)(后面会单独讨论为何这里使用弱引用)。

    至此,我们可以知道ThreadLocal和Thead的内存结构如下:

    ThreadLocal的垃圾回收

    网上看到很多文章都在讲ThreadLocal的内存泄露问题,所以也在这里简单说一下自己的理解。

    从上面的结构可以看出ThreadLocal涉及到的要回收的对象包括:

    • ThreadLocal实例本身
    • 各线程中的threadLocalMap,其中包括各个Entry的 key, value

    下面先简述java的引用,然后分别讨论ThreadLocal本身的回收和threadLcoalMap的回收

    Java引用

    • 强引用(StrongReference):对象可达就不会被gc回收,空间不足时报error
    • 软引用(SoftReference):对象无其他强引用,当空间不足时才会被gc回收。
    • 弱引用(WeakReference):对象无其他强引用,gc过程扫描到就会被回收。

    ThreadLocal的回收

    ThreadLocal实例的引用主要包括两种:

    • ThreadLocal定义处的强引用
    • 各线程中ThreadLocalMap里的key=weak(threadLocal), 是弱引用

    强引用还在的情况下ThreadLocal一定不会被回收;无强引用后,由于各个Thread中Entry的key是弱引用,会在下次GC后变为null。ThreadLocal实例什么时候被回收完全取决于强引用何时被干掉,那么什么时候强引用会被销毁呢?最简单的就是 threadLocal=null强引用被赋值为null;其它也可是threadLocal是一个局部变量,在方法退出后引用被销毁,等等。

    这里来回答一下前面提到的为什么ThreadLocalMap中将key设计为弱引用,我们假设如果ThreadLocalMap中是强引用会出现什么情况?定义ThreadLocal时定义的强引用被置为null的时候,如果还有其它使用了该ThreadLocal的线程没有完成,还需要很久会执行完成,那么这个线程将一直持有该ThreadLocal实例的引用,直到线程完成,期间ThreadLocal实例都不能被回收,最重要的是如果不了解ThreadLocal内部实现,你可能都不知道还有其他线程引用了threadLocal实例。

    线程结束时清除ThreadLocalMap的代码Thread.exit()如下:

       /**
         * This method is called by the system to give a Thread
         * a chance to clean up before it actually exits.
         */
        private void exit() {
            if (group != null) {
                group.threadTerminated(this);
                group = null;
            }
            /* Aggressively null out all reference fields: see bug 4006245 */
            target = null;
            /* Speed the release of some of these resources */
            threadLocals = null;
            inheritableThreadLocals = null;
            inheritedAccessControlContext = null;
            blocker = null;
            uncaughtExceptionHandler = null;
        }
    

    所以,对于threadLocal对象本身而言, 只要通过threadLocal=null就可以实现回收了。

    各线程中threadLocalMap的回收

    单从引用的角度来看,各线程中的threadLocalMap,其中包括各个Entry的key 和 value。线程(也就是Thread实例)本身一直持有threadLocalMap的强引用,只有在线程结束的时候才会被回收。而key是threadLocal对象的弱引用,当threadLocal被置为null时就会被回收,此时的Entry数组中就会出现很多key为null,但是value有值的元素,那么value在threadLocal对象为空后应该怎么回收呢?

    ThreadLocal在实现的时候提供了一些方法:set/get/remove,可以在执行它们的时候调用ThreadLocalMap的方法回收ThreadLocalMap中已经失效(key=null)的entry实例。

    这里就以set为例看看ThreadLocal是如何回收entry的,ThreadLocal set方法实现如下:

    //ThreadLocal
    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
    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;
                int i = key.threadLocalHashCode & (len-1);  //获取当前threadLocal实例的hashcode,同时也是table的下标
    
        		//这里for循环找key,是因为hash冲突会使hashcode指向的下标不是真实的存储位置
                for (Entry e = tab[i];
                     e != null;
                     e = tab[i = nextIndex(i, len)]) { 
                    ThreadLocal<?> k = e.get();
    				//找到了设置为新值
                    if (k == key) {
                        e.value = value;
                        return;
                    }
    				//entry不为null,key为null
                    //说明原来被赋值过,但是原threadLocal已经被回收
                    if (k == null) {
                        replaceStaleEntry(key, value, i);
                        return;
                    }
                }
    			//如果下标对应的entry为null, 则新建一个entry
                tab[i] = new Entry(key, value);
                int sz = ++size;
       			//清理threadlocal中其它被回收了的entry(也就是key=null的entry)
                if (!cleanSomeSlots(i, sz) && sz >= threshold)
                    //rehash
                    rehash();
            }
    

    看一下cleanSomeSlots的实现:

    //ThreadLocalMap
    private boolean cleanSomeSlots(int i, int n) {
                boolean removed = false;
                Entry[] tab = table;
                int len = tab.length;
                do {
                    //获取下一个entry的下标
                    i = nextIndex(i, len);
                    Entry e = tab[i];
                    //entry不为null,key为null
                    //说明原来被赋值过,但是原threadLocal已经被回收
                    if (e != null && e.get() == null) {
                        n = len;
                        removed = true;
                        // 删除已经无效的entry
                        i = expungeStaleEntry(i);
                    }
                } while ( (n >>>= 1) != 0);
                return removed;
            }
    
    
    
    private int expungeStaleEntry(int staleSlot) {
                Entry[] tab = table;
                int len = tab.length;
    
                // 回收无效entry
                tab[staleSlot].value = null;
                tab[staleSlot] = 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();
                    //entry不为null,key为null,应该回收
                    if (k == null) {
                        e.value = null;
                        tab[i] = null;
                        size--;
                    } else {
                        //rehash的实现
                        //计算当前entry的k的hashcode,看是下标是否应该为i
                        //如果不为i说明,是之前hash冲突放到这儿的,现在需要reash
                        int h = k.threadLocalHashCode & (len - 1);
                        //h!=i 说明hash冲突了, entry不应该放在下标为i的位置
                        if (h != i) {
                            tab[i] = null;
                            // Unlike Knuth 6.4 Algorithm R, we must scan until
                            // null because multiple entries could have been stale.
                            //找正确的位置h,但是还是有可能冲突所以要循环
                            while (tab[h] != null)
                                h = nextIndex(h, len);
                            tab[h] = e;
                        }
                    }
                }
                return i;
            }
    

    从上面的分析我们可以看到把ThreadLocalMap中的key设计为weakReference,也使set方法可以通过key==null && entry != null判断entry是否失效

    总结一下ThreadLocal set方法的实现:

    • 根据threadLocal计算hashcode找到entry[]数组对应位置设置值
    • 遍历数组找到其它失效的(entry不为null,key为null)的entry删除

    内存泄露问题

    ThreadLocal通过巧妙的设计最大程度上减少了内存泄露的可能,但是并没有完全消除。

    当我们使用完ThreadLocal后没有调用set/get/remove方法,那么可能会导致失效内存不能及时被回收,导致内存泄露,尤其是在value占用内存较大的情况。

    所以最佳实践是,在明确ThreadLocal不再使用时,手动调用remove方法及时清空。

    总结

    • ThreadLocal 并不解决线程间共享数据的问题
    • ThreadLocal是通过让线程内的ThreadLocalMap.Entry的key指向自身,来实现了对线程内对象的引用,从而可以在线程内方便的使用变量。同时因为操作的都是线程内的变量,也避免了实例线程安全的问题
    • ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景
    • ThreadLocalMap 的 Entry 对 ThreadLocal 的引用为弱引用,避免了 ThreadLocal 对象无法被回收的问题
    • ThreadLocalMap 的 set 方法通过调用 cleanSomeSlots 方法回收键为 null 的 Entry 对象的值(即失效实例)从而防止内存泄漏(其它的remove,get类似)
    • 在明确ThreadLocal不再使用时,手动调用remove方法及时清空

    参考

    正确理解Thread Local的原理与适用场景

  • 相关阅读:
    py4 程序流程控制
    [Selenium+Java] Handling AJAX Call in Selenium Webdriver
    [Selenium+Java] SSL Certificate Error Handling in Selenium
    [Selenium+Java] Desired Capabilities in Selenium
    [Selenium+Java] How to use AutoIT with Selenium
    [Selenium+Java] https://www.guru99.com/using-robot-api-selenium.html
    [Selenium+Java] Selenium with HTMLUnit Driver & PhantomJS
    [Selenium+Java] Log4j with Selenium Tutorial
    [Selenium+Java] Parallel Execution & Session Handling in Selenium
    [Selenium+Java] How to Take Screenshot in Selenium WebDriver
  • 原文地址:https://www.cnblogs.com/chrischennx/p/9557285.html
Copyright © 2020-2023  润新知