ThreadLocal

December 21, 2021

作用

同一个 ThreadLocal 对象可以管理多个线程中的对象,且线程之间数据是隔离的,互不干扰。

有两个主要应用场景:

  • 每个线程需要自己拥有一份独立的对象,比如 SimpleDateFormat,它是线程不安全的
  • 在同一线程中传递全局变量,比如会话信息、用户权限等等,就不需要为每个方法都加一个入参了



示例

演示一下全局变量的用法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class UserHolder{
    public static final ThreadLocal<User> USER_HOLDER = new ThreadLocal<>();
}

//以下方法在不同类中

public void method1(Long id){
    User user = findUserById(id);
    UserHolder.USER_HOLDER.set(user);
}

public void method2(){
    User currentUser = UserHolder.USER_HOLDER.get();
    //业务逻辑
}

public void method3(){
    User currentUser = UserHolder.USER_HOLDER.get();
    //业务逻辑
}

只要是在同一个线程中,method2method3 取到的都是同一个用户。

不同线程取到的是不同用户。




原理

本质:每个线程会维护自己的一个 map:ThreadLocalMap

1
ThreadLocal.ThreadLocalMap threadLocals = null;

看下 ThreadLocal set() 方法的源码:

1
2
3
4
5
6
7
8
public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
1
2
3
ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

set 时会先获取当前线程,然后取当前线程中的 ThreadLocalMap,把值 set 进去。

每个线程使用 threadLocal.get() 时是在自己的map中获取,就是这样做到线程隔离的。

可看到set时,key 是 this,即 threadLocal 对象,这么设计是因为 threadLocalMap 每个线程只有一个,而一个线程可以有多个 threadLocal 对象,如果需要多个全局变量的话。

再继续看 threadLocalMap 中的 set 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

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

ThreadLocalMap 类似于 HashMap,不同在于 ThreadLocalMap 只有数组(初始容量为16),没有链表,通过 key.threadLocalHashCode & (len-1) 计算出数组下标存储位置。

那没有链表,如何解决冲突的呢?

其实很简单,就往后顺延就可以了。如果下标位置中没值,直接存储。如果有值,判断 key 是否相等,相等的话刷新,不等的话顺延一个位置重复之前的步骤。

ThreadLocalMap 有一个阈值:数组容量的$2/3$,当超过这个阈值的时候会进行扩容,扩容为原来容量的2倍。

由于 ThreadLocalMap 解决冲突的办法是顺延法,相对于 HashMap 的链表法在大数据量时效率较低。所以它不适合用来存储大量的值,它的设计初衷也只是为了解决全局变量传递的问题,并不是用来做一个容器。





内存泄露问题

内存泄露指的是已分配出去的内存,回收不了,就不能再利用,这块内存就像凭空消失了一样。内存泄露积累下来,还会造成内存空间耗尽,导致 OOM。

先看一下 ThreadLocalMapEntry 的数据结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

可看到 Entry 是一个 WeakReference,在介绍 WeakReference 之前先说下垃圾回收。

JVM 的垃圾回收算法有两种:引用计数法和可达性分析算法,从各自名字就能看出大概是什么意思。目前主流垃圾回收器采取的是可达性分析算法,该算法的本质是会从一系列 “GC Roots” 合集出发,探索所有能被该合集引用到的对象,并将其加入到该集合中,这个过程称为 “标记”,然后继续探索。最终,未被探索到的对象便是 “死亡” 对象,便会被垃圾回收器回收。

所以一个对象会不会被 GC 就看它能不能被探索到,换句话说有没有引用指向它,是不是可达的。GC 的工作会由 JVM 自动来完成,但程序员也可以通过代码的方式(通过定义不同类型的对象引用 )来影响一个对象的生命周期,这就是 Reference 的作用

Java 中的四种引用:

  • 强引用
  • 软引用
  • 弱引用
  • 虚引用

threadlocalmap 的 key 是一个弱引用,发生gc就会被回收,这样就出现了 key 为 null 的 entry,而如果使用的是线程池,线程一直存在,就会存在一个强引用:thread -> threadlocalmap -> entry -> value,导致 value 不能被回收。

使用完以后记得 remove,清理 key 为 null 的 entry