您现在的位置是:首页 > 文章详情

java源码-ThreadLocal

日期:2018-08-10点击:492

开篇

 ThreadLocal主要解决的问题是给每个线程绑定自己的值,这个值是和线程绑定的,是线程的局部变量,是其他线程没法访问的。

 ThreadLocal的源码的核心知识点在于ThreadLocal变量如何跟线程绑定ThreadLocal如何实现gc垃圾回收,这篇文章希望能够讲解清楚这两个知识点。

 ThreadLocal的实现原理:每一个线程在使用ThreadLocal的时候实际上是以ThreadLocal对象作为key值共享的对象为value值保存在Thread.threadLocalMaps变量中也就是ThreadLocalMap实例,因此ThreadLocal仅仅是作为一个key值来保存,多线程在使用同一个ThreadLocal或者不同的ThreadLocal但是保存相同的共享对象时他们的threadLocalMaps值是相同的,因此如果有共享资源发生冲突问题,ThreadLocal并不能解决!如果要解决并发冲突的问题要么使用安全对象,要么使用上锁机制来保证多线程顺序访问!

ThreadLocal的案例

 通过下面例子可以对ThreadLocal的有一个直观的了解。

  • 案例一:主线程main当中存在对象StudentInfo而子线程Thread-0当中不存在该变量,因为主线程通过set设置对象。
  • 案例二:主线程main当中的对象StudentInfo通过set设置而子线程Thread-0当中的对象是初始化函数提供的,两者也不相同。
    /** 创建一个ThradLocal实例 */
    private static ThreadLocal<StudentInfo> threadLocal = new ThreadLocal<StudentInfo>();
 
    public static void main(String[] args) {
        StudentInfo info = new StudentInfo("sdew23", "张三", "男");
        // 为主线程保存一个副本StudentInfo对象
        threadLocal.set(info);
        System.out.println(threadLocal.get());
        System.out.println(Thread.currentThread().getName());
        // 开启子线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                // threadLocal.set(info);
                System.out.println(threadLocal.get());
                System.out.println(Thread.currentThread().getName());
            }
        }).start();
      }
img_407e58fb0d7d06f792205b2ea0300572.png




    /** 创建一个ThradLocal实例 */
    private static ThreadLocal<StudentInfo> threadLocal = new ThreadLocal<StudentInfo>() {
        @Override
        public StudentInfo initialValue() {
            return new StudentInfo("sss", "小西", "女");
        }
    };
 
    public static void main(String[] args) {
        StudentInfo info = new StudentInfo("sdew23", "张三", "男");
        // 为主线程保存一个副本StudentInfo对象
        threadLocal.set(info);
        System.out.println(threadLocal.get());
        System.out.println(Thread.currentThread().getName());
        // 开启子线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                // threadLocal.set(info);
                System.out.println(threadLocal.get());
                System.out.println(Thread.currentThread().getName());
            }
        }).start();
    }
img_5238a50c872c48a46e3895d7854c4d82.png


ThreadLocal线程隔离的原理

 ThreadLocal之所以能够实现线程隔离主要是因为在Thread的类变量当中存在一个ThreadLocal.ThreadLocalMap threadLocals类型对象,这样保证了每个线程能够单独维护一份ThreadLocal对象的map,也就自然而然实现了线程隔离。

 ThreadLocal对象的set操作获取当前线程的ThreadLocal.ThreadLocalMap对象,以ThreadLocal对象作为key,以ThreadLocal保存的对象如下例中的StudentInfo为value,保存到ThreadLocalMap当中,每个线程执行set操作都是把对象保存至对应的ThreadLocalMap,所以也就解释了为什么能够隔离了。

ThreadLocal.ThreadLocalMap采用环形数组实现,也就是Entry[] table,set操作就是创建一个Entry对象然后放到环形数组table当中,通过线性探测方法解决冲突问题

// 每个线程包含一个ThreadLocal.ThreadLocalMap对象
    public class Thread implements Runnable {
         ThreadLocal.ThreadLocalMap threadLocals = null;
    }


    {
          private static ThreadLocal<StudentInfo> threadLocal = new ThreadLocal<StudentInfo>();
          StudentInfo info = new StudentInfo("sdew23", "张三", "男");
          threadLocal.set(info);
    }


    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 getMap(Thread t) {
        return t.threadLocals;
    }


  public class ThreadLocal<T> {
     static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

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

        private Entry[] table;

        private void set(ThreadLocal<?> key, Object value) {
            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);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
    }
}
img_da00f59d2b85f81d9ad57c78a4d6ccb2.png
环形数组


