ThreadLocal的理解

280 阅读3分钟

ThreadLocal是Java语言提供的一个线程局部(Thread-Local)变量。当你在多线程环境下工作时,每个线程可以通过ThreadLocal存储属于自己的对象副本,各个线程间的这些副本是相互独立的。这意味着你无需同步就可以保证字段不会出现并发访问问题。

理解ThreadLocal

ThreadLocal的常见用途包括:

  • 维护数据库连接、会话信息、简单的性能监视统计等线程特定的数据。
  • 在没有依赖注入容器的情况下管理线程作用域的数据。

每个线程调用ThreadLocalget()方法时,都会返回与当前线程关联的对象副本。如果是第一次调用ThreadLocalget()方法,且之前没有调用过set()方法,则ThreadLocal通常会调用它的initialValue()方法,以便为该线程提供一个初始值。

源码分析

ThreadLocal内部是通过一个ThreadLocalMap来维持线程局部变量的。每个Thread都有一个ThreadLocalMap引用,该Map的键是ThreadLocal实例,值是对应的线程局部实例。

下面是ThreadLocal的一个简单使用示例和一部分实现源码分析:

示例代码

public class ThreadLocalExample {

    // 创建一个Integer类型的线程局部变量
    private static final ThreadLocal<Integer> threadLocalValue = ThreadLocal.withInitial(() -> 0);

    public static class MyRunnable implements Runnable {

        @Override
        public void run() {
            threadLocalValue.set((int) (Math.random() * 100D));

            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(threadLocalValue.get());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyRunnable sharedRunnableInstance = new MyRunnable();

        Thread thread1 = new Thread(sharedRunnableInstance);
        Thread thread2 = new Thread(sharedRunnableInstance);

        thread1.start();
        thread2.start();

        thread1.join(); // 等待线程1终止
        thread2.join(); // 等待线程2终止
    }
}

在上面的代码中,我们创建了一个ThreadLocal实例,并给它一个初始值0。然后,我们创建了一个实现Runnable接口的类,在该类的run方法中改变了threadLocalValue的值,并打印出来。

源码分析(以 OpenJDK 8 为例)

public class ThreadLocal<T> {
    // 线程局部变量的值存储在每个线程自己的 threadLocals 中,而不是ThreadLocal对象中。
    static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value; // 我们的值

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
        ...
    }
    
    // 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();
    }

    ...
}

ThreadLocal实际上并不存储值,它只是作为一个键,通过get()set()方法操作Thread内部的ThreadLocalMap。这个Map由多个Entry组成,Entry是ThreadLocal的内部类,它扩展了WeakReference,用于作为键的引用。这是因为ThreadLocal可能会被GC回收,而不影响线程生命周期内的使用。

细节说明

  • ThreadLocal在使用完毕后应该调用remove()方法,以防止内存泄漏。尤其是在使用线程池时,线程是会被重用的,如果不清理,可能会导致之后执行的任务使用到旧数据。
  • ThreadLocal的键(即ThreadLocal对象)是弱引用,但值不是。如果ThreadLocal没有外部强引用且被GC,其Entry的键会变为null,这时ThreadLocalMap会将这些键为null的Entry称为"stale entries",它们的值不会被GC,直到下一次ThreadLocalMap调整或显式调用remove()
  • ThreadLocal不应该用于实例变量,它通常作为静态字段使用。如果你把它放在实例变量中,并在一个线程中操作,那么它会与那个线程绑定,并不会因为实例的改变而改变。

ThreadLocal是实现线程安全的一个重要工具,但使用时需要注意内存泄漏的问题,并确保不滥用,因为每个ThreadLocal在每个线程中都会保存一个副本,如果数据量大的话,会占用相应更多的内存。