ThreadLocal 的使用以及原理

305 阅读3分钟

前言

ThreadLocal 如果直接翻译的话,可以翻译为线程本地(副本),即每一个线程都有自己的数据副本,那么,在多线程情况下,这个数据在线程之间就是相互隔离的,不用担心数据被破坏。

场景

假设有一个方法调用链,这条方法调用链上都需要使用到同一个变量来实现功能,例如:某一个业务场景中,我们需要调用多个方法,每个方法都需要使用到登录用户的信息进行判断、鉴权等操作,很容易想到这种调用情况。

public void function(User user) {
    ......;
    f1(user);
    ......;
    f2(user);
    ......;
    f3(user);
    ......
}

这种情况下,每一个方法都需要去传递一个需要共同使用的变量,这个时候可以把它抽取为一个静态变量,各个方法直接在方法内部获取这个变量即可。

public static User user;

public void function() {
    ......;
    f1();
    ......;
    f2();
    ......;
    f3();
    ......
}

这样子,在单线程情况下可以很好地满足需求,但是在多个线程下,各个线程都会操作这个变量,很容易造成 A 线程修改了 user 的部分信息,B 线程读取了 user 被修改的信息。或者试想以下,这个变量是一个数据库连接实例,A 线程刚获取到这个实例,B 线程就将这个连接关闭、销毁了,那么 A 线程就无法继续进行正常业务了。

这时,引入 ThreadLocal 类保存这些各个线程要操作的变量,就不会导致各个线程操作同一个数据了,因为各个线程都有自己的数据副本。

使用

public class Main {

    private static ThreadLocal<Integer> localData = new ThreadLocal<>();

    public static void main(String[] args) {

        new Thread(() -> {
            System.out.println("线程 1 获取数据:" + localData.get());
            localData.set(1);
            for (int i = 0; i < 5; i++) {
                localData.set(localData.get()+1);
                System.out.println("线程 1 获取数据:" + localData.get());
            }
        }).start();

        new Thread(() -> {
            System.out.println("线程 2 获取数据:" + localData.get());
            localData.set(2);
            for (int i = 0; i < 5; i++) {
                localData.set(localData.get()*2);
                System.out.println("线程 2 获取数据:" + localData.get());
            }
        }).start();
    }
}

运行结果:

线程 1 获取数据:null
线程 2 获取数据:null
线程 2 获取数据:4
线程 1 获取数据:2
线程 2 获取数据:8
线程 1 获取数据:3
线程 2 获取数据:16
线程 1 获取数据:4
线程 2 获取数据:32
线程 1 获取数据:5
线程 2 获取数据:64
线程 1 获取数据:6

可以看到线程 1 和线程 2 的数据操作都互不影响。

原理分析

ThreadLocal 的 set 方法

首先看 LocalDataset() 方法

public class ThreadLocal<T> {
    public void set(T value) {
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 调用 LocalData 的 getMap 方法,参数是当前线程
        // 得到 一个 ThreadLocalMap 对象,存放传递过来的值,key 为 ThreadLocal 实例
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }

    // 返回当前线程的 threadLocals 变量
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
}

public class Thread implements Runnable {
    ......;
    // ThreadLocalMap 是 Thread 的内部静态类,本质上就是一个 Entry,通过 key-value 存储
    ThreadLocal.ThreadLocalMap threadLocals = null;
    
    static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    ......;
}

简单来说,Thread 中有一个 ThreadLocalMap 内部类,其本质上就是一个 Entry,通过 key-value 存储值,其中 key 为每一个 ThreadLocal 对象实例,value 为每一个线程想要保存的值。

当线程通过 ThreadLocal 变量调用 set 方法时,本质上是获取当前线程的实例中的一个 Entry,然后以 ThreadLocal 的实例为 key 保存值,如果有多个 ThreadLocal 对象,就保存多个 key-value。这样,每个线程中的数据就相互隔离了,不会被其他线程破坏。

ThreadLocal 的 get 方法

再看看 get 方法

public class ThreadLocal<T> {
    
	public T get() {
        // 获取当前线程,获取线程中的 ThreadLocalMap 对象
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
       	// key 为 ThreadLocal 对象,获取这个 key 对应的值,并返回
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // 如果事先没有调用过 set 方法,那么会调用这个方法,默认返回 null
        return setInitialValue();
    }
    
    private T setInitialValue() {
        T value = initialValue();
        // .............
        return value;
    }
    
    protected T initialValue() {
        return null;
    }
}

经过上面 set 方法的源码分析,也可以看出 get 方法本质上也是通过操作当前ThreadLocal实例的当前线程的 ThreadLocalMap 实例获取线程保存的值的。

小结

当一个线程需要反复使用同一个对象实例时,我们可以将其抽离为静态成员变量,但为了线程安全,我们可以使用 ThreadLocal 来保存这个静态成员变量,将其与其他线程隔离,不用担心被其他线程破坏。

其实,ThreadLocal 的操作,最后还是回归到了 Thread线程本身中,数据也是保存到了 Thread 线程实例中,ThreadLocal 只是起到了一个中间桥梁的作用。