ThreadLocal

简介

ThreadLocal使用场合主要解决多线程中数据数据因并发产生不一致问题。ThreadLocal为每个线程的中并发访问的数据提供一个副本,通过访问副本来运行业务,这样的结果是耗费了内存,单大大减少了线程同步所带来性能消耗,也减少了线程并发控制的复杂度。
ThreadLocal不能使用基本数据类型,只能使用Object类型。

实现

ThreadLocal就是绑定在线程上的,可以让每个线程可以存储隔离的线程安全的数据。

其实现就是在Thread类的本地变量中存储ThreadLocal.ThreadlocalMap一个成员变量。

public class Thread implements Runnable {
   ......(其他源码)
    /* 
     * 当前线程的ThreadLocalMap,主要存储该线程自身的ThreadLocal
     * 本文主要讨论的就是这个ThreadLocalMap
     */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal,自父线程集成而来的ThreadLocalMap,
     * 主要用于父子线程间ThreadLocal变量的传递
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}

然后在设置的时候直接使用当前ThreadLocal变量为key到Thread中的ThreadLocalMap中取值,可以看到这里的ThreadlocalMap是一个hash结构

public class ThreadLocal{
    public void set(T value) {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null)
                //key为自己声明的threadLocal
                map.set(this, value);
            else
                //创建一个ThreadLocalmap
                createMap(t, value);
    }
    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;
            }
        }
        //setInitialValue就是可以在没有设置当前线程的threadlocal的初始值,可以自己定义默认为null
        return setInitialValue();
    }
}

InheritableThreadLocal子线程共享

InheritableThreadLocal主要用于子线程创建时,需要自动继承父线程的ThreadLocal变量,方便必要信息的进一步传递。

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    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);
    }
}

在新建线程的init方法中如果inheritThreadLocals为true则会继承,默认是true.并且是使用InheritableThreadLocal的才能继承,判断条件就是thread.inheritableThreadLocals是不是为null,而InheritableThreadLocal.createMap就是将threadlocalMap重写为了inheritableThreadLocals

示例

@Slf4j
public class InheritableThreadLocalSample {
  private static InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
  private static ThreadLocal<String> threadLocal = new ThreadLocal<>();

  public static void main(String[] args) {
    Thread parent = new Thread(() -> {
      threadLocal.set("uncle");
      inheritableThreadLocal.set("uncle");
      log.info("父线程的值:threadLocal:{},inheritableThreadLocal:{}", threadLocal.get(),inheritableThreadLocal.get());
      Thread child = new Thread(() -> {
        log.info("父线程的值:threadLocal:{},inheritableThreadLocal:{}", threadLocal.get(),inheritableThreadLocal.get());
      },"child thread");
      child.start();
    },"parent thread");
    parent.start();
  }

}

输出

2020-12-17 17:11:59 [parent thread] INFO  com.unclezs.samples.java.thread.threadlocal.InheritableThreadLocalSample #lambda$main$1:18 - 父线程的值:threadLocal:uncle,inheritableThreadLocal:uncle
2020-12-17 17:11:59 [child thread] INFO  com.unclezs.samples.java.thread.threadlocal.InheritableThreadLocalSample #lambda$null$0:20 - 父线程的值:threadLocal:null,inheritableThreadLocal:uncle

内存泄漏问题

使用的时候注意ThreadLocal用完之后没用的要释放掉,以免造成内存泄漏。为什么不释放就可能照成内存泄漏呢?JVM不会回收吗?

首先看看ThreadLocalMap的结构,也是哈希结构,不过是开放地址法解决冲突的

static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
        Object value;
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    private void set(ThreadLocal<?> key, Object value) {
        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();
            //已经存在直接设置value
            if (k == key) {
                e.value = value;
                return;
            }
            //entity存在,但是弱引用key threadlocal已经被回收了 获取到的key为null
            if (k == null) {
                //直接覆盖这个已经被回收的key的位置 放入新的
                replaceStaleEntry(key, value, i);
                return;
            }
        }
        //不存在冲突
        tab[i] = new Entry(key, value);
        int sz = ++size;
        //先清理已经被回收了的,范围是keyindex->size 判断是否需要扩容
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }
}

所以这里可以看出来,首先Entity是个弱引用对象,引用对象为ThreadLocal,所以ThreadLocal只要发生了GC了就会被回收,这个时候Entity中的样子就变成了:

key=null,value=xx

虽然key threadlocal被回收了,但是value并没有被回收,而且entity也还是在table数组中强引用占用着内存。所以如果不处理这些数据就有可能造成内存泄漏

你可以想象,我们平时使用线程池,这个线程复用技术,因为ThreadLocal实现还是在Thread中放一个ThreadLocalMap,如果复用了Thread也代表会重复用这个ThreadLocalMap,如果一个线程在使用完ThreadLocal之后,另一个线程又拿过来用,导致ThreadLocalMap里面的一直没有正常释放,甚至越放越多。最终导致每个线程里面的ThreadLocalMap都有大量数据,导致内存泄漏

虽然ThreadLocalMap会在set,get以及resize等方法中对stale slots做自动删除(set以及get不保证所有过期slots会在操作中会被删除,而resize则会删除threadLocalMap中所有的过期slots)。但是最佳的实践还是用put就有对应的remove才行

比如用户登录进行put,用户注销进行remove。

FastThreadLocal

这个为什么说快呢?难道Hash存取数据还不够快吗?还有更快的吗?那也只有数组了。

这个实现是在Netty中的,用数组代替了 Hash+开发地址法。因为ThreadLocalMap是放在线程的成员变量中的,所以Netty自己继承出来一个FastThread,里面就包含了个InternalThreadLocalMap,别看名字是map其实是数组实现的。

所以具体要使用FastThreadLocal需要使用FastThread才行。

更多了解去 Netty进阶:自顶向下解析FastThreadLocal

评论

博客
分类
标签
归档
关于