• 结合源码谈谈ThreadLocal!


    网上有很多关于ThreadLocal的介绍,有的介绍比较简单,也有的介绍很复杂,比较难懂,今天,自己结合它的源码,也做个简易梳理,记录如下!

    ThreadLocal的作用

    在多请求并发访问过程中,我们往往需要将一个指定变量隔离起来,达到只对当前线程可用,其他线程不可用的效果,因此,我们就会使用到ThreadLocal来实现。

    实现原理其实就是在每个线程中维护了一个Map结构(ThreadLocalMap,它是ThreadLocal中的静态内部类),ThreadLocal对象为Key,需要隔离的值为Value。为了达到线程全局可用,我们往往将ThreadLocal声明为全局静态变量。

    Thread中的ThreadLocalMap对象

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
    

    那么ThreadLocal具体如何做到线程隔离的?我们下面做具体分析!

    ThreadLocal

    ThreadLocal的生命周期图示如下:

    我们暂不分析ThreadLocalMap,先单独来看ThreadLocal的几个方法源码介绍!

    1.对象初始化

    ThreadLocal初始化比较简单!

    public static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();
    

    我们往往在初始化时会给他指定一个默认值,不指定的话,默认值为null,这里有两种指定方式:

    第一种:直接复写ThreadLocal中的initialValue方法
    第二种:利用函数式编程,创建SuppliedThreadLocal对象,由get方法直接返回初始值

    public static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<String>(){
        @Override
        protected String initialValue() {
            return "Test";
        }
    };
    
    public static final ThreadLocal<String> THREAD_LOCAL = ThreadLocal.withInitial(()->"Test");
    

    SuppliedThreadLocal对象是对ThreadLocal的一个特定实现,通过构造函数传入Supplier,再由实现的initialValue方法返回supplier.get()的结果,其他也没什么可多介绍的。

    2.获取变量

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

    从get方法我们可以看到,ThreadLocal是从当前线程中获取到了ThreadLocalMap对象,然后取出其中的Entry.Value值,如果对象不存在就返回初始值,初始化方法initialValue会在这里调用一次,其他操作不再调用。

    3.设置变量

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

    set方法与get方法一样,会通过当前线程取出ThreadLocalMap对象,然后将当前ThreadLocal对象作为Key,存储Value值,ThreadLocalMap不存在时,会创建新的Map。

    4.移除变量

    public void remove() {
       ThreadLocalMap m = getMap(Thread.currentThread());
       if (m != null)
           m.remove(this);
    }
    

    移除变量同样是根据currentThread来找到的Map,然后对当前ThreadLocal做remove操作。

    ThreadLocalMap

    通过ThreadLocal的操作介绍我们可以看到,ThreadLocal的操作都是基于ThreadLocalMap来实现的,所以,ThreaLocalMap才是我们对ThreadLocal变量实现线程隔离的重点。

    1.Entry

    ThreadLocalMap中存储数据关系的是Entry,它的Key是ThreadLocal对象,采用弱引用,Value是一个强引用对象Object。当Entry.get()获取的ThreadLocal为Null时,GC回收将直接清除该对象,但Value对象,需要我们手动清除,所以,我们需要在每个ThreadLocal调用结束时,执行remove方法,否则,有可能出现内存泄漏情况。

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

    那么,为什么ThreadLocal中没有使用普通的Key-Value形式定义存储结构呢?

    因为如果这里使用普通的key-value形式来定义存储结构,实质上就会造成节点的生命周期与线程强绑定,只要线程没有销毁,那么节点在GC分析中一直处于可达状态,没办法被回收,而程序本身也无法判断是否可以清理节点。弱引用是Java中四档引用的第三档,比软引用更加弱一些,如果一个对象没有强引用链可达,那么一般活不过下一次GC。当某个ThreadLocal已经没有强引用可达,则随着它被垃圾回收,在ThreadLocalMap里对应的Entry的键值会失效,这为ThreadLocalMap本身的垃圾清理提供了便利。

    关于弱引用与强引用的关系以及他们的对象回收机制,这里不做过多介绍,有兴趣的同学可以自行学习!

    2.初始化

    ThreadLocalMap的操作是基于Entry[]数组table完成的,数组初始化大小为16。table是一个2的N次方的数组,ThreadLocal通过AtomicInteger类型的nextHashCode,每次偏移HASH_INCREMENT=0x61c88647的大小来实现数据在数组上的平均分布。

    Entry[]数组table为什么是一个2的N次方数组呢?

    第一个原因是ThreadLocalMap使用的是开放定址法中的线性探测法,均匀分布的好处在于很快就能探测到下一个临近的可用slot,从而保证效率。
    第二个原因是位运算比取模效率高,rehash的时候只需要判断0还是1。

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

    关于Entry[]中如何解决碰撞冲突问题,可以参考:ThreadLocal 和神奇的数字 0x61c88647

    3.获取Entry

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

    查询table中的Entry值时,采用神奇的0x61c88647,ThreadLocal对象作为Key与Entry的Key相同时,返回此Entry,否则,采用开放定址法,从i开始线性探测查找Entry。

    4.设置Entry

    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);
              return;
          }
      }
    
      tab[i] = new Entry(key, value);
      int sz = ++size;
      if (!cleanSomeSlots(i, sz) && sz >= threshold)
          rehash();
    }
    

    set方法中兼容新增与修改操作,如果找到同一个ThreadLocal对应的Entry时,则直接重新赋值Value,否则新建Entry赋值给table[i]。

    5.移除Entry

    private void remove(ThreadLocal<?> key) {
        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)]) {
            if (e.get() == key) {
                e.clear();
                expungeStaleEntry(i);
                return;
            }
        }
    }
    

    remove操作同样采用线性探测到指定的table[i],找到Key相同的ThreadLocal对象,然后通过指定弱引用的Key值为Null移除,并将table[i].value也置为Null,通过GC回收彻底删除元素。

    InheritableThreadLocal

    ThreadLocal解决了线程隔离问题,但对于子线程想要获取到父线程中的变量,又如何做呢?JDK为我们提供了另外一个线程本地变量实现类InheritableThreadLocal

    InheritableThreadLocal继承自ThreadLocal,与ThreadLocal一样,它也会在Thread中定义一个Map结构来维护Entry访问。

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    

    所以,每个Thread中都会保存有当前线程的ThreadLocal对象threadLocals,和继承父线程的ThreadLocal对象inheritableThreadLocals。

    那么,父线程数据如何传递给子线程的呢?我们来看Thread的init方法,是如何做的。

    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        this.inheritableThreadLocals =
           ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    

    线程初始化时,会将父线程的ThreadLocalMap传给子线程,通过Entry[]数组拷贝,完成子线程ThreadLocal对象的创建。具体操作在ThreadMap的另一构造方法完成。

    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) {
                @SuppressWarnings("unchecked")
                ThreadLocal<Object> key = (ThreadLocal<Object>) 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++;
                }
            }
        }
    }
    

    InheritableThreadLocal中重写了ThreadLocal的三个方法:

    childValue:获取父线程变量值。
    getMap:获取继承过来的ThreaLocal对象。
    createMap:创建继承父线程的ThreadLocal对象。

    InheritableThreadLocal实现了Entry数组拷贝后,其他操作方法与ThreadLocal相同。

  • 相关阅读:
    将抓包工具证书从用户目录移动至系统目录,解决反爬对于本地证书认证(安卓7)
    《C++ concurrency in action》 读书笔记 -- Part 2 第三章 线程间的数据共享
    《C++ concurrency in action》 读书笔记 -- Part 3 第四章 线程的同步
    C++14 也快要来了
    《C++ concurrency in action》 读书笔记 -- Part 4 第五章 C++的多线程内存模型 (1)
    利用表达式树构建委托改善反射性能
    使用Task简化Silverlight调用Wcf(再续)
    逆变与协变详解
    Beginning Silverlight 4 in C#数据访问和网络
    使用Task简化Silverlight调用Wcf(续)
  • 原文地址:https://www.cnblogs.com/ason-wxs/p/13706467.html
Copyright © 2020-2023  润新知