ThreadLocal

45 阅读7分钟

什么是ThreadLocal?

ThreadLocalJava中一个非常重要的线程封闭工具,它用于创建线程局部变量。每个线程都有自己独立初始化的变量副本,从而避免了多线程环境下的共享和同步问题。

ThreadLocal实现原理

set方法

    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方法是根据当前线程对象去取出一个ThreadLocalMap对象,实际的数据是放在线程中的ThreadLocalMap对象中的。键是ThreadLocal对象,值是传入的参数。

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

getMap这个方法可以看出,这个ThreadLocalMap对象是线程对象的一个数据对象。

Thread类中的数据域:

ThreadLocal.ThreadLocalMap threadLocals = null;
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

在默认情况下,线程中的这两个变量都为null,只有当线程第一次调用ThreadLocal中的set或者get方法时才会创建它们,因此在ThreadLocal中的set方法中会判断map是否存在,如果不存在,就会调用createMap来创建。

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

get方法

    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方法依然是先获取线程中的ThreadLocalMap,然后以ThreadLocal对象为键,查找对应的值。

setInitialValue是一个兜底操作,放入一个初始化的值,并返回,默认是返回null,可以重写返回一个实际的值。

下面举一个实际例子:

public class ThreadLocalWithDefault<T> extends ThreadLocal<T> {
    public interface DefaultObjectFactory<T>{
        public T getDefault();
    }
    private DefaultObjectFactory<T> defaultObjectFactory;
    public ThreadLocalWithDefault(DefaultObjectFactory<T> defaultObjectFactory) {
        this.defaultObjectFactory=defaultObjectFactory;
    }
    @Override
    protected T initialValue() {
        return defaultObjectFactory.getDefault();
    }
}
public static void main(String[] args) {
    ThreadLocalWithDefault<String> threadLocalWithDefault=new ThreadLocalWithDefault<String>(()->{
        return "hello,Thread";
    });
    String s = threadLocalWithDefault.get();
    System.out.println(s);
}

这个ThreadLocalWithDefault重写了initialValue,因此当线程直接get之后,可以获得到initialValue返回的值,并将其设置在ThreadLocal中。

数据清理

从上面可以看出,数据实际上是存在线程中的,当线程不退出的情况下,对象的引用将一致存在。

当线程退出的时候,会做一些清理操作,其中包括清理ThreadLocalMap

    private void exit() {
        if (threadLocals != null && TerminatingThreadLocal.REGISTRY.isPresent()) {
            TerminatingThreadLocal.threadTerminated();
        }
        if (group != null) {
            group.threadTerminated(this);
            group = null;
        }
        /* Aggressively null out all reference fields: see bug 4006245 */
        target = null;
        /* Speed the release of some of these resources */
        threadLocals = null;
        inheritableThreadLocals = null;
        inheritedAccessControlContext = null;
        blocker = null;
        uncaughtExceptionHandler = null;
    }

因此,使用线程池的时候,当前线程会保留在线程池中,如果将一个比较大的对象设置在ThreadLocal中,可能会出现内存泄漏。

因此要及时回收对象,使用ThreadLocal对象的remove方法。

ThreadLocalMap

ThreadLocalMap中的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与一般的Entry不同,ThreadLocalMap中传入的键成为了一个弱引用对象。

为什么要将键设置成为一个弱引用对象?

这是为了避免内存泄漏,防止ThreadLocal对象本身的内存泄漏。 因为ThreadLocalMap是在线程中的,如果不使用弱引用,当外部的ThreadLocal强引用消失后,Entry这里还在强引用ThreadLocal对象,而且仅有Entry还在强引用这个ThreadLocal对象,而我们此时无法将这个ThreadLocal删除了,这个导致这个对象无法被GC。因此设计为弱引用,解决了键的泄漏问题

内存泄漏

虽然官方将键设置为弱引用,为我们解决了键的泄漏问题,但是ThreadLocal依然会出现内存泄漏,其根本原因在于ThreadLocalMap中的Entry中键是弱引用,而值是强引用。这里的泄漏是值泄漏

在正常情况下:

  • ThreadLocal tl = new ThreadLocal();创建一个 tl对象。
  • 线程通过 tl.set(value)存储数据。
  • tl不再被需要时,tl变量置为 nulltl = null;)。
  • 由于 Entry 的 Key 是弱引用,在下次 GC 时,这个 Key 会被回收,Entry 的 Key 变为 null
  • 当下次操作 ThreadLocalMap(如 set、get、remove)时,Map 会自动清理这些 key==null的条目(惰性清理)。
  • 清理后,Value 的强引用断开,可以被正常回收。

在泄漏的情况(常见于线程池)::

  1. 强引用消失tl变量被置为 null,堆中的 ThreadLocal 对象只被 Entry 的 Key 弱引用着。
  2. Key 被回收:GC 发生时,由于弱引用的特性,堆中的 ThreadLocal 对象被回收。Entry 中的 Key 变为 null,但 Value 依然被 Entry 强引用
  3. 线程未终止:如果这个线程是线程池中的核心线程,它会一直存活(与线程池同生命周期),那么它的 ThreadLocalMap会一直存在。
  4. Value 无法释放:这个 key=null的 Entry 及其 Value 对象,由于线程的强引用链(Thread -> ThreadLocalMap -> Entry -> Value)一直存在,只要线程存活,即使你再也无法通过任何代码访问到这个 Value(因为 Key 没了,get不到),它也无法被回收。这就造成了内存泄漏
  5. 更糟的情况:如果这个 Value 本身又间接引用了其他大对象,会导致一连串的对象都无法被回收。

泄漏主要是ThreadLocalMap的惰性清理没有触发,导致Entry中的value没有释放。

ThreadLocal不支持继承

