ThreadLocal

简介

利用synchronzed或者lock解决线程安全的问题时,会让未获取到锁的线程进行阻塞等待。线程安全问题的核心在于多个线程会对同一个临界区共享资源进行操作,那么,如果每个线程都使用自己的“共享资源”,各自用各自的,又互相不影响,让多个线程间达到隔离的状态,这样就不会出现线程安全的问题。

于是ThreadLocal应运而生,使每个线程都拥有某个变量副本,达到人手一份的效果,从而避免共享资源的竞争。

ThreadLocal的两个作用

  1. 让某个需要用到的对象在线程间隔离(每个线程都有自己的独立的对象);
  2. 在任何方法中都可以轻松获取到该对象;

根据共享对象的生成时机不同,选择 initialValue 或 set 来保存对象

场景一:initialValue

​ 在ThreadLocal第一次get的时候把对象给初始化出来,对象的初始化时机可以由我们控制;

场景二:set

​ 如果需要保存到ThreadLocal里的对象的生成时机不由我们随意控制,例如拦截器生成的用户信息,则使用 ThreadLocal.set,以便后续使用;

源码分析

  1. Thread ThreadLocal ThreadLocalMap 之间的关系

    Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,即每个线程有一个自己的ThreadLocalMap。

    ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocal,value为代码中放入的值(实际上key并不是ThreadLocal本身,而是它的一个弱引用)。

    每个线程在往ThreadLocal里放值的时候,都会往自己的ThreadLocalMap里存,读也是以ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。

    ThreadLocalMap有点类似HashMap的结构,只是HashMap是由数组+链表实现的,而ThreadLocalMap中并没有链表结构。

    我们还要注意Entry, 它的key是ThreadLocal<?> k,继承自WeakReference, 即弱引用类型。

  2. set():设置在当前线程中 threadLocal 变量的值

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

    总结:通过当前线程对象thread获取该thread所维护的threadLocalMap,若threadLocalMap不为null,则以threadLocal实例为key,值为value的键值对存入threadLocalMap,若threadLocalMap为null的话,就新建threadLocalMap然后在以threadLocal为键,值为value的键值对存入即可。

  3. 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();
    }

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

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

  4. initialValue()

    protected T initialValue() {
    return null;
    }

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

  5. remove()

    public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
    m.remove(this);
    }
  6. ThreadLocalMap的hash冲突

    虽然ThreadLocalMap中使用了黄金分隔数来作为hash计算因子,大大减少了hash冲突的概率,但是仍然会存在冲突。

    此时就会线性向后查找,一直找到Entry为null的槽位才会停止查找,将当前元素放入此槽位中。(线性探测)

适用场景

  1. 每个线程需要一个独享的对象(通常是工具类,典型需要使用的类有 SimpleDateFormat 和 Random);

    重写了 initialValue() 方法;

  2. 每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦;

    • 用 ThreadLocal 保存一些业务内容(用户权限信息,从用户系统获取到的用户名,userID 等)
    • 这些信息在用一个线程内相同,但是不同的线程使用的业务内容是不同的;
    • 在线程生命周期内,都通过这个静态 ThreadLocal 实例的 get() 方法取得自己 set() 过的那个对象,避免了这个对象(例如user对象)作为参数传递的麻烦;
    • 强调的是同一个请求内(同一个线程内)不同方法间的共享
    • 不需要重写 initialValue() 方法,但是必须手动调用 set() 方法

ThreadLocal带来的好处

  • 线程安全;

  • 不需要加锁,提高执行效率;

  • 更高效利用内存,节省开销;

  • ThreadLocal使得代码耦合度更低,更优雅:免去传参的繁琐,无论是场景一还是场景二,都可以在任何地方直接通过ThreadLocal拿到,再也不需要每次都传同样的参数。

可能会带来的问题

内存泄漏的原因

ThreadLocal在ThreadLocalMap中是以一个弱引用身份被Entry中的Key引用的,因此如果ThreadLocal没有外部强引用来引用它,那么ThreadLocal会在下次JVM垃圾收集时被回收。这个时候就会出现Entry中Key已经被回收,出现一个null Key的情况,外部读取ThreadLocalMap中的元素是无法通过null Key来找到Value的。因此如果当前线程的生命周期很长,一直存在,那么其内部的ThreadLocalMap对象也一直生存下来,这些null key就存在一条强引用链的关系一直存在:Thread –> ThreadLocalMap–>Entry–>Value,这条强引用链会导致Entry不会回收,Value也不会回收,但Entry中的Key却已经被回收的情况,造成内存泄漏。

但是JVM团队已经考虑到这样的情况,并做了一些措施来保证ThreadLocal尽量不会内存泄漏:在ThreadLocal的get()、set()、remove()方法调用的时候会清除掉线程ThreadLocalMap中所有Entry中Key为null的Value,并将整个Entry设置为null,利于下次内存回收。

private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;

// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;

// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;

// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}

为什么不使用强引用?

如果key使用强引用,当引用的ThreadLocal对象被回收,但ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。

InheritableThreadLocal

使用ThreadLocal的时候,在异步场景下,无法给子线程共享父线程中创建的线程副本数据。此时可以使用InheritableThreadLocal。

https://blog.csdn.net/ThinkWon/article/details/102508381

Author: Jiayi Yang
Link: https://jiayiy.github.io/2020/08/01/ThreadLocal/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.