ThreadLocal源码的一些理解

ThreadLocal在工作中经常使用,尤其在Web的请求周期中,常见的使用场景如在SpringMvc中,从登录拦截器中preHandler中set一个全局的变量,在postHanlder时remove,这里很多使用容易忽略remove,因为大多数Web服务器是使用线程池工作的,如果在一个request中不remove操作,而在第二次请求中又因为某些业务没有在拦截器中做set操作,则而第二次从ThreadLocal中获取的值变是一个失效值,这点是比较容易忽视的一个问题。

另外如果是在一个request中新建一个新线程,在子线程中同样是拿不到这个ThreadLocal的,这时使用InheritableThreadLocal便可以解决这个。

本文不对ThreadLocal的使用场景做过多介绍,下面只对源码进行一个分析,如果有不对之处还望指正。

1、get方法没什么多介绍的,从当前线程的map中获取Entry,如果存在便返回,否则设置初始值。setInitialValue默认是null,并在其中做初始化map的工作。

  public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T)e.value;
        }
        return setInitialValue();
    }

2、set方法与remove方法也很简单,这里也不做过多介绍,接下来重点需要解释的是ThreadLocalMap以及Entry这两个内部类,因为ThreadLocal的get和set的功能都是基于这两个类完成的,先看ThreadLocalMap。


ThreadLocal内部初始化了16个Entry数组的table,并设置阈值达到在2/3的时候做自动扩容,而table的长度必须为2的幂,这些在构造函数里面,其中Entry的位置是通过threadLocalHashCode与15做与操作得到的,这里的threadLocalHashCode是ThreadLocal的一个从0开始自增的值。

threadLocalHashCode的值是从0开始每次自增1100001110010001000011001000111,举个例子,当初始长度为16时,threadLocalHashCode的值会是

7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0 7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0 7 14 5 12 3 10 1 8的循环。

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);
}

第二个构造方法的作用是复制一个ThreadLocalMap,这个方法在Thread类里面调用,当new一个Thread的时候,会将父线程的inheritableThreadLocals这个ThreadLocalMap复制到当前线程的inheritableThreadLocals中,这样形成一个链表,即使这个子线程再new一个线程,子子线程同样可以获取到所以父线程的inheritableThreadLocals,传递性得以保持。

if (parent.inheritableThreadLocals != null)//Thread初始化时的操作
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    ThreadLocal key = e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);//注意着,因为复制时需要一次新的寻址,并不是单纯的按位复制。
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }
获取Entry的方法同样是先用hash获取位置,如果发现当前Entry为空或者发现Entry中的key与当前的ThreadLocal不是同一个,则继续寻找,在getEntryAfterMiss中继续寻找,如果找了一圈仍然没有找到,则返回null,这里需要注意,在一个固定长度的table中,例如16,table的顺序永远是按照7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0这样的顺序set进去的。在查询过程中,如果某一个entry的key为null,则说明这个key已经被回收了,则需要对该Entry进行回收清理,否则该value得不到清理就会有内存泄漏。
 
 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;
                if (k == null)
                    expungeStaleEntry(i);//清理当前Entry
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            tab[staleSlot].value = null;//显示置为null,以便gc可以回收
            tab[staleSlot] = null;//
            size--;

            // Rehash until we encounter null清理完了之后再继续循环一遍,并重新将entry hash一遍。
            Entry e;
            int i;
            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;
        }

ThreadLocal.set的时候调用的是也是ThreadLocalMap的set方法,这个方法先找到位置i,再发table数组中找当前的Entry,如果Entry的key与当前ThreadLocal一致,那是最好了,直接将Entry的value替换,如果为(k=null),则说明这个key被回收了,则重置便好了,如果最后循环一遍直到Entry为null,说明这是一个新的ThreadLocal,则new一个新的Entry,并对table重新清理。
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);

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);//这里是因为key为null,但是当前entry没有清理,便可以复用。
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)//如果是新增了entry,需要重新做一道清理,如果大于阈值还需要重新分配。
                rehash();
        }

其实ThreadLocal类代码不多,核心思想我认为有以下三点:
1、理解Entry是一个WeakReference,否则的话在线程池场景中,一直复用线程,即使ThreadLocal的作用域已经不再,但是因为在ThreadLocalMap中被引用,这样就会造成内存泄漏。
2、另外一个就是起清理弱引用的value的expungStaleEntry方法,这个方法在WeakHashMap中也有,思路类似,可以做个对比区分。
3、threadLocalHashCode的使用很巧,在多线程场景中是解决hash冲突的另外一种可以借鉴的方式,将多个ThreadLocal分散到不同的位置,在hash冲突时可减少重新寻找的次数。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值