下面以一个例子来展示:

public static void main(String[] args) {
    //当前父线程设置
    ThreadLocal<String> threadLocal=new  ThreadLocal<String>();
    threadLocal.set("parent");
    Thread thread=new Thread(()->{
        System.out.println("子线程:"+threadLocal.get());
    });
    thread.start();
    System.out.println("父线程:"+threadLocal.get());
}

结果如下:

子线程:null
父线程:parent

同一个ThreadLocal变量在父线程中被设置值后, 在子线程中是获取不到的。因为main线程和它的子线程不是同一个线程,因此是获取不到的。

但是有些场景下,我们需要父线程设置了之后,在父线程中开启的任何子线程都可以继承父线程设置的变量。

InheritableThreadLocal类

InheritableThreadLocal就是为了解决上述问题而产生的:

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
 
    public InheritableThreadLocal() {}


    protected T childValue(T parentValue) {
        return parentValue;
    }

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

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

InheritableThreadLocal重写了createMap,现在第一次调用set方法的时候,创建的是当前线程的inheritableThreadLocals变量的实例,而不是threadLocals

与此同时,也重写了getMap方法,现在返回的是线程的inheritableThreadLocals而不是threadLocals

访问父线程变量的方法

回到Thread的构造方法中:

private Thread(ThreadGroup g, Runnable target, String name,
               long stackSize, AccessControlContext acc,
               boolean inheritThreadLocals) {
    if (name == null) {
        throw new NullPointerException("name cannot be null");
    }

    this.name = name;
    //获取创建该线程的当前线程,也就是要创建线程的父线程
    Thread parent = currentThread();
    SecurityManager security = System.getSecurityManager();
    if (g == null) {
    
        if (security != null) {
            g = security.getThreadGroup();
        }
        if (g == null) {
            g = parent.getThreadGroup();
        }
    }
    g.checkAccess();
    if (security != null) {
        if (isCCLOverridden(getClass())) {
            security.checkPermission(
                    SecurityConstants.SUBCLASS_IMPLEMENTATION_PERMISSION);
        }
    }

    g.addUnstarted();

    this.group = g;
    this.daemon = parent.isDaemon();
    this.priority = parent.getPriority();
    if (security == null || isCCLOverridden(parent.getClass()))
        this.contextClassLoader = parent.getContextClassLoader();
    else
        this.contextClassLoader = parent.contextClassLoader;
    this.inheritedAccessControlContext =
            acc != null ? acc : AccessController.getContext();
    this.target = target;
    setPriority(priority);
    //当要启用继承ThreadLocal且父线程的inheritableThreadLocals不为空
    if (inheritThreadLocals && parent.inheritableThreadLocals != null)
        //为创建的线程设置inhertiableThreadLocals
        this.inheritableThreadLocals =
            ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    /* Stash the specified stack size in case the VM cares */
    this.stackSize = stackSize;

    /* Set thread ID */
    this.tid = nextThreadID();
}

因此主要的实现在ThreadLocal.createInheritedMap(parent.inheritableThreadLocals)

static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}
private ThreadLocalMap(ThreadLocalMap parentMap) {
    Entry[] parentTable = parentMap.table;
    int len = parentTable.length;
    setThreshold(len);
    table = new Entry[len];

    for (Entry e : parentTable) {
        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++;
            }
        }
    }
}

这个ThreadLocalMap的私有构造方法:

  • 继承父线程的 ThreadLocal 值

  • 在创建子线程时,将父线程的 ThreadLocalMap中的数据浅拷贝到子线程

  • 主要用于实现 InheritableThreadLocal功能

下面关注一下childValue方法,它的功能是传入一个父线程的参数parentValue,然后返回一个子线程的参数childValue。其默认实现是:

protected T childValue(T parentValue) {
    return parentValue;
}

这个默认实现是将父线程的参数原封不动的返回作为子线程的参数。

因此上述的步骤表明,之所以子线程能够访问父线程的数据,是因为将父线程的数据浅拷贝了下来。

使用inheritableThreadLocal的示例

public static void main(String[] args) {
    //当前父线程设置
    ThreadLocal<String> threadLocal=new InheritableThreadLocal<String>();
    threadLocal.set("parent");
    Thread thread=new Thread(()->{
        System.out.println("子线程:"+threadLocal.get());
    });
    thread.start();
    System.out.println("父线程:"+threadLocal.get());
}

输出的结果:

子线程:parent
父线程:parent

值得注意的是,数据浅拷贝的操作仅仅会发生在构造线程的时候。因此

public static void main(String[] args) {
    //当前父线程设置
    ThreadLocal<String> threadLocal=new InheritableThreadLocal<String>();
    Thread thread=new Thread(()->{
        System.out.println("子线程:"+threadLocal.get());
    });
    threadLocal.set("parent");
    thread.start();
    System.out.println("父线程:"+threadLocal.get());
}

如果在构造线程之后,再设置InheritableThreadLocal中的值,那么子线程中无法获取到这个值。

父线程:parent
子线程:null

同样,如果在线程创建之后,更新父线程中的值,子线程也无法无法获取到最新的值。

public static void main(String[] args) {
    //当前父线程设置
    ThreadLocal<String> threadLocal=new InheritableThreadLocal<String>();
    threadLocal.set("parent");
    Thread thread=new Thread(()->{
        System.out.println("子线程:"+threadLocal.get());
        try {
            TimeUnit.SECONDS.sleep(1);
            System.out.println("子线程:"+threadLocal.get());
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
    });
    thread.start();
    System.out.println("父线程:"+threadLocal.get());
    threadLocal.set("parent-new");
    System.out.println("父线程:"+threadLocal.get());
}

结果:

父线程:parent
子线程:parent
父线程:parent-new
子线程:parent