ThreadLocal解析

86 阅读5分钟

ThreadLocal是什么?

ThreadLocal是线程本地变量,若创建了一个ThreadLocal变量,则访问这个变量的每个线程都有这个变量的本地拷贝,当多线程操作这个变量时,实际操作的是自己本地内存的变量,从而起到线程隔离作用,避免线程安全问题。

ThreadLocal 使用场景

作为副本,当某数据以线程作为作用域并且不同线程有不同副本时,可以考虑使用 保存线程的上下文信息,在任意需要地方都可获取,避免显式传参 解决线程安全问题,避免某些情况需要考虑线程安全必须同步带来的性能损失

ThreadLocal内部实现

每个线程都有一个 ThreadLocalMap(ThreadLocal内部类),Map 中元素的键为 ThreadLocal,而值对应线程的变量副本。Map 是数组实现,使用线性探测解决hash冲突,需要手动调用set、get、remove防止内存泄漏。

ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。

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

一个线程内可以存在多个 ThreadLocal 对象,所以其实是 ThreadLocal 内部维护了一个 Map ,这个 Map 不是直接使用的 HashMap ,而是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类。而我们使用的 get()、set() 方法其实都是调用了这个ThreadLocalMap类对应的 get()、set() 方法。

调用 threadLocal.set() --> 调用 getMap(Thread) --> 返回当前线程的 ThreadLocalMap < ThreadLocal, value >-->map.set(this, value),this 是 ThreadLocal

调用 get() --> 调用getMap(Thread) --> 返回当前线程的 ThreadLocalMap<ThreadLocal, value>-->map.getEntry(this),返回value。

这个储值的Map并非ThreadLocal的成员变量,而是java.lang.Thread 类的成员变量。ThreadLocalMap实例是作为java.lang.Thread的成员变量存储的,每个线程有唯一的一个threadLocalMap。这个map以ThreadLocal对象为key,”线程局部变量”为值,所以一个线程下可以保存多个”线程局部变量”。

对ThreadLocal的操作,实际委托给当前Thread,每个Thread都会有自己独立的ThreadLocalMap实例,存储的仓库是Entry[] table;Entry的key为ThreadLocal,value为存储内容;因此在并发环境下,对ThreadLocal的set或get,不会有任何问题。

由于Tomcat线程池的原因,我最初使用的”线程局部变量”保存的值,在下一次请求依然存在(同一个线程处理),这样每次请求都是在本线程中取值。所以在线程池的情况下,处理完成后主动调用该业务treadLocal的remove()方法,将”线程局部变量”清空,避免本线程下次处理的时候依然存在旧数据。

ThreadLocal 并不是用来解决共享资源的多线程访问的问题,因为每个线程中的资源只是副本,并不共享。因此ThreadLocal适合作为线程上下文变量,简化线程内传参。

//ThreadLocal
public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
//ThreadLocal.ThreadLocalMap
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;
        }

  // 代表key弱引用已被回收
        
        if (k == null) {
          // 如果被回收 需要替换
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;

            // Back up to check for prior stale entry in current run.
            // We clean out whole runs at a time to avoid continual
            // incremental rehashing due to garbage collector freeing
            // up refs in bunches (i.e., whenever the collector runs).
           // 向前遍历 检查是否有已经被回收的key
            int slotToExpunge = staleSlot;
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;

            // Find either the key or trailing null slot of run, whichever
            // occurs first
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();

                // If we find key, then we need to swap it
                // with the stale entry to maintain hash table order.
                // The newly stale slot, or any other stale slot
                // encountered above it, can then be sent to expungeStaleEntry
                // to remove or rehash all of the other entries in run.
              // 遇到相同的需要替换value
                if (k == key) {
                    e.value = value;

                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    // Start expunge at preceding stale entry if it exists
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                // If we didn't find stale entry on backward scan, the
                // first stale entry seen while scanning for key is the
                // first still present in the run.
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            // If key not found, put new entry in stale slot
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            // If there are any other stale entries in run, expunge them
            // 如果有其他的过期对象会清理
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

ThreadLocal造成的内存泄露

原因

在jvm中栈内存为线程私有,其中存储对象引用。堆内存为线程共享,存储了对象实例。所以,栈中存储了ThreadLocal、Thread的引用,堆中存储了它们的具体实例。

图片.png

ThreadLocalMap中使用的 key 为 ThreadLocal 的弱引用。弱引用很容易被回收,如果ThreadLocal(ThreadLocalMap的Key)被垃圾回收器回收了,但是ThreadLocalMap生命周期和Thread是一样的,它这时候如果不被回收,就会出现这种情况:ThreadLocalMap的key没了,value还在,这就会造成了内存泄漏问题。

解决内存泄漏

很简单,使用完ThreadLocal后,及时调用remove()方法释放内存空间。

ThreadLocal<String> localVariable = new ThreadLocal();
try {
    localVariable.set("---”);
    ...
} finally {
    localVariable.remove();
}

为什么key要设计成弱引用

key设计成弱引用同样是为了防止内存泄漏。 若key被设计成强引用,如果ThreadLocal Reference被销毁,此时它指向ThreadLocal的强引用就没有了,但是此时key还强引用指向ThreadLocal,就会导致ThreadLocal不能被回收,这时候就发生了内存泄漏的问题。

ThreadLocalMap 和 HashMap 区别

HashMap 的数据结构是数组+链表,HashMap 是通过链地址法解决hash 冲突的问题,HashMap 里面的Entry 内部类的引用都是强引用。 ThreadLocalMap的数据结构仅仅是数组,ThreadLocalMap 是通过开放地址法来解决hash 冲突的问题,ThreadLocalMap里面的Entry 内部类中的key 是弱引用,value 是强引用