<返回更多

一文看懂Java中的ThreadLocal源码和注意事项

2023-04-12  今日头条  Code404
加入收藏

一、ThreadLocal的原理

ThreadLocal是一个非常重要的类,它为每个线程提供了一个独立的变量副本。因此,每个线程都可以独立地访问和修改该变量,而不会影响其他线程的访问。这种机制在多线程编程中非常有用,特别是在需要处理线程安全问题时。

ThreadLocal的原理很简单:它为每个线程维护一个Map,该Map中存储了每个线程对应的变量值。当我们调用ThreadLocal的get()方法时,它将先获取当前线程,然后从当前线程的Map中查找对应的变量;如果该变量不存在,那么就通过initialValue()方法来创建一个新的变量,并将其存储到当前线程的Map中。在以后的访问中,该线程便可以直接从Map中获取变量值,而不需要通过参数传递或共享变量的方式。

二、源码关键片段讲解

1.get方法

用于获取当前线程的 ThreadLocal 对象所对应的值

    /**
     * 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() {
        // 通过 Thread.currentThread() 方法获取当前线程对象 t
        Thread t = Thread.currentThread();
        // 通过 getMap(t) 方法获取当前线程的 ThreadLocalMap 对象 map
        ThreadLocalMap map = getMap(t);
        // 如果当前线程已经存储了该 ThreadLocal 对象对应的值,那么就通过 map.getEntry(this) 方法得到该值的 Entry 对象 e,并返回其 value 属性即可
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // 否则,需要进行初始化,并将初始化后的值保存到当前线程中。这里调用的是 setInitialValue() 方法,其实现在类的另一个方法中。
        return setInitialValue();
    }

2.setInitialValue方法

用于初始化当前线程的 ThreadLocalMap 对象并将初始化后的值保存到其中。

    /**
     * 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() {
        // 调用 initialValue() 方法获取 ThreadLocal 对象的初始值
        T value = initialValue();
        // 通过 Thread.currentThread() 方法获取当前线程对象 t
        Thread t = Thread.currentThread();
        // 通过 getMap(t) 方法获取当前线程的 ThreadLocalMap 对象 map
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // 如果 map 不为空,就使用 map.set(this, value) 方法
            // 将 ThreadLocal 对象和其对应的值存储到 map 中,
            // 即将该对象和值作为 key-value 存入 map 的 table 数组的某个位置,
            // table 的索引通过 hashCode() 方法进行计算并取模得到;
            map.set(this, value);
        } else {
            // 否则,就需要创建新的 ThreadLocalMap 对象,
            // 然后再将其存储到当前线程的 
            // ThreadLocalMap.ThreadLocalMapHolder.threadLocalMap 中。
            // 这里调用的是 createMap(t, value) 方法,其实现在类的另一个方法中。
            createMap(t, value);
        }
        if (this instanceof TerminatingThreadLocal) {
            // 如果该 ThreadLocal 对象是 TerminatingThreadLocal 的实例,
            // 那么就需要调用 TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this) 方法进行注册
            TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
        }
        return value;
    }

3.createMap方法

用于为当前线程创建 ThreadLocalMap 对象,并将该对象存储到当前线程的 threadLocals 属性中

    /**
     * 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
     */
    // 接受两个参数:当前线程 t 和 ThreadLocal 对象的初始值 firstValue 
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

将新创建的 ThreadLocalMap 对象作为参数传入 ThreadLocalMap 的构造方法,并将得到的结果存储到当前线程的 threadLocals 属性中。需要注意的是,线程的 threadLocals 属性是一个
ThreadLocal.ThreadLocalMap 类型的变量,表示线程的本地变量表。如果当前线程没有 threadLocals 属性,则会新建一个。如果当前线程已经有了 threadLocals 属性,则直接使用新建的 ThreadLocalMap 替换原来的对象即可。

需要强调的是,这里的 createMap() 方法是 ThreadLocal 类的一个 protected 方法,因此只能在 ThreadLocal 类及其子类中被调用。同时,在 InheritableThreadLocal 类中还有一个覆盖了该方法的版本,用于处理可以被子线程继承的线程本地变量。

4.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() {
         // 通过 Thread.currentThread() 方法获取当前线程对象 t
         // 通过 getMap(t) 方法获取当前线程的 ThreadLocalMap 对象 m
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null) {
             // 如果 m 不为 null,就调用 m.remove(this) 方法将该 ThreadLocal 从 m 中删除
             m.remove(this);
         }
     }

注意,remove() 方法只会删除当前线程的 ThreadLocalMap 中与该 ThreadLocal 对象对应的 entry,不会影响其他线程的值。

三、ThreadLocalMap的Entry为什么是弱引用?

ThreadLocal的内部类ThreadLocalMap源码可以看到,它的Entry继承WeakReference,是一个弱引用的类

   /**
     * 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;
            }
        }
        .........
    }

其实使用弱引用是为了避免内存泄漏问题。

ThreadLocalMap 中的 key 为 ThreadLocal 对象,value 为线程本地变量对应的副本。当一个线程结束时,如果不显式地清理 ThreadLocalMap 中该线程对应的 Entry 对象,那么这些 Entry 对象及其对应的 value 副本会一直存在于内存中,就会导致内存泄漏问题。

如果 ThreadLocalMap 中使用的是强引用,那么即使线程已经结束,由于这些 Entry 对象仍然被 ThreadLocalMap 引用着,垃圾回收器也无法将这些 Entry 对象回收掉,从而导致内存泄漏。

因此,为了避免这种情况,ThreadLocalMap 中使用了 WeakReference 来引用 ThreadLocal 对象。WeakReference 对象具有弱引用特性,当 ThreadLocal 变量没有被其他对象引用时,就可以被垃圾回收器回收,同时 ThreadLocalMap 中对应的 Entry 对象也会被自动删除,从而避免了内存泄漏问题。

特别需要注意的是,虽然 ThreadLocalMap 中的 Entry 对象是弱引用,但是 value 部分却是强引用,因此当 ThreadLocal 变量被垃圾回收器回收后,对应的 value 副本仍然会一直存在于内存中,需要开发者手动清理。因此,在使用 ThreadLocal 时,需要注意在不需要使用 ThreadLocal 变量时,及时调用 remove() 方法进行清理。

四、代码示例和解释

下面是一个简单的ThreadLocal使用示例:

public class ThreadLocalTest {
    private static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    int num = threadLocal.get();
                    System.out.println(Thread.currentThread().getName() + " get num=" + num);
                    threadLocal.set(num + 1);
                    System.out.println(Thread.currentThread().getName() + " set num=" + (num + 1));
                    System.out.println(Thread.currentThread().getName() + " get num=" + threadLocal.get());
                }
            }).start();
        }
    }
}

上面的代码中,我们使用了一个静态变量threadLocal,它是一个ThreadLocal对象。我们在main方法中创建了5个线程,每个线程中都调用了threadLocal的get()方法来获取变量值,并输出线程名和变量值。然后,调用了threadLocal的set()方法来修改变量值,并输出线程名和修改后的变量值。最后再次调用get()方法来获取变量值,并输出线程名和变量值。

Thread-1 get num=0
Thread-1 set num=1
Thread-1 get num=1
Thread-4 get num=0
Thread-4 set num=1
Thread-4 get num=1
Thread-3 get num=0
Thread-3 set num=1
Thread-3 get num=1
Thread-2 get num=0
Thread-2 set num=1
Thread-2 get num=1
Thread-0 get num=0
Thread-0 set num=1
Thread-0 get num=1

我们可以看到,虽然这个变量是static的,但是每个线程都有自己的变量副本,它们互不干扰。这就避免了线程安全问题的出现。

五、注意事项

在使用ThreadLocal时,需要注意以下几点:

1.内存泄漏问题:ThreadLocal 中的变量可以在程序执行结束后一直存在于内存中,如果不及时清理,会导致内存泄漏问题。因此,在使用完 ThreadLocal 变量之后,需要手动调用 remove() 方法,将变量和当前线程绑定的副本从 ThreadLocalMap 中移除,并释放相关内存空间。

2.资源占用问题:ThreadLocal 对象本身也是一个对象,使用过多的 ThreadLocal 对象会占用大量的内存空间。因此,应该根据实际情况,合理地使用和管理 ThreadLocal 对象,避免过度使用。

3.只适用于线程内部传递数据:ThreadLocal 对象只能在同一个线程内部传递数据,无法在多个线程之间直接传递数据。

声明:本站部分内容来自互联网,如有版权侵犯或其他问题请与我们联系,我们将立即删除或处理。
▍相关推荐
更多资讯 >>>