美文网首页
ThreadLocal笔记

ThreadLocal笔记

作者: 8989cc121281 | 来源:发表于2018-06-05 15:30 被阅读23次

    定义:
    创建线程局部变量的类

    特点:
    一般情况下,创建的变量可以被任何一个线程访问并且修改,但是使用ThreadLocal创建的变量只能被当前线程访问,其他线程没有办法修改。

    实现原理:

    set方法

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

    首先获取当前线程引用,currentcurrentThread()为本地方法,然后根据当前工作线程的引用获取ThreadLocalMap对象,getMap方法比较简单,返回Thread类的成员变量threadLocals,即ThreadLocalMap对象。

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

    返回到set方法继续看,如果获取到的ThreadLocalMap对象为空的话则通过createMap方法创建ThreadLocalMap对象,通过直接对threadLocals实例化来创建,代码如下:

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

    可以发现ThreadLocalMap对象传入的key值为this,即代表当前ThreadLocal对象,因此每一个都会有一个ThreadLocalMap对象,以当前的ThreadLocal对象为key值存储value对象。

    get方法:

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

    通过getMap获取到当前线程的ThreadLocalMap对象,然后如果map不为空再以当前ThreadLocal对象为key获取value,从而保证了数据隔离。如果getMap返回null值则需要进行初始化操作,setInitialValue源码和set方法类似,只不过value值是默认值,如下:

    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }
    // 默认会返回null值,可以通过重写来自定义返回值。
    protected T initialValue() {
        return null;
    }
    

    哈希策略

    在createMap方法内部会通过构造方法实例化一个ThreadLocalMap对象,下面看一下Map内部的存储逻辑:

    //这里新建了一个Entry对象的数据,并且将当前值设置进去。
    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 final int threadLocalHashCode = nextHashCode();
    // 用于计算hashCode,从零开始,在所有的ThreadLocal间共享。
    private static AtomicInteger nextHashCode = new AtomicInteger();
    // 由于初始长度为16,每次扩容为以前的两倍,即长度都是2的n次方,使用这个常量尽可能的分布均匀。
    private static final int HASH_INCREMENT = 0x61c88647;
    // 返回下一个散列码。
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
    

    假设我们set的时候已经创建好了ThreadLocalMap对象,进入ThreadLocalMap对象的set方法看看:

     private void set(ThreadLocal key, Object value) {
            Entry[] tab = table;
            int len = tab.length;//初始为16扩容乘2,所以一直为2的n次幂
            /**  根据key的hash值找到数组的存储位置,使用0x61c88647与上2的n次幂-1,二进制的低位全为1,
                  结果就是0x61c88647的低位,可以验证一下,分布均匀的。
             */
            int i = key.threadLocalHashCode & (len-1);
            /** 如果进入for循环,说明出现了碰撞冲突,用到开放寻址法,线性探测
                解决方案是通过+1的方式逐渐遍历数组找到空位
             */
            for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
                ThreadLocal k = e.get();
                if (k == key) {   // 如果key值相同则直接替换value
                    e.value = value;
                    return;
                }
                if (k == null) {   // 找到空位
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
            // 没有碰撞冲突就直接添加
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
      // nextIndex方法如下
      private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }
    

    关于内存泄漏

    多数观点:threadlocal里面使用了一个存在弱引用的map,当释放掉threadlocal的强引用以后,map里面的value却没有被回收.而这块value永远不会被访问到了. 所以存在着内存泄露. 最好的做法是将调用threadlocal的remove方法。
    但是ThreadLocal在调用set或者get方法的时候都会去检测key有没有被回收(调用expungeStaleEntry方法),然后将其value值设置为null,这个方法和WeakHashMap对象的expungeStaleEntries()方法一样。因此value在key被gc后可能还会存活一段时间,但最终也会被回收,但是若不再调用get或者set方法时,那么这个value就在线程存活期间无法被释放。 expungeStaleEntry方法代码如下:

    private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
    
            // expunge entry at staleSlot
            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();
                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;
        }
    

    本文是为了探究Looper原理记录ThreadlLocal笔记,参考了很多文献如下:
    https://blog.csdn.net/u013256816/article/details/51776846
    https://my.oschina.net/xianggao/blog/392440?fromerr=CLZtT4xC
    《Android开发艺术探索》

    相关文章

      网友评论

          本文标题:ThreadLocal笔记

          本文链接:https://www.haomeiwen.com/subject/iutusftx.html