ThreadLocal的get操作

 ThreadLocal的get操作从当前Thread获取ThreadLocal.ThreadLocalMap threadLocals对象,在ThreadLocalMap中以ThreadLocal对象作为key返回Entry也即ThreadLocal当中保存的value。

 ThreadLocal的get操作内部考虑线性探测方法来解决存储时候的冲突问题,第一次以hash值取值成功则直接返回,如果未取到那么就通过线性探测法继续查找直到null值为止。

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


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


        private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }


        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);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }


ThreadLocal的gc清理

ThreadLocal的key回收

 ThreadLocal的垃圾回收机制是它的一大设计亮点,也是面试当中经常会被问到的问题,这里我们从两个维度进行说明。

  • ThreadLocal当中的Entry的key实现了WeakReference弱引用,所以gc机制可以对key进行回收
  • ThreadLocal当中的Entry的value由于没实现WeakReference弱引用,所以只有程序主动进行回收,在set/get操作中实现回收。
  • 关于回收的细节可以参考文章《一篇文章,从源码深入详解ThreadLocal内存泄漏问题》,作者讲解的非常清楚给我很大启发。
        // 关于key的弱引用实现机制
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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


ThreadLocal的value回收-get阶段

 getEntryAfterMiss过程当中会调用expungeStaleEntry()方法删除过期数据,判断过期数据的标准是对Entry.get()方法返回的key为null(被jvm的gc主动回收了),这个时候需要把对应的value也进行删除。

 ThreadLocal的expungeStaleEntry()方法内部除了删除staleSlot指定的过期数据外,还负责检查往后遍历直至遇到数组元素为null停止。在删除过程中还会对某些未过期的数据进行重hash,我认为之所以重hash有可能之前通过线性探测法往后放的,通过重hash后如果发现原来位置为空则可以直接放置hash值对应的下标位置。之所以这么做我个人觉得是因为get或者set的依据都是以null作为结束依据。

 get时候重hash是为了把元素放置到hash值第一次对应的位置当中(未经过线性探测法解决碰撞冲突),这样就可以支持以null作为结尾的依据了。

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


ThreadLocal的value回收过程

 cleanSomeSlots以i作为起始地址,expungeStaleEntry()完成i位置的元素的gc回收,然后继续遍历回收直至遇到null元素的下标i,然后由cleanSomeSlots继续nextIndex()下移到下一个位置继续查找。

        private boolean cleanSomeSlots(int i, int n) {
            boolean removed = false;
            Entry[] tab = table;
            int len = tab.length;
            do {
                i = nextIndex(i, len);
                Entry e = tab[i];
                if (e != null && e.get() == null) {
                    n = len;
                    removed = true;
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0);
            return removed;
        }




        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;
        }
img_4ab49fb0df5149bc53be50ed36a3e566.png
cleanSomeSlots过程图

说明:

  • 1、如图当前n等于hash表的size即n=10,i=1,在第一趟搜索过程中通过nextIndex,i指向了索引为2的位置,此时table[2]为null,说明第一趟未发现脏entry,则第一趟结束进行第二趟的搜索。

  • 2、第二趟所搜先通过nextIndex方法,索引由2的位置变成了i=3,当前table[3]!=null但是该entry的key为null,说明找到了一个脏entry,先将n置为哈希表的长度len,然后继续调用expungeStaleEntry方法,该方法会将当前索引为3的脏entry给清除掉(令value为null,并且table[3]也为null),但是该方法可不想偷懒,它会继续往后环形搜索,往后会发现索引为4,5的位置的entry同样为脏entry,索引为6的位置的entry不是脏entry保持不变,直至i=7的时候此处table[7]位null,该方法就以i=7返回。至此,第二趟搜索结束;

  • 3、由于在第二趟搜索中发现脏entry,n增大为数组的长度len,因此扩大搜索范围(增大循环次数)继续向后环形搜索;

  • 4、直到在整个搜索范围里都未发现脏entry,cleanSomeSlot方法执行结束退出。


参考文章

ThreadLocal理解与使用
一篇文章,从源码深入详解ThreadLocal内存泄漏问题

原文链接:https://yq.aliyun.com/articles/666327
关注公众号

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。

持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。

转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。

文章评论

共有0条评论来说两句吧...

文章二维码

扫描即可查看该文章

点击排行

推荐阅读

最新文章