深入理解 Java 中的 ThreadLocal

发布时间 2023-06-24 23:38:02作者: easy16

1. 什么是 ThreadLocal

在 Java 多线程编程中,我们经常会遇到共享变量的并发访问问题。为了解决这个问题,Java 提供了 ThreadLocal 类,它允许我们在每个线程中存储和访问线程局部变量,而不会影响其他线程的数据。

2. 使用 ThreadLocal

使用 ThreadLocal 很简单,我们只需要创建一个 ThreadLocal 对象,然后使用 set() 方法设置值,使用 get() 方法获取值即可。

2.1 两个线程使用一个ThreadLoal变量

点击查看代码
public static void main(String[] args) {
        ThreadLocal<String> local = new ThreadLocal<>();

        Thread t1 = new Thread(() -> {
            local.set("t1");
            System.out.println("tid=" + Thread.currentThread() + ", local=" + local + ",local val =" + local.get());
        });
        t1.start();
        Thread t2 = new Thread(() -> {
            local.set("t2");
            System.out.println("tid=" + Thread.currentThread() + ", local=" + local + ",local val =" + local.get());
        });
        t2.start();
    }

运行结果

从以上执行结果可以看出,创建的ThreadLocal变量 local在线程t1和t2中是同一个变量,但是通过set()方法设置数据后,保存的却是各自的副本,再使用get()方法访问的是各自的数据,实现了不同线程间数据的隔离。

2.2 单个线程有两个ThreadLoal变量

点击查看代码
public static void main(String[] args) {
        ThreadLocal<String> local1 = new ThreadLocal<>();
        ThreadLocal<String> local2 = new ThreadLocal<>();

        Thread t1 = new Thread(() -> {
            local1.set("v1");
            local2.set("v2");
            System.out.println("tid=" + Thread.currentThread() + ", local1=" + local1 + ", val =" + local1.get());
            System.out.println("tid=" + Thread.currentThread() + ", local1=" + local2 + ", val =" + local2.get());

        });
        t1.start();
    }

运行结果:

线程可以有多个threadLocal变量,他们之间也是相互独立的。

3. 实现原理

ThreadLocal 的实现原理主要涉及三个关键点:ThreadLocal 类、Thread 类和 ThreadLocalMap 类。

  • ThreadLocal 类是 ThreadLocal 变量的容器,每个线程中可以定义多个 ThreadLocal 对象。
  • Thread 类是 Java 中表示线程的类,每个线程都有一个 ThreadLocalMap 对象。
点击查看代码
public
class Thread implements Runnable {
   
    private volatile String name;
    private int            priority;
    private Thread         threadQ;
    private long           eetop;

    /* omit some. */


    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
  • ThreadLocalMap 类是 ThreadLocal 对象的存储结构,它是一个特定于线程的键值对集合。
点击查看代码
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;
            }
        }

        /**
         * The initial capacity -- MUST be a power of two.
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;

        /**
         * The number of entries in the table.
         */
        private int size = 0;

当我们使用 ThreadLocal 设置值时,值被存储在当前线程的 ThreadLocalMap 中。在当前线程中,我们可以通过 ThreadLocal 对象来获取和更新这些值,而不会干扰其他线程的数据。

3.1 源码分析set()流程

点击查看代码
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的set()方法在执行时,首先获取当前线程中对应的 ThreadLocalMap 对象。

如果当前线程的 ThreadLocalMap 为 null,则需要先进行初始化。调用 createMap() 方法创建一个新的 ThreadLocalMap 对象,并将其设置到当前线程中。

ThreadLocalMap 是一个自定义的哈希表结构,用于存储 ThreadLocal 对象和对应的值。在 ThreadLocalMap 中,ThreadLocal 实例是弱引用,而值则是强引用。

接下来,set() 方法会将根据当前 ThreadLocal 实例作为键,将传入的值作为值,创建出一个Entry,存储到 ThreadLocalMap 的Entry[] tab数组中。

ThreadLocalMap 使用线性探测法解决哈希冲突,并使用开放地址法进行存储。当插入新的键值对时,会遍历数组,找到合适的位置进行插入。如果发生哈希冲突,则会继续向后查找空槽进行插入。

存储完成后,当前线程的 ThreadLocalMap 中就包含了该 ThreadLocal 实例及其对应的值。

3.2 源码分析get()流程

点击查看代码
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;
            }
        }
        return setInitialValue();
    }

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

get() 方法会获取当前线程中的 ThreadLocalMap 对象

如果当前线程的 ThreadLocalMap 为 null,表示当前线程没有使用 ThreadLocal,则直接返回 null。

如果当前线程的 ThreadLocalMap 不为 null,则通过当前 ThreadLocal 实例作为键,从 ThreadLocalMap 中获取对应的值。 从key.threadLocalHashCode & (table.length - 1)可知ThreadLocal 实例在每个线程中Entry[]table数组的下标是固定的。

在 ThreadLocalMap 中,会根据 ThreadLocal 实例的哈希值进行索引,查找对应的存储位置。

如果找到对应的位置,即表示当前线程已经使用过该 ThreadLocal 实例,可以直接返回存储的值。

如果未找到对应的位置,或者位置对应的 ThreadLocal 实例与当前 ThreadLocal 实例不匹配,即表示当前线程没有使用过该 ThreadLocal 实例,返回 null。

3.3 三者之间的关系图

4. ThreadLocal 的应用场景

ThreadLocal 在许多场景下非常有用,特别是在以下情况下:

  • 多线程环境下的数据隔离:当多个线程需要访问同一个对象的数据时,使用 ThreadLocal 可以避免线程间的数据竞争和并发访问问题。
  • 线程上下文传递:在跨线程的业务逻辑中,可以使用

5. 注意事项

  • 内存泄漏问题:由于 ThreadLocal 使用了线程的唯一标识作为索引,在使用完毕后,如果没有手动清理或及时移除 ThreadLocal 对象的引用,可能会导致内存泄漏问题。确保在使用完毕后及时调用 remove() 方法或将 ThreadLocal 对象设置为 null。

  • 初始值设置:每个线程在第一次访问 ThreadLocal 对象时,会调用 initialValue() 方法来获取初始值。如果需要特定的初始值,可以通过继承 ThreadLocal 并重写 initialValue() 方法来实现。

  • 共享变量问题:虽然 ThreadLocal 可以在每个线程中存储自己的数据,但要注意共享变量的访问。如果多个线程共享同一个对象,并且这个对象中包含 ThreadLocal 变量,那么多个线程对 ThreadLocal 变量的修改会相互影响。确保对共享变量的访问是线程安全的。

  • 使用场景选择:ThreadLocal 应该谨慎使用,只在确实需要在每个线程中存储和访问数据时使用。滥用 ThreadLocal 可能导致代码的复杂性增加,并且可能不利于代码的维护和理解。

  • 清理操作:在使用 ThreadLocal 时,需要确保在合适的时机进行清理操作。当线程执行完毕或不再需要使用 ThreadLocal 时,应该手动调用 remove() 方法来清理 ThreadLocal 对象,以避免潜在的内存泄漏问题。

总之,使用 ThreadLocal 需要谨慎并遵循最佳实践,确保正确管理和使用 ThreadLocal 对象,以实现线程间的数据隔离和安全访问