IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    一文读懂ThreadLocal的原理及使用场景

    架构狂魔哥发表于 2023-07-20 23:42:40
    love 0

    ThreadLocal 是什么

    ThreadLocal 类是用来提供线程内部的局部变量,即线程本地变量。这种变量在多线程环境下访问(通过get和set方法访问)时能够保证各个线程的变量相对独立于其他线程内的变量,不同线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度。

    顾名思义,ThreadLocal 表示线程的“本地变量”,即每个线程都拥有该变量副本,达到人手一份的效果,各用各的,这样就可以避免共享资源的竞争。

    ThreadLocal 的使用场景

    高并发中会存在多个线程同时修改一个共享变量的场景,这就可能会出现线性安全问题。

    为了解决线性安全问题,可以通过加锁来实现,例如使用synchronized 或者Lock。但是加锁的方式可能会导致系统变慢。加锁方式如下所示:

    另外一种方式,可以使用ThreadLocal类访问共享变量,这样会在每个线程的本地,都保存一份共享变量的拷贝副本。这是一种“空间换时间”的方案,虽然会让内存占用大很多,但是由于不需要同步也就减少了线程可能存在的阻塞等待,从而提高时间效率。

    ThreadLocal 的实现原理

    接下来就让我们学习 ThreadLocal 的几个核心方法,来了解ThreadLocal 的实现原理。

    方法声明描述
    public void set(T value)设置当前线程绑定的局部变量
    public T get()获取当前线程绑定的局部变量
    Public void remove()移除当前线程绑定的局部变量

    set() 方法

    set 方法设置当前线程中 ThreadLocal 变量的值,该方法的源码为:

    public void set(T value) {
          //1. 获取当前线程实例对象
        Thread t = Thread.currentThread();
    
          //2. 通过当前线程实例获取到ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
    
        if (map != null)
                //3. 如果Map不为null,则以当前ThreadLocal实例为key,值为value进行存入
            map.set(this, value);
        else
                //4.map为null,则新建ThreadLocalMap并存入value
          createMap(t, value);
    }

    方法的逻辑很清晰,具体请看上面的注释。通过源码我们知道 value 是存放在 ThreadLocalMap 里的,当前先把它理解为一个普普通通的 map 即可,也就是说,数据 value 是存放在 ThreadLocalMap 这个容器中的,并且是以当前 ThreadLocal 实例为 key 的。

    简单看下 ThreadLocalMap 是什么,有个简单的认识就好,后面会具体说的。

    首先 ThreadLocalMap 是怎样来的?源码很清楚,是通过getMap(t)进行获取:

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

    该方法直接返回当前线程对象 t 的一个成员变量 ThreadLocals:

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

    也就是说ThreadLocalMap 的引用是作为 Thread 的一个成员变量的,被 Thread 进行维护的。回过头再来看 set 方法,当 map 为 Null 的时候会通过createMap(t,value)方法 new 出来一个:

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

    该方法就是new 一个 ThreadLocalMap 实例对象,然后同样以当前 ThreadLocal 实例作为 key,值为 value 存放到 ThreadLocalMap 中的,然后将当前线程对象的 ThreadLocals 赋值为 ThreadLocalMap 对象。

    总结一下 set 方法:

    通过当前线程对象 thread 获取该 thread 所维护的 ThreadLocalMap,如果 ThreadLocalMap 不为 null,则以 ThreadLocal 实例为 key,值为 value 的键值对存入 ThreadLocalMap,若 ThreadLocalMap 为 null 的话,就新建 ThreadLocalMap,然后再以 ThreadLocal 为键,值为 value 的键值对存入即可。

    get() 方法

    get 方法是获取当前线程中 ThreadLocal 变量的值,同样的先看源码:

    public T get() {
        //1. 获取当前线程的实例对象
      Thread t = Thread.currentThread();
    
        //2. 获取当前线程的ThreadLocalMap
      ThreadLocalMap map = getMap(t);
      if (map != null) {
            //3. 获取map中当前ThreadLocal实例为key的值的entry
        ThreadLocalMap.Entry e = map.getEntry(this);
    
        if (e != null) {
          @SuppressWarnings("unchecked")
                //4. 当前entitiy不为null的话,就返回相应的值value
          T result = (T)e.value;
          return result;
        }
      }
        //5. 若map为null或者entry为null的话通过该方法初始化,并返回该方法返回的value
      return setInitialValue();
    }

    弄清楚 set 方法的逻辑后,看 get 方法只需要带着逆向思维去看就好,如果是那样存的,反过来去拿就好。代码逻辑请看注释,另外,看下 setInitialValue 主要做了些什么事情?

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

    这段方法的逻辑和 set 方法几乎一致,另外值得关注的是 initialValue 方法:

    protected T initialValue() {
        return null;
    }

    这个方法是 protected 修饰的,也就是说继承 ThreadLocal 的子类可重写该方法,实现赋值为其他的初始值。

    总结一下 get 方法:

    通过当前线程 thread 实例获取到它所维护的 ThreadLocalMap,然后以当前 ThreadLocal 实例为 key 获取该 map 中的键值对(Entry),如果 Entry 不为 null 则返回 Entry 的 value。如果获取 ThreadLocalMap 为 null 或者 Entry 为 null 的话,就以当前 ThreadLocal 为 Key,value 为 null 存入 map 后,并返回 null。

    remove() 方法

    public void remove() {
        //1. 获取当前线程的ThreadLocalMap
        ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
            //2. 从map中删除以当前ThreadLocal实例为key的键值对
            m.remove(this);
    }

    get、set 方法实现了存数据和读数据的操作,remove 方法实现了如何删数据的操作。删除数据当然是从 map 中删除数据,先获取与当前线程相关联的 ThreadLocalMap,然后从 map 中删除该 ThreadLocal 实例为 key 的键值对即可。

    ThreadLocalMap 详解

    从上面的分析我们已经知道,数据其实都放在了 ThreadLocalMap 中,ThreadLocal 的 get、set 和 remove 方法实际上都是通过 ThreadLocalMap 的 getEntry、set 和 remove 方法实现的。如果想真正全方位的弄懂 ThreadLocal,势必得再对 ThreadLocalMap 做一番理解。

    Entry 数据结构

    ThreadLocalMap 是 ThreadLocal 一个静态内部类,和大多数容器一样,内部维护了一个数组(Entry 类型的 table 数组)。

    /**
     * The table, resized as necessary.
     * table.length MUST always be a power of two.
     */
    private Entry[] table;

    通过注释可以看出,table 数组的长度为 2 的幂次方。接下来看下 Entry 是什么:

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

    Entry 是一个以 ThreadLocal 为 key,Object 为 value 的键值对,另外需要注意的是这里的ThreadLocal 是弱引用,因为 Entry 继承了 WeakReference,在 Entry 的构造方法中,调用了 super(k)方法,会将 ThreadLocal 实例包装成一个 WeakReferenece。

    到这里我们可以用一个图来理解下 Thread、ThreadLocal、ThreadLocalMap、Entry 之间的关系:

    注意上图中的实线表示强引用,虚线表示弱引用。如图所示,每个线程实例中都可以通过 ThreadLocals 获取到 ThreadLocalMap,而 ThreadLocalMap 实际上就是一个以 ThreadLocal 实例为 key,任意对象为 value 的 Entry 数组。

    当我们为 ThreadLocal 变量赋值时,实际上就是以当前 ThreadLocal 实例为 key,值为 value 的 Entry 往这个 ThreadLocalMap 中存放。

    需要注意的是,Entry 中的 key 是弱引用,当 ThreadLocal 外部强引用被置为 null(ThreadLocalInstance=null)时,那么系统 GC 的时候,根据可达性分析,这个 ThreadLocal 实例就没有任何一条链路能够引用到它,此时 ThreadLocal 势必会被回收,这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry,就没有办法访问这些 key 为 null 的 Entry 的 value,如果当前线程再迟迟不结束的话,这些 key 为 null 的 Entry 的 value 就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value 永远无法回收,造成内存泄漏。

    当然,如果当前 thread 运行结束,ThreadLocal、ThreadLocalMap、Entry 没有引用链可达,在垃圾回收的时候都会被系统回收。在实际开发中,会使用线程池去维护线程的创建和复用,比如固定大小的线程池,线程为了复用是不会主动结束的。

    set 方法

    与 ConcurrentHashMap、HashMap 等容器一样,ThreadLocalMap 也是采用散列表进行实现的。

    ThreadLocalMap 中使用开放地址法来处理散列冲突,而 HashMap 中使用的分离链表法。之所以采用不同的方式主要是因为:

    在 ThreadLocalMap 中的散列值分散的十分均匀,很少会出现冲突。并且 ThreadLocalMap 经常需要清除无用的对象,使用纯数组更加方便。

    set 方法的源码如下:

    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;
        //根据ThreadLocal的hashCode确定Entry应该存放的位置
        int i = key.ThreadLocalHashCode & (len-1);
    
        //采用开放地址法,hash冲突的时候使用线性探测
        for (Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            ThreadLocal<?> k = e.get();
            //覆盖旧Entry
            if (k == key) {
                e.value = value;
                return;
            }
            //当key为null时,说明ThreadLocal强引用已经被释放掉,那么就无法
            //再通过这个key获取ThreadLocalMap中对应的entry,这里就存在内存泄漏的可能性
            if (k == null) {
                //用当前插入的值替换掉这个key为null的“脏”entry
                replaceStaleEntry(key, value, i);
                return;
            }
        }
        //新建entry并插入table中i处
        tab[i] = new Entry(key, value);
        int sz = ++size;
        //插入后再次清除一些key为null的“脏”entry,如果大于阈值就需要扩容
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }

    set 方法的关键部分请看上面的注释。

    ThreadLocal 的 hashcode

    private final int ThreadLocalHashCode = nextHashCode();
    private static final int HASH_INCREMENT = 0x61c88647;
    private static AtomicInteger nextHashCode =new AtomicInteger();
      /**
       * Returns the next hash code.
       */
      private static int nextHashCode() {
          return nextHashCode.getAndAdd(HASH_INCREMENT);
      }

    从源码中我们可以清楚的看到 ThreadLocal 实例的 hashCode 是通过 nextHashCode() 方法实现的,该方法实际上是用一个 AtomicInteger 加上 0x61c88647 来实现的。

    0x61c88647 这个数是有特殊意义的,它能够保证 hash 表的每个散列桶能够均匀的分布,这是Fibonacci Hashing。

    也正是能够均匀分布,所以 ThreadLocal 选择使用开放地址法来解决 hash 冲突的问题。

    总结

    本文主要讲解了ThreadLocal的作用及基本用法,以及ThreadLocal的实现原理和基础方法。线上环境中,ThreadLocal还有可能引起内存泄漏,这方面内容我们后续接着讲。

    本文由mdnice多平台发布



沪ICP备19023445号-2号
友情链接