qingmei2 / blogs

📝 The Android programing blogs(简体中文).
1.04k stars 126 forks source link

ThreadLocal原理分析 #22

Open qingmei2 opened 5 years ago

qingmei2 commented 5 years ago

ThreadLocal原理分析

接下来笔者的文章方向偏向于 Android & Java 面试相关知识点系统性的总结,欢迎关注。

ThreadLocal类是java.lang包下的一个类,用于线程内部的数据存储,通过它可以在指定的线程中存储数据,本文针对该类进行原理分析。

通过思维导图对其进行简单的总结:

一.ThreadLocal源码分析

ThreadLocal类最重要的几个方法如下:

1.get()方法分析

ThreadLocal类比较简单,其最重要的就是get()set()方法,顾名思义,起作用就是取值和设置值:

// 获取当前线程中的变量副本
public T get() {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取线程中的ThreadLocalMap对象
    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;
        }
    }
    // 若没有该变量副本,返回setInitialValue()
    return setInitialValue();
}

这里先将ThreadLocalMap暂时理解为一个Map结构的容器,内部存储着该线程作用域下的的所有变量副本,我们从ThreadLocal类中取值的时候,实际上是从ThreadLocalMap中取值。

如果Map中没有该变量的副本,会从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;
}

可以看到,setInitialValue()中也非常的简单,依然是从当前线程中获取到ThreadLocalMap,略微不同的是,setInitialValue()会对变量进行初始化,存入ThreadLocalMap中并返回。

这个初始化的方法的执行,需要开发者自己重写initialValue()方法,否则返回值依然为null

public class ThreadLocal<T> {
    // ...
    protected T initialValue() {
       return null;
    }
}    

2.set()方法分析

setInitialValue()方法类似,set()方法也非常简单:

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // map不为空,直接将ThreadLocal对象作为key
        // 变量本身的值为value,存入map
        map.set(this, value);
    else
        // 否则,创建ThreadLocalMap
        createMap(t, value);
}

可以看到,这个方法的作用就是将变量副本作为value存入Map,需要注意的是,key并非是我们下意识认为的Thread对象,而是ThreadLocal本身(ThreadValue本身是一对一的,我们更容易将其映射为key-value的关系)。

3.remove()方法分析

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

对于变量副本的移除,也是通过map进行处理的,和set()get()相同,Entry的键值对中,ThreadLocal本身作为key,对变量副本进行检索。

4.小结

可以看出,ThreadLocal本身内部的逻辑都是围绕着ThreadLocalMap在运作,其本身更像是一个空壳,仅作为API供开发者调用,内部逻辑都委托给了ThreadLocalMap

接下来我们来探究一下ThreadLocalMapThread以及ThreadLocal之间的关系。

二、ThreadLocalMap分析

ThreadLocalMap内部代码和算法相对复杂,个人亦是一知半解,因此就不逐行代码进行分析,仅系统性进行概述。

首先来看一下ThreadLocalMap的定义:

public class ThreadLocal<T> {

    // ThreadLocalMap是ThreadLocal的内部类
    static class ThreadLocalMap {

      // Entry类,内部key对应的是ThreadLocal的弱引用
      static class Entry extends WeakReference<ThreadLocal<?>> {
          // 变量的副本,强引用
          Object value;

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

ThreadLocal中的嵌套内部类ThreadLocalMap本质上是一个map,依然是key-value的形式,其中有一个内部类Entry,其中key可以看做是ThreadLocal实例的弱引用。

和最初的设想不同的是,ThreadLocalMapkey并非是线程的实例Thread,而是ThreadLocal,那么ThreadLocalMap是如何保证同一个Thread中,ThreadLocal的指定变量唯一呢?

// 1.ThreadLocal的set()方法
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    // ...
}

// 2.getMap()实际上是从Thread中获取threadLocals成员
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

public class Thread implements Runnable {
    // 3.每个Thread实例都持有一个ThreadLocalMap的属性
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

Thread本身持有ThreadLocal.ThreadLocalMap的属性,每个线程在向ThreadLocalsetValue的时候,其实都是向自己的ThreadLocalMap成员中加入数据;get()同理。

三、内存泄漏的风险?

在上一小节中,我们看到ThreadLocalMap中的Entry中,其ThreadLocal作为key,是作为弱引用进行存储的。

ThreadLocal不再被作为强引用持有时,会被GC回收,这时ThreadLocalMap对应的ThreadLocal就变成了null。而根据文档所叙述的,当key == null时,这时就可以默认该键不再被引用,该Entry就可以被直接清除,该清除行为会在Entry本身的set()/get()/remove()中被调用,这样就能 一定情况下避免内存泄漏

这时就有一个问题出现了,作为keyThreadLocal变成了null,那么作为value的变量可是强引用呀,这不就导致内存泄漏了吗?

其实一般情况下也不会,因为即使再不济,线程在执行结束时,自然也会消除其对value的引用,使得Value能够被GC回收。

当然,在某种情况下(比如使用了 线程池),线程再次被使用,Value这时依然可以被获取到,自然也就发生了内存泄漏,因此此时,我们还是需要通过手动将value的值设置为null(即调用ThreadLocal.remove()方法)以规避内存泄漏的风险。

参考&感谢


关于我

Hello,我是却把清梅嗅,如果您觉得文章对您有价值,欢迎 ❤️,也欢迎关注我的博客或者Github

如果您觉得文章还差了那么点东西,也请通过关注督促我写出更好的文章——万一哪天我进步了呢?