Java中ThreadLocal详解(一篇就够了)

前言

ThreadLocal直译为线程局部变量,或许将它命名为ThreadLocalVariable更为合适。其主要作用就是实现线程本地存储功能,通过线程本地资源隔离,解决多线程并发场景下线程安全问题。

ThreadLocal

接下来,通过ThreadLocal的使用案例、应用场景、源码分析来进行深层次的剖析,说明如何避免使用中出现问题以及解决方案。

使用案例

前面提到关于ThreadLocal的线程隔离性,通过下面一个简单的例子来演示ThreadLocal的隔离性。

package com.starsray.test.tl;

import java.util.ArrayList;
import java.util.List;

public class ThreadLocalTest {
    // 声明一个ThreadLocal成员变量
    private final ThreadLocal<Person> tl = new ThreadLocal<>();
    // 声明一个List作为参照对象
    private final List<Person> list = new ArrayList<>();


    public static void main(String[] args) {
        new ThreadLocalTest().test();
    }

    public void test() {
        // 创建测试Person对象
        Person person = new Person();
        person.setName("张三");
        person.setAge(24);
        // 创建线程一:再启动1s后,分别添加person对象到tl、list对象中
        new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            tl.set(person);
            list.add(person);
            System.out.println(Thread.currentThread().getName() + " [thread] get():" + tl.get());
            System.out.println(Thread.currentThread().getName() + " [list] get():" + list.get(0));
        },"thread-1").start();

        // 创建线程二:在启动2s后,分别去tl、list对象中取person对象
        new Thread(() -> {
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + " [thread] get():" + tl.get());
            System.out.println(Thread.currentThread().getName() + " [list] get():" + list.get(0));
        },"thread-2").start();
    }
    // 测试静态内部类Person
    static class Person {
        private String name;
        private int age;

        public void setName(String name) {
            this.name = name;
        }

        public void setAge(int age) {
            this.age = age;
        }

        @Override
        public String toString() {
            return "Person{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
}

案例中使用两个线程同时对List和ThreadLocal对象进行操作,通过对照实验,从输出结果可以看到,ThreadLocal实现了线程间数据隔离,这也说明每一个Thread对象维护了自己的一份数据。

hread-1 [thread] get():Person{name='张三', age=24}
thread-1 [list] get():Person{name='张三', age=24}
thread-2 [thread] get():null
thread-2 [list] get():Person{name='张三', age=24}

应用场景

针对ThreadLocal而言,由于其适合隔离、线程本地存储等特性,因此天然的适合一些Web应用场景,比如下面所列举的例子:

  • 代替参数显式传递(很少使用)
  • 存储全局用户登录信息
  • 存储数据库连接,以及Session等信息
  • Spring事务处理方案

源码分析

通过使用案例的展示,接下来对ThreadLocal的实现原理进行简单分析。

WeakReference

在对ThreadLocal的源码展开描述之前,首先简单提一下Java中四种引用类型,强、软、若、虚之一的弱引用,这四种引用关系引用程度依次降低。Java中弱引用通过WeakReference表示,在JDK1.2引入。

public class WeakReference<T> extends Reference<T> {
    // 创建一个给定类型的对象弱引用
    public WeakReference(T referent) {
        super(referent);
    }
    // 创建一个给定类型的对象弱引用,并注册到队列
    public WeakReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }
}

弱引用用来描述非必须对象,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。如果发生垃圾收集,无论内存空间是否满足,都会回收掉被弱引用关联的对象。
例如下面代码模拟,为了便于模拟出效果,指定虚拟机启动参数:-Xms4m -Xmx4m

public class WeakRefTest {
    @Override
    protected void finalize() {
        System.out.println("gc");
    }
    public static void main(String[] args) {
        for (int i = 0; i < 500; i++) {
            WeakRefTest weakRefTest = new WeakRefTest();
            new WeakReference<>(weakRefTest);
            if (i >= 450) {
                System.gc();
            }
        }
    }
}
  • finalize()是Object方法,当虚拟机在回收对象时,允许执行完该方法后再进行回收
  • System.gc()会通知虚拟机进行垃圾回收,并不会立即进行垃圾回收

执行结果:

gc
...

关于弱引用的特性,为什么ThreadLocal中要使用弱引用来维护一个对象,后面会继续进行描述。

ThreadLocalMap

ThreadLocalMap是ThreadLocal的一个静态内部类。每一个Thread对象实例中都维护了ThreadLocalMap对象,对象本质存储了一组以ThreadLocal为key(this对象实际使用的是唯一threadLocalHashCode值),以本地线程包含变量为value的K-V键值对。
在ThreadLocalMap内部还维护了一个Entry静态内部类,该类继承了WeakReference,并指定其所引用的泛型类为ThreadLocal类型。Entry是一个键值对结构,使用ThreadLocal类型对象作为引用的key。

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之所以使用数组结构,一个Thread在运行的过程中会存在多个ThreadLocal对象的场景,ThreadLocalMap作为ThreadLocal的静态内部类,需要维护多个ThreadLocal对象所存储的value值。

// 初始化默认容量为 16
private static final int INITIAL_CAPACITY = 16;
// 数据存储结构底层实现为Entry数组,其长度必须为2的倍数
private Entry[] table;
// table中Entry的实际数量,初始值为0
private int size = 0;
// 存储的阈值
private int threshold; // Default to 0
// resize扩容阈值加载因子为2/3
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

整个ThreadLocal类中核心内容都是对ThreadLocalMap进行操作,而ThreadLocalMap的核心内容都是围绕Entry组成的Map存储结构进行操作。关于ThreadLocal、ThreadLocalMap、Entry之间的关系如图所示:
ThreadLocal.png
ThreadLocal对象是当前线程的ThreadLocalMap的访问入口,Thread类中维护了两个关于ThreadLocalMap的成员变量。

// ThreadLocal变量
ThreadLocal.ThreadLocalMap threadLocals = null;

// InheritableThreadLocal变量,该类继承自ThreadLocal
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

threadLocals在Thread类中作为成员变量,初始化线程对象时并不会被赋予值,只有在使用ThreadLocal时进行赋值。查看ThreadLocal中的get方法

public T get() {
    // 获取当前操作线程
    Thread t = Thread.currentThread();
    // 调用getMap方法,返回当前线程的实例变量threadLocals值
    ThreadLocalMap map = getMap(t);
    // 如果返回map不为空,返回map中所存储的以当前ThreadLocal对象为key的值
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 如果map为空进行map值的初始化
    return setInitialValue();
}

ThreadLocalMap getMap(Thread t) {
    // 返回传入线程(当前线程)中成员变量的threadLocals值
    return t.threadLocals;
}

private T setInitialValue() {
    // 调用initialValue()方法设置初始值,默认不设置任何值,可以在创建ThreadLocal
    // 对象时被重写进行初始化,只会进行一次初始化。
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

void createMap(Thread t, T firstValue) {
    // 初始化当前线程对象实例变量threadLocals的值,Map所对应的key为当前ThreadLocal对象
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

接下来查看set方法

public void set(T value) {
    // 获取当前线程对象
    Thread t = Thread.currentThread();
    // 调用getMap方法,传入当前对象的值,获取当前线程的实例变量threadLocals值
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        // 如果map为空,创建ThreadLocalMap
        createMap(t, value);
}

而inheritableThreadLocals会在创建线程时,根据线程构造方法传参,确定是否进行初始化。

// 该init方法为Thread内部线程初始化方法,inheritThreadLocals是否继承父类变量,默认false
private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals);
// 如果inheritThreadLocals为true并且parent(为当前线程,视为要被继承线程的父线程)
// 的ThreadLocal不为null,调用createInheritedMap方法进行继承初始化
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

// 为子线程创建一个新的ThreadLocalMap并初始化parentMap中的变量实现继承
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}

threadLocalHashCode

在ThreadLocal的成员变量中包含了threadLocalHashCodeHASH_INCREMENT两个成员变量:

private final int threadLocalHashCode = nextHashCode();

// 连续生成的哈希码之间的差异
// 将隐式顺序线程本地 ID 转换为接近最佳传播的乘法哈希值,用于二次幂大小的表
private static final int HASH_INCREMENT = 0x61c88647;

关于threadLocalHashCode的解释:

ThreadLocals 依赖于附加到每个线程(Thread.threadLocals 和inheritableThreadLocals)的每线程线性探针哈希映射。 ThreadLocal 对象充当键,通过 threadLocalHashCode 进行搜索。这是一个自定义哈希码(仅在 ThreadLocalMaps 中有用),它消除了在相同线程使用连续构造的 ThreadLocals 的常见情况下的冲突,同时在不太常见的情况下保持良好行为。

HASH_INCREMENT是用来计算下一个哈希码(threadLocalHashCode)的哈希魔数,源码中用十六进制表示,其对应的十进制数为1640531527,至于为什么是这个数值并不是偶然,而是这个数在有符号的int范围内是黄金分割数。
如下代码所示,输出的结果刚好是-1640531527,也就是说是32位有符号整数的黄金分割值。

public static void main(String[] args) {
    long c = (long) ((1L << 32) * (Math.sqrt(5) - 1) / 2);
    System.out.println(c);
    //强制转换为带符号为的32位整型,值为-1640531527
    int i = (int) c;
    System.out.println(i);
    System.out.println(Integer.MAX_VALUE);
}

ThreadLocal在ThreadLocalMap中是根据ThreadLocal对象的threadLocalHashCode进行索引的,查看下面一段源码,从源码可以看到在Entry表里求下标的算法为:
哈希key:keyIndex = key.threadLocalHashCode & (table.length - 1);

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

哈希key的求值也可以看作:keyIndex = ((i + 1) * HASH_INCREMENT) & (length - 1)i为ThreadLocal实例的个数,HASH_INCREMENT是哈希魔数0x61c88647lengthThreadLocalMap中可容纳的Entry的容量。初始容量为16,扩容后总是2的幂次方。
下面做个测试为什么使用HASH_INCREMENT作为魔数,以及求取下标的算法巧妙之处:

public class TestThreadLocalHashCode {

    public static void main(String[] args) {
        hashCode(4);
        hashCode(16);
        hashCode(32);
        hashCode(64);
    }

    private static void hashCode(int capacity) {
        final int HASH_INCREMENT = 0x61c88647;
        final AtomicInteger nextHashCode = new AtomicInteger(HASH_INCREMENT);
        int keyIndex;
        for (int i = 0; i < capacity; i++) {
            keyIndex = nextHashCode.getAndAdd(HASH_INCREMENT) & (capacity - 1);
            // keyIndex = ((i + 1) * HASH_INCREMENT) & (capacity - 1);
            System.out.print(keyIndex + " ");
        }
        System.out.println();
    }
}

在不触发二次扩容的场景下,每个ThreadLocalMap中的元素分别为4,16,32,64,输出结果:

3 2 1 0 
7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0 
7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0 
7 14 21 28 35 42 49 56 63 6 13 20 27 34 41 48 55 62 5 12 19 26 33 40 47 54 61 4 11 18 25 32 39 46 53 60 3 10 17 24 31 38 45 52 59 2 9 16 23 30 37 44 51 58 1 8 15 22 29 36 43 50 57 0 

每组测试在进行散列算法后刚好填满了整个容器,实现了完美散列,这使得ThreadLocal在使用的过程中可以尽可能高的提高在ThreadLocalMap中获取元素的命中率,提高了ThreadLocal在使用中的效率。
此外从输出结果来看,ThreadLocal中使用了斐波那契散列法,保证哈希表的离散度。它选用的乘数值即是2^32的黄金分割比0x61c88647

注意事项

ThreadLocal提供了便利的同时当然也需要注意在使用过程中的一些细节问题。下面进行简单总结

异步调用

ThreadLocal默认情况下不会进行子线程对父线程变量的传递性,在开启异步线程的时候需要注意这一点,关于这一点可以通过Thread类构造方法提供的inheritThreadLocals参数进行封装,或者使用Spring根据装饰器模式进行封装的TaskDecorator类实现跨线程传递方法。

线程池问题

线程池中线程调用使用ThreadLocal 需要注意,由于线程池中对线程管理都是采用线程复用的方法,在线程池中线程非常难结束甚至于永远不会结束,这将意味着线程持续的时间将不可预测。另外重复使用可能导致ThreadLocal
对象未被清理,在ThreadLocalMap中进行值操作时被覆盖,或取到旧值。如下代码所示:

package com.starsray.test.thread;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

public class ThreadLocalPoolTest {
    private final static ThreadLocal<AtomicInteger> tl = ThreadLocal.withInitial(() -> new AtomicInteger(0));

    static class Task implements Runnable{

        @Override
        public void run() {
            System.out.println(tl.get().incrementAndGet());
        }
    }
    
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(2);
        for (int i = 0; i < 5; i++) {
            pool.execute(new Task());
        }
        pool.shutdown();
    }
}

期待的输出结果应该是1,实际输出结果,由于超出后线程被复用,输出结果也会取到旧值。

1
1
2
2
3
3

当然,如果必须要在线程池中使用ThreadLocal也不是不能使用,在线程池类ThreadPoolExecutor中定义了钩子函数,可以在初始化或者任务执行完做特殊处理,如初始化ThreadLocal或者记录日志。重写beforeExecute方法:

protected void beforeExecute(Thread t, Runnable r) { }

内存泄露

ThreadLocal对象不仅提供了get、set方法,还提供了remove方法。虽然get、set已经对空值进行清理,但在实际使用时,手动调用remove方法养成良好的编程习惯是非常有必要的。
ThreadLocal中主要的存储单元Entry类继承了WeakReference,该类的引用在虚拟机进行GC时会被进行清理,但是对于value如果是强引用类型,就需要进行手动remove,避免value的内存泄露。关于引用关系参考下图所示:
image

关于内存泄露这块的重点在于两部分:

  • ThreadLocal被一强(tl = new强引用)一弱(WeakReference<ThreadLocal<?>>)两部分引用,强引用可以通过编码解决(tl = null),而弱引用部分在GC时会自动清理掉key部分的引用。
  • 关于value部分的引用,如果是强引用类型的value通过remove方法可以清理,避免内存泄露。

具体细节查看remove部分的源码:

public void remove() {
    // 获取当前线程中threadLocals对应的ThreadLocalMap
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        // 如果ThreadLocalMap不为空,继续调用remove(this)方法
        m.remove(this);
}

查看remove(this)具体内容

private void remove(ThreadLocal<?> key) {
    // 创建新都tab数组,引用指向当前ThreadLocal对象中的table
    Entry[] tab = table;
    // tab的长度
    int len = tab.length;
    // 根据当前ThreadLocal对象的唯一threadLocalHashCode值并通过与操作
    // 获取当前ThreadLocal对象在table中value所在的下标值i
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        // 对table进行遍历,如果Entry所对应的key为当前ThreadLocal对象,执行clear方法
        if (e.get() == key) {
            // 将引用置为null
            e.clear();
            // 清楚陈旧的键值对
            expungeStaleEntry(i);
            return;
        }
    }
}

查看clear()方法,Clear方法位于Reference类中,由于Entry类继承了WeakReference(继承WeakReference),此处的clear属于多态的应用。

public void clear() {
    this.referent = null;
}

接下来expungStaleEntry(i)方法则是整个remove的核心逻辑了,这里首先再明确以下两个变量的意义:

  • size:前面提到size是Entry的数量,即ThreadLocal中成员变量table的实际键值对数量
  • i:table中与当前ThreadLocal对象相匹配的Entry的key值的下标
private int expungeStaleEntry(int staleSlot) {
    // 创建新都tab数组,引用指向当前ThreadLocal对象中的table
    Entry[] tab = table;
    // tab的长度
    int len = tab.length;

    // 将tab中下标staleSlot(i)对应的value引用置为null
    tab[staleSlot].value = null;
    // 将tab中下标staleSlot的Entry置为null
    tab[staleSlot] = null;
    // Entry对应的长度减1
    size--;

    // Rehash until we encounter null 直到遇到null开始rehash
    Entry e;
    int i;
    // 从staleSlot后以索引开始遍历,直到遇到某个Entry不为空为止
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        // 获取Entry对应的ThreadLocal对象的引用key值
        ThreadLocal<?> k = e.get();
        if (k == null) {
            // 如果为空,将value和键值对同时置空,size减1
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            // 如果k不为null,说明弱引用未被GC回收,获取table中k对应的下标
            int h = k.threadLocalHashCode & (len - 1);
            // 判断传入下标,与当前k对象的下标是否一直
            if (h != i) {
                // 如果不一致,需要对tab中的值进行更新,直接清空
                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)
                    // 采用R算法的变种,从当前h开始寻找一个为null的值存储e
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    // 返回第一个entry为null的下标
    return i;
}

关于expungeStaleEntry中原作者对关键地方进行了英文注释,源码提及了Knuth 6.4 Algorithm R算法,R算法主要说明了如何从使用线性探测的散列表中删除一个元素。
与Knuth 6.4算法R不同,这里必须扫描到null,可能出现空的Entry,多个条目可能已经过时,由于不使用引用队列,因此只有在表开始空间不足时才能保证删除过时的条目。

总结

Thread对象中通过维护了一个ThreadLocal.ThreadLocalMap类型的threadLocals变量实现线程间变量隔离,并维护了一个ThreadLocal.ThreadLocalMap类型的inheritableThreadLocals变量实现线程间变量的继承,是否继承由线程初始化时inheritThreadLocals参数进行决定,默认不继承。
ThreadLocal中核心存储的类为ThreadLocalMap类,ThreadLocalMap类本身是一个定制化的Map,这个Map以当前ThreadLocal对象作为key值进行K-V存储。ThreadLocalMap的初始化容量为16,扩容因子为2/3。
ThreadLocalMap在进行存储时,会获取当前this对象的threadLocalHashCode值(这也是为什么使用ThreadLocal作为key的原因),该值是唯一的,只在ThreadLocalMap中有用,使用Unsafe提供的AtomicInt类操作获取。
ThreadLocalMap中进行存储的基本单位为Entry数组,数组下标通过threadLocalHashCode进行&运算并根据当前数组长度进行自动扩容。

说明:为什么ThreadLocal的key要使用当前ThreadLocal对象或者说是threadLocalHashCode的值,而不是使用当前线程对象?
一个ThreadLocal对象只会对应一个线程对象,但是一个Thread对象会存在多个ThreadLocal对象,之所以不使用Thread对象作为key,是为了避免多个ThreadLocal对象(或者说ThreadLocalMap)之间的互相影响。

posted @ 2022-05-03 23:52  星光Starsray  阅读(374)  评论(0编辑  收藏  举报