• ThreadLocal源码原理以及防止内存泄露


    ThreadLocal的原理图

    在线程任务Runnable中,使用一个及其以上ThreadLocal对象保存多个线程的一个及其以上私有值,即一个ThreadLocal对象可以保存多个线程一个私有值

    (重点)每一个线程Thread对象,都有一个threadLocals属性;

    核心属性,因为每个Thread对象(线程)都拥有自己私有的threadLocals属性,ThreadLocal对象通过操作(set和get)每个线程自身私有属性threadLocals(数据结构:ThreadLocal.ThreadLocalMap)做到让每个线程拥有自己的私有变量;

    threadLocals数据结构是ThreadLocal内部类ThreadLocalMap,这个Map中的key是当前ThreadLocal对象,value是线程想要隔离的值,值的类型是ThreadLocal的泛型

    ThreadLocal源码实现

    (1)set方法

    ThreadLocal<String> local=new ThreadLocal<String>(); t.set("herock"); 为啥直接设置value就知道该value属于哪个线程?仔细体会上面的文字。

    首先获取到当前线程,通过getMap(t)传入线程获得该线程持有的实例变量ThreadLocalMap

     getMap(t)返回线程t的实例变量threadLocals

    如果threadLocalsnull就创建map,如果不为null以ThreadLocal实例key,以所存值为value存入map;

    而该Map是线程私有属性threadLocals变量的值,所以建议最好把ThreadLocal对象设置为static,这样该Runnable只创建一个ThreadLocal对象,每个相同或不同的线程任务都引用的同一个ThreadLocal对象;

    什么叫相同或不同的线程任务?比如有RunnableA和RunnableB线程任务都需要ThreadLocal对象,一组线程执行A,一组线程执行B,他们都可以使用同一个ThreadLocal对象,即作为key存入线程自身的私有属性threadLocals中。

    (2)ThreadLocalget方法

    public T get() {
      Thread t = Thread.currentThread();
      ThreadLocalMap map = getMap(t);
      if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);//这里的this是ThreadLocal对象
        if (e != null) {
          @SuppressWarnings("unchecked")
          T result = (T)e.value;//获得value
          return result;
        }
      }
      return setInitialValue();
    }

    防止内存泄漏

    ThreadLocal 被线程的实例变量引用,在线程未死之前, ThreadLocal 变量无法被回收,会导致内存泄漏?其实不然,在源程序中已经有相应的措施了。

    ThreadLocalMap 的每个 Entry 都是一个对  的弱引用;

    看下面的数据结构就知道与HashMap不一样, 没有next, 所以不是用拉链法解决冲突, 在set方法中用的是开放地址法解决key的冲突.

    static class Entry extends WeakReference<ThreadLocal<?>> {
      /** The value associated with this ThreadLocal. */
      Object value;
      Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
      }
    }

    ThreadLocalMap中的entry弱引用(会在下一次垃圾回收时回收),从而避免上文所述 ThreadLocal 被引用不能被回收而造成的内存泄漏的问题。

    但是,这里又可能出现另外一种内存泄漏的问题。

    ThreadLocalMap 维护 ThreadLocal 变量与具体value的映射,当 ThreadLocal 变量被回收后,该Entrykey变为 null,该 Entry 无法被移除。从而使得实例被该 Entry 引用而无法被回收造成内存泄漏。

    再看一次set方法

    针对该问题,ThreadLocalMapset 方法中:

    首先,根据key(ThreadLocal对象)的hashcode与len-1进行按位与计算出在ThreadLocalMap中哈希数组的index下标值,tab[i]即为链表的头结点;

    然后遍历以tab[i]的头结点链表中的元素,如果有元素等于了该Entry(弱引用的ThreadLocal),就把value付给该entry对应的value;

    (重点)如果遍历的元素中有值为null(因为弱引用在垃圾回收时被回收了),就把该位置的元素的设置为null(通过 replaceStaleEntry 方法将所有key为 nullEntry 的值value设置为 null,从而使得该值可被回收,然后在把想要插入的key-value插入进去);

    如果遍历完了都没有与待插入元素相等的链表节点,就将该键值对进行插入

    另外,会在 rehash 方法中通过 expungeStaleEntry 方法将键和值为 null 的 Entry 设置为 null 从而使得该 Entry 可被回收。通过这种方式,ThreadLocal 可防止内存泄漏

    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) {
        //这里是链表中某个Entry元素的key为null时,这里就要进行清理,然后再把想要插入的值key和value插入进去 replaceStaleEntry(key, value, i);
    return; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }

    总结

    • ThreadLocal 并不解决线程间共享数据的问题
    • 每个线程持有一个 Map 并维护了 ThreadLocal 对象与具体实例的映射,该 Map 由于只被持有它的线程访问,故不存在线程安全以及锁的问题
    • ThreadLocalMap 的 Entry弱引用,避免了 ThreadLocal 对象无法被回收的问题
    • ThreadLocalMapset 方法通过调用 replaceStaleEntry 方法回收键为 null 的 Entry 对象的值(即为具体实例)以及 Entry 对象本身从而防止内存泄漏
    • ThreadLocal 适用于变量在线程间隔离且在方法间共享的场景,比如一个Servlet类中有一个属性,有方法对该属性进行写操作和读操作,为了隔离

    参考来源:

    http://www.jasongj.com/java/threadlocal/

    https://blog.csdn.net/herock3/article/details/80160978

  • 相关阅读:
    Java 抽象类 初学者笔记
    JAVA super关键字 初学者笔记
    Java 标准输入流 初学者笔记
    JAVA 将对象引用作为参数修改实例对象参数 初学者笔记
    JAVA 根据类构建数组(用类处理数组信息) 初学者笔记
    JAVA 打印日历 初学者笔记
    Python 测试代码 初学者笔记
    Python 文件&异常 初学者笔记
    Python 类 初学者笔记
    ubuntu网络连接失败
  • 原文地址:https://www.cnblogs.com/theRhyme/p/9561019.html
Copyright © 2020-2023  润新知