• 线程局部变量ThreadLocal实现原理


    之前做项目用到过ThreadLocal,但是没有看源码层面的具体实现,今天特来补一补课。

    ThreadLocal,即线程局部变量,用来为每一个使用它的线程维护一个独立的变量副本。

    这种变量只在线程的生命周期内有效。并且与锁机制那种以时间换取空间的做法不同,ThreadLocal没有任何锁机制,它以空间换取时间的方式保证变量的线程安全

    1、ThreadLocal类图结构

    SuppliedThreadLocal:主要是JDK1.8用来扩展对Lambda表达式的支持,有兴趣的自行百度。

    ThreadLocalMap:是ThreadLocal的静态内部类,也是实际保存变量的类。

    Entry:是ThreadLocalMap的静态内部类。ThreadLocalMap持有一个Entry数组,以ThreadLocal为key,变量为value,封装一个Entry。

    2、Thread,ThreadLocal,ThreadLocalMap和Entry的关系

     说明:

    1. 一个Thread拥有一个ThreadLocalMap对象;
    2. 一个ThreadLocalMap拥有多个Entry数组;
    3. 每个Entry都有k--v;
    4. Entry的key就是某个具体的ThreadLocal对象,是弱引用,key丢失后,GC不会回收Value会造成OOM(后面详细说)

    3、ThreadLocal的set()方法

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

    可看出:

    1、一个Thread只拥有一个ThreadLocalMap对象;

    2、具体存值调用的是ThreadLocalMap的set(),传入的参数key就是当前ThreadLocal对象。

    3.1、ThreadLocalMap的set()方法

    private void set(ThreadLocal<?> key, Object value) {
                Entry[] tab = table;
                int len = tab.length;
                int i = key.threadLocalHashCode & (len-1); // 1
    
                for (Entry e = tab[i];  // 2
                     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);
                        return;
                    }
                }
    
                tab[i] = new Entry(key, value); // 3
                int sz = ++size;
                if (!cleanSomeSlots(i, sz) && sz >= threshold) // 4
                    rehash();
            }

    可看出:

    1、通过key的hashCode与数组容量 -1 取模,计算数组index;

    2、从当前index开始遍历,清除key为null的无效Entry

    3、将K-V封装为Entry,并放入数组

    4、判断是否需要进行Entry数组扩容。threshold的值为数组容量的2/3。

    大家可以回顾一下HashMap的源码,是不是很相似?

    index都是和长度-1取模,也都是容量不够了,超出阈值就扩容(这里是扩容为2倍,和HashMap一样)。

    3.2、扩容方法resize()

    private void resize() {
                Entry[] oldTab = table;
                int oldLen = oldTab.length;
                int newLen = oldLen * 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);  //1.计算新的索引
                            while (newTab[h] != null)
                                h = nextIndex(h, newLen);
                            newTab[h] = e;
                            count++;
                        }
                    }
                }
    
                setThreshold(newLen);
                size = count;
                table = newTab;
            }

    1、然后遍历旧数组,根据新数组容量重新计算Entry在新数组中的位置。

    4、ThreadLocal的get()方法

    public T get() {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t); 
            if (map != null) {
                ThreadLocalMap.Entry e = map.getEntry(this);   //ThreadLocalMap的getEntry()方法
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    T result = (T)e.value;
                    return result;
                }
            }
            return setInitialValue();
        }

    4.1、ThreadLocalMap的getEntry()方法

    private Entry getEntry(ThreadLocal<?> key) {
                int i = key.threadLocalHashCode & (table.length - 1); // 1:计算索引
                Entry e = table[i];
                if (e != null && e.get() == key) // 2:当前Entry不为空,且key相同,直接返回entry;
                    return e;
                else
                    return getEntryAfterMiss(key, i, e); //3:否则去邻居index寻找
            }
    
    private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
                Entry[] tab = table;
                int len = tab.length;
    
                while (e != null) { //4:循环查找,发现无效key就清楚,直到找到结束循环;
                    ThreadLocal<?> k = e.get();
                    if (k == key)
                        return e;
                    if (k == null)
                        expungeStaleEntry(i);
                    else
                        i = nextIndex(i, len);
                    e = tab[i];
                }
                return null;
            }

    5、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);  //1:求得索引
                for (Entry e = tab[i];
                     e != null;
                     e = tab[i = nextIndex(i, len)]) {
                    if (e.get() == key) {
                        e.clear();    //清除entry<k,v>,显示调用,可预防OOM
                        expungeStaleEntry(i);
                        return;
                    }
                }
            }

    注意:

    static class Entry extends WeakReference<ThreadLocal<?>> {}

    ThreadLocal可能存在OOM问题。

    因为ThreadLocalMap是使用ThreadLocal的弱引用作为key的,发生GC时,key被回收,这样我们就无法访问key为null的value元素。

    如果value本身是较大的对象,那么线程一直不结束的话,value就一直无法得到回收。特别是在我们使用线程池时,线程是复用的,不会杀死线程,这样ThreadLocal弱引用被回收时,value不会被回收。

    所以,在使用ThreadLocal时,线程逻辑代码结束时,必须显示调用ThreadLocal.remove()方法。

    参考致谢:

    1、线程局部变量ThreadLocal实现原理

    Over.......

  • 相关阅读:
    Android相对布局中控件的常用属性【转】
    Android:仿微信设置菜单
    Android:scrollview与listview共存
    感想12.26
    (C#)GDI+绘制垂直文字
    10.14 近期小结
    学习C++的忠告
    C# TCP学习笔记
    C#读书笔记(4)—重学数组
    近期学习计划 12.23
  • 原文地址:https://www.cnblogs.com/gjmhome/p/14395883.html
Copyright © 2020-2023  润新知