并发编程原理与实战(三十四)ThreadLocal核心API与线程安全数据隔离机制深度解析

前一篇我们学习了基于AQS框架下简易的自定锁的实现过程,接下来我们来学习并发编程中常见的一些类,本文先来学习ThreadLocal。

背景与基础概念

为什么需要ThreadLocal?我们已经知道加锁或者使用原子类可以保证线程安全的访问共享资源,这两种方式本质上都是多线程竞争共享资源,但是某些场景下我们只需要多线程并发安全执行,不需要访问共享资源,也就是线程之间相互隔离,线程只访问自己独享的资源,在此背景下引入了ThreadLocal。

ThreadLocal是什么?ThreadLocal是Java中用于实现线程数据隔离的类,位于java.lang包下。通过为每个线程创建独立的局部变量,实现线程间的数据隔离。这种设计避免了多线程共享变量时的同步竞争,无需依赖锁机制即可保证线程安全。

相比synchronized等技术采用"时间换空间"的策略(通过加锁排队访问共享资源),ThreadLocal采用"空间换时间"策略(为每个线程分配线程私有变量),消除了锁竞争带来的性能损耗,尤其适合高频访问的线程局部变量场景。

认识ThreadLocal类

/**
 * This class provides thread-local variables.  These variables differ from
 * their normal counterparts in that each thread that accesses one (via its
 * {@code get} or {@code set} method) has its own, independently initialized
 * copy of the variable.  {@code ThreadLocal} instances are typically private
 * static fields in classes that wish to associate state with a thread (e.g.,
 * a user ID or Transaction ID).
 *
 * <p>For example, the class below generates unique identifiers local to each
 * thread.
 * A thread's id is assigned the first time it invokes {@code ThreadId.get()}
 * and remains unchanged on subsequent calls.
 * <pre>
 * import java.util.concurrent.atomic.AtomicInteger;
 *
 * public class ThreadId {
 *     // Atomic integer containing the next thread ID to be assigned
 *     private static final AtomicInteger nextId = new AtomicInteger(0);
 *
 *     // Thread local variable containing each thread's ID
 *     private static final ThreadLocal&lt;Integer&gt; threadId =
 *         new ThreadLocal&lt;Integer&gt;() {
 *             &#64;Override protected Integer initialValue() {
 *                 return nextId.getAndIncrement();
 *         }
 *     };
 *
 *     // Returns the current thread's unique ID, assigning it if necessary
 *     public static int get() {
 *         return threadId.get();
 *     }
 * }
 * </pre>
 * <p>Each thread holds an implicit reference to its copy of a thread-local
 * variable as long as the thread is alive and the {@code ThreadLocal}
 * instance is accessible; after a thread goes away, all of its copies of
 * thread-local instances are subject to garbage collection (unless other
 * references to these copies exist).
 * @param <T> the type of the thread local's value
 *
 * @author  Josh Bloch and Doug Lea
 * @since   1.2
 */
public class ThreadLocal<T> {
...
}

此类提供线程局部变量。这些变量与普通变量的区别在于:访问它们的每个线程(通过其get或set方法)都会获得该变量的独立初始化的副本。ThreadLocal实例通常作为希望将状态与线程关联的类的私有静态字段(例如用户ID或事务ID)。

例如,下面的类为每个线程生成唯一的局部标识符。线程的ID在首次调用ThreadId.get()时分配,后续调用保持不变。

import java.util.concurrent.atomic.AtomicInteger;
public class ThreadId {
  // 包含待分配的下一个线程ID的原子整数
  private static final AtomicInteger nextId = new AtomicInteger(0);

  // 包含每个线程ID的线程局部变量
  private static final ThreadLocal<Integer> threadId =
      new ThreadLocal<Integer>() {
          @Override protected Integer initialValue() {
              return nextId.getAndIncrement();
      }
  };

  //返回当前线程的唯一ID,必要时分配
  public static int get() {
      return threadId.get();
  }
}

只要线程存活且ThreadLocal实例可访问,每个线程都会隐式持有其线程局部变量的副本;当线程结束时,所有线程局部变量的副本都将被垃圾回收(除非存在对这些副本的其他引用)。

我们创建5个线程对上面这个例子进行测试:

/**
 * 测试用例
 */
public static void main(String[] args) {
    // 创建5个测试线程
    for (int i = 0; i < 5; i++) {
        new Thread(() -> {
            // 首次获取ID
            System.out.println(Thread.currentThread().getName() +
                    " ID: " + ThreadId.get());
            // 再次验证ID不变
            System.out.println(Thread.currentThread().getName() +
                    " Confirm ID: " + ThreadId.get());
        }).start();
    }
}

运行结果:

Thread-0 ID: 0
Thread-0 Confirm ID: 0
Thread-4 ID: 4
Thread-4 Confirm ID: 4
Thread-1 ID: 1
Thread-1 Confirm ID: 1
Thread-3 ID: 2
Thread-3 Confirm ID: 2
Thread-2 ID: 3
Thread-2 Confirm ID: 3

从运行结果可以看出,每个线程首次获取ID后,这个ID就只跟该线程有关,该线程后续获取ID时保持不变,而其他线程是获取不到该线程的的ID的,这样就做到了线程之间的数据隔离,那么这样的机制是如何实现的?下面我们就就通过分析ThreadLocal的核心API来解答这个问题。

ThreadLocal的核心API

ThreadLocal的核心API提供了线程局部变量的完整生命周期管理。下图是基于jdk21的ThreadLocal类结构图。
在这里插入图片描述

构造函数

匿名内部类重写 initialValue() 创建实例

/**
 * Creates a thread local variable.
 * @see #withInitial(java.util.function.Supplier)
 */
public ThreadLocal() {
}

看到这里可能会有一个疑问,文章开头的例子中通过new ThreadLocal() {}的方式创建实例,也就是通过匿名内部类重写 initialValue() 的方式创建ThreadLocal实例。而ThreadLocal构造函数却是空的?

ThreadLocal 是一个泛型类,但它的构造函数是空的,这是因为 Java 的泛型信息在编译时会被擦除,运行时无法获取具体的类型参数。泛型擦除意味着在编译期间,所有的泛型信息都会被移除,而在运行时,所有泛型类型都转换为其边界类型(通常是 Object)。

虽然构造函数本身没有泛型参数,但 ThreadLocal 的类型安全性通过编译器类型检查、get/set 方法的泛型参数类型检查等方式保证。

使用withInitial() 方法创建实例

通过匿名内部类重写 initialValue() 的方式创建ThreadLocal实例是Java 8之前的写法,Java 8 及以后‌推荐使用 withInitial() 方法

/**
 * Creates a thread local variable. The initial value of the variable is
 * determined by invoking the {@code get} method on the {@code Supplier}.
 *
 * @param <S> the type of the thread local's value
 * @param supplier the supplier to be used to determine the initial value
 * @return a new thread local variable
 * @throws NullPointerException if the specified supplier is null
 * @since 1.8
 */
public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
    return new SuppliedThreadLocal<>(supplier);
}

该方法是一个静态方法,用于创建一个线程局部变量。该变量的初始值通过 Supplier 函数式接口来提供初始值。
@param 线程局部变量值的类型
@param supplier 用于确定初始值的供应商
@return 一个新的线程局部变量
@throws NullPointerException 如果指定的供应商为 null

这样代码更简洁了,如下:

// 创建ThreadLocal实例
private static final ThreadLocal<Integer> threadId =
        ThreadLocal.withInitial(() -> nextId.getAndIncrement());

() -> nextId.getAndIncrement() 是 Java 中的 Lambda 表达式写法,它定义了一个不接受任何参数并返回 nextId.getAndIncrement() 结果的无参函数式接口实现。

set/get相关方法

set(T value)方法

/**
 * Sets the current thread's copy of this thread-local variable
 * to the specified value.  Most subclasses will have no need to
 * override this method, relying solely on the {@link #initialValue}
 * method to set the values of thread-locals.
 *
 * @param value the value to be stored in the current thread's copy of
 *        this thread-local.
 */
public void set(T value) {
    set(Thread.currentThread(), value);
    if (TRACE_VTHREAD_LOCALS) {
        dumpStackIfVirtualThread();
    }
}

将当前线程中此线程局部变量设置为指定值。大多数子类无需重写此方法,仅依赖initialValue 方法来设置线程局部变量的值。@param value 要存储在当前线程的此线程局部变量中的值。内部调用set(Thread t, T value)方法:

private void set(Thread t, T value) {
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
}

set(Thread t, T value)方法以当前线程对象t和要存储到线程局部变量的值value为参数。

/**
 * Get the map associated with a ThreadLocal. Overridden in
 * InheritableThreadLocal.
 *
 * @param  t the current thread
 * @return the map
 */
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

根据当前线程对象t获取ThreadLocal.ThreadLocalMap类型的变量threadLocals。

public class Thread implements Runnable {
...
/*
 * ThreadLocal values pertaining to this thread. This map is maintained
 * by the ThreadLocal class.
 */
ThreadLocal.ThreadLocalMap threadLocals;
...
}

变量threadLocals是线程类Thread 中的属性,用于存储线程的私有数据,这个印证了文章开头说的每个线程有独立的局部变量。

/**
 * Create the map associated with a ThreadLocal. Overridden in
 * InheritableThreadLocal.
 *
 * @param t the current thread
 * @param firstValue value for the initial entry of the map
 */
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

如果变量threadLocals为空则调用createMap(Thread t, T firstValue)方法创建ThreadLocalMap对象。

ThreadLocalMap

/**
 * ThreadLocalMap is a customized hash map suitable only for
 * maintaining thread local values. No operations are exported
 * outside of the ThreadLocal class. The class is package private to
 * allow declaration of fields in class Thread.  To help deal with
 * very large and long-lived usages, the hash table entries use
 * WeakReferences for keys. However, since reference queues are not
 * used, stale entries are guaranteed to be removed only when
 * the table starts running out of space.
 */
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;

    /**
     * The next size value at which to resize.
     */
    private int threshold; // Default to 0    
    ...
}

ThreadLocalMap 是一种专为维护线程局部变量而设计的定制化哈希表。其所有操作均仅在ThreadLocal 类内部使用。该类被声明为包级私有(default权限,也称为包访问权限或默认权限),以便在 Thread 类中声明字段(Thread 类与ThreadLocal类均在java.lang包下)。为了处理大规模且长期存在的使用场景,哈希表条目使用 WeakReference(弱引用)作为键。但由于未使用引用队列,只有当哈希表空间不足时,才会确保清除过期的条目。

该哈希表中的条目继承自WeakReference,使用其主引用字段作为键(该键始终是一个ThreadLocal对象)。请注意,空键(即entry.get() == null)表示该键不再被引用,因此可以将该条目从表中清除。在后续代码中,此类条目被称为“陈旧条目”。

总的来讲,化哈希表采用Entry数组存储数据,每个Entry使用弱引用指向ThreadLocal对象作为key,实际存储的变量值作为value。

get()方法

/**
 * Returns the value in the current thread's copy of this
 * thread-local variable.  If the variable has no value for the
 * current thread, it is first initialized to the value returned
 * by an invocation of the {@link #initialValue} method.
 *
 * @return the current thread's value of this thread-local
 */
public T get() {
    return get(Thread.currentThread());
}

返回当前线程中此线程局部变量的值,内部调用get(Thread t)方法。

private T get(Thread t) {
    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(t);
}

获取当前线程对象的局部变量哈希表ThreadLocalMap,如果不为空则通过ThreadLocal对象(键)获取线程的局部变量值。如果线程的局部变量哈希表ThreadLocalMap为空,则调用setInitialValue(Thread t)方法返回初始化的值。

/**
 * Variant of set() to establish initialValue. Used instead
 * of set() in case user has overridden the set() method.
 *
 * @return the initial value
 */
private T setInitialValue(Thread t) {
    T value = initialValue();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
    if (this instanceof TerminatingThreadLocal<?> ttl) {
        TerminatingThreadLocal.register(ttl);
    }
    if (TRACE_VTHREAD_LOCALS) {
        dumpStackIfVirtualThread();
    }
    return value;
}

initialValue方法就是我们创建ThreadLocal对象时重写的方法。

initialValue()

/**
 * Returns the current thread's "initial value" for this
 * thread-local variable.  This method will be invoked the first
 * time a thread accesses the variable with the {@link #get}
 * method, unless the thread previously invoked the {@link #set}
 * method, in which case the {@code initialValue} method will not
 * be invoked for the thread.  Normally, this method is invoked at
 * most once per thread, but it may be invoked again in case of
 * subsequent invocations of {@link #remove} followed by {@link #get}.
 *
 * @implSpec
 * This implementation simply returns {@code null}; if the
 * programmer desires thread-local variables to have an initial
 * value other than {@code null}, then either {@code ThreadLocal}
 * can be subclassed and this method overridden or the method
 * {@link ThreadLocal#withInitial(Supplier)} can be used to
 * construct a {@code ThreadLocal}.
 *
 * @return the initial value for this thread-local
 * @see #withInitial(java.util.function.Supplier)
 */
protected T initialValue() {
    return null;
}

返回当前线程中此线程局部变量的“初始值”。当线程首次通过 get方法访问该变量时,此方法将被调用,除非该线程之前已调用过set 方法,在这种情况下,该线程不会调用initialValue方法(因为哈希表已创建)。通常情况下,此方法在每个线程中最多调用一次,但如果在调用remove后再次调用get,则可能会再次调用此方法。

此实现直接返回null;如果程序员希望线程局部变量的初始值不为null,则可以子类化ThreadLocal并重写此方法,或者使用ThreadLocal#withInitial(java.util.function.Supplier)方法来构造一个ThreadLocal。

remove()方法

/**
* Removes the current thread's value for this thread-local
* variable.  If this thread-local variable is subsequently
* {@linkplain #get read} by the current thread, its value will be
* reinitialized by invoking its {@link #initialValue} method,
* unless its value is {@linkplain #set set} by the current thread
* in the interim.  This may result in multiple invocations of the
* {@code initialValue} method in the current thread.
*
* @since 1.5
*/
public void remove() {
 remove(Thread.currentThread());
}

private void remove(Thread t) {
     ThreadLocalMap m = getMap(t);
     if (m != null) {
         m.remove(this);
     }
 }

移除当前线程中此线程局部变量的值。如果当前线程随后通过get读取该线程局部变量,其值将通过调用initialValue方法重新初始化,除非在此期间当前线程通过set设置了该值。这可能导致当前线程多次调用initialValue 方法。

get/set方法使用示例

对文章开头的例子进行改造下,在首次获取线程ID前设置线程的局部变量。

/**
 * 测试用例
 */
public static void main(String[] args) {
    // 创建5个测试线程
    for (int i = 0; i < 5; i++) {
        new Thread(() -> {
            // 设置值
            threadId.set(threadId.get() + 10);
            // 首次获取ID
            System.out.println(Thread.currentThread().getName() +
                    " ID: " + ThreadId.get());
            // 再次验证ID不变
            System.out.println(Thread.currentThread().getName() +
                    " Confirm ID: " + ThreadId.get());
        }).start();
    }
}

运行结果:

Thread-1 ID: 11
Thread-1 Confirm ID: 11
Thread-4 ID: 14
Thread-4 Confirm ID: 14
Thread-2 ID: 12
Thread-2 Confirm ID: 12
Thread-0 ID: 10
Thread-0 Confirm ID: 10
Thread-3 ID: 13
Thread-3 Confirm ID: 13

总结

通过分析ThreadLocal的核心API,我们总结出ThreadLocal线程安全的数据隔离机制:

1、ThreadLocal通过为每个线程创建独立的局部变量来实现线程安全的数据隔离,其核心机制基于Thread、ThreadLocal和ThreadLocalMap三者的协作。

2、每个Thread对象内部维护一个ThreadLocalMap实例作为线程的私有存储空间。当调用ThreadLocal的set()方法时,系统会获取当前线程的ThreadLocalMap,并以ThreadLocal对象作为键、要存储的数据作为值进行存储。同样,get()方法通过当前线程和ThreadLocal对象来获取对应的值。

3、这种设计实现了真正的线程数据隔离,每个线程操作的都是自己的私有变量,从根本上避免了多线程访问共享资源带来的竞争问题。与传统的同步机制(如synchronized)通过加锁控制共享资源访问不同,ThreadLocal采用"空间换安全"策略,无需显式加锁即可保证线程安全。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

帧栈

您的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值