ThreadLocal本地线程变量原理解析

在Java多线程并发环境下,如果我们需要对某一个变量进行操作的话,很有可能将造成线程安全问题,为了解决这种线程安全问题,我们可以给操作这个变量的方法或代码块加各种锁,虽然可以实现线程安全,但这样做会使系统性能受一定的影响,又比如在某个业务场景下,需要多个线程来同时操作每个用户或每笔订单的信息,为了保证线程安全,需要将这些变量都作为每个线程独有的变量,这种情况下,我们可以考虑使用Java中提供的ThreadLocal类来实现,它可以实现每个线程都有属于自己的本地变量副本,从而实现变量的线程安全及其他业务需求。

在了解ThreadLocal的原理前,先贴一个例子来验证ThreadLocal是否实现了每个线程都拥有自己的变量副本

public class ThreadLocalMain {
    private static ThreadLocal local = new ThreadLocal<>();
    public static void main(String[] args) {
        new Thread(() -> {
            local.set("马超");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ",local -> "+local.get());

        }).start();

        new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            local.set("曹操");
            System.out.println(Thread.currentThread().getName() + ",local -> "+local.get());
        }).start();
    }
}

根据上面的运行结果,两个线程操作同一个ThreadLocal中的值,确实没有出现覆盖的情况。

那么它是如何实现的呢?

下面通过ThreadLocal的源码来了解它的实现原理。

这里我们主要了解它的两个核心方法

set(T value);
get();

先从set方法开始

/**
* 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) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

从方法上方的注释及代码来看,可以知道这个方法可以将一个传入的值set到当前线程的本地副本中。

在方法第一行代码中,获取到当前的线程t,再通过getMap方法传入当前线程,来获取这个线程中的threadLocals变量,这个变量是ThreadLocal中的一个内部类ThreadLocalMap,用来保存变量副本。接着判断这个获取到的这个map是否为空(有没有被实例化,因为这个类是延迟构造的),如果不为空则调用ThreadLocalMap的set方法传入当前的引用this及value来设置其中的值,否则调用createMap方法来为当前线程实例化一个ThreadLocalMap,ThreadLocalMap类的内部维护一个Entry[] table数组变量,用来保存本地线程变量的副本,它的结构类似于一个map集合,键为当前操作的这个ThreadLocal(也就是传入的this),值为set方法传入的value。

我们可以点进来看一下这个createMap方法是如何实例化一个ThreadLocalMap的。

void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
}
ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
        table = new Entry[INITIAL_CAPACITY];
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY);
}

通过这个ThreadLocalMap的构造函数,可以看到传入的ThreadLocal作为key,和传入的value通过hash计算来找到table中的某个位置进行存放。

看完set方法的原理后,了解了ThreadLocal是如何存储每个线程的独立变量副本了。

最后再来看一下get方法,和set方法同理,先贴一下源码

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

既然set方法是将值保存在Thread中的ThreadLocalMap,那么get方法也就是通过当前的ThreadLocal作为key,从ThreadLocalMap中的table变量表中取出对应的变量。在代码的最后一行中,setInitialValue()方法用于当你将ThreadLocal作为key取不到数据时(ThreadLocalMap为空,也就是没有调用set方法,直接调用get),会将一个null值保存到table变量表中,此时并不会报空指针异常,而是拿到一个null值。

另外ThreadLocalMap在使用的时候也会造成一些新的问题,例如如果我们的线程一般都是交给线程池来管理的,而线程池的核心就是重复使用这些已经创建好的线程来执行任务,所以当我们的一个线程执行完毕后,后面进来的任务执行时如果分配到这个线程,在未调用set方法情况下就很有可能会get到这个线程中ThreadLocalMap保存的变量,造成读到的数据为脏数据。

除了这个问题外,还可能有引起内存泄漏的问题,原因在于在ThreadLocalMap中保存的变量key使用了指向ThreadLocal的弱引用(WeakReference),当ThreadLocal没有任何强引用关系后,这个在ThreadLocalMap中引用这个ThreadLocal的key也会被回收,变成null,而它对应的value值是不会被回收的,那么将造成内存泄漏问题,除非在当前该线程执行完毕出栈后,也就不存在这个value无法被回收的情况,但是如果我们使用线程池,线程即使执行完任务也不会被销毁,而是在线程池中保持活跃,等待新的任务,这种情况下,将可能引起内存泄漏问题。

为了解决以上出现的问题,我们在使用完这个变量后,最好调用一下remove方法将其删除。


最后,如果你觉得写的还可以的话,请在右上角(app左下角)点个赞吧~

你可能感兴趣的:(Java)