”ThreadLocal“ 本地线程变量是什么,怎么用

这篇文章主要从以下几个角度来分析理解

  1. ThreadLocal是什么
  2. ThreadLocal怎么用
  3. ThreadLocal源码分析
  4. ThreadLocal内存泄漏问题

1. ThreadLocal是什么?

ThreadLocal 叫做本地线程变量,意思是说,ThreadLocal 中填充的的是当前线程的变量,该变量对其他线程而言是封闭且隔离的,ThreadLocal 为变量在每个线程中创建了一个副本,这样每个线程都可以访问自己内部的副本变量。

ThreadLocal使用的场景也有很多,相信你在日常的开发中也经常遇到:
1、在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
2、线程间数据隔离
3、进行事务操作,用于存储线程事务信息。
4、数据库连接,Session会话管理。

2. ThreadLocal怎么用?

首先,在定义ThreadLocal的时候,我们可以同时定义存储在ThreadLocal中的特定类型的对象。

ThreadLocal threadLocalValue = new ThreadLocal<>();

上面我们定义了一个存储Integer的ThreadLocal对象。
要存储和获取ThreadLocal中的对象也非常简单,使用get()和set()即可:

threadLocalValue.set(1);
Integer result = threadLocalValue.get();

我们可以将ThreadLocal看成是一个map,而当前的线程就是map中的key。
除了使用set方法对ThreadLocal赋值外,我们还可以使用默认的初始化方法进行赋值,ThreadLocal提供的静态方法withInitial来初始化一个ThreadLocal。

ThreadLocal threadLocal = ThreadLocal.withInitial(() -> 1);

要想删除ThreadLocal中的存储数据,可以调用:

threadLocal.remove();

3. ThreadLocal源码分析

3.1 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();
        //获取线程中变量 ThreadLocal.ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        //如果不为空,
        if (map != null)
            map.set(this, value);
        else
            //如果为空,初始化该线程对象的map变量,其中key 为当前的threadlocal 变量
            createMap(t, value);
    }

我们可以看到,在ThreadLocal内部有一个ThreadLocalMap类,我们在对ThreadLocal进行数据存储的时候,其实是对ThreadLocalMap进行数据存取。

3.2 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() {
        //     拿到当前线程
        Thread t = Thread.currentThread();
        //    从ThreadLocalMap获取当前线程的数据
        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();
    }

4. ThreadLocal 内存泄漏问题

4.1 在了解ThreadLocal内存泄露之前,我们需要先了解什么是强引用,弱引用:

强引用
在程序代码中普遍存在,类似Object obj = new Object()这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象.
弱引用
用来描述非必需对象的,但是它的强度比软引用更弱一些, 被弱引用关联的对象只能生存到下一次垃圾收集发生之前. 当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象.在JDK 1.2之后,提供了WeakReference类来实现弱引用.

使用代码来进行验证

import java.lang.ref.WeakReference;
public class TestWeakReference {

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed");
    }

    public static void main(String[] args) {
        TestWeakReference twr = new TestWeakReference();
        WeakReference wr = new WeakReference(twr);
        /**
         *  此时TestSoftReference的一个对象有两个引用指向它:
         *  1. 一个强引用twr
         *  2. 一个弱引用sr
         */
        System.out.println("before gc: " + wr.get());
        twr = null;  //去掉强引用twr
        System.gc();
        System.out.println("after  gc: " + wr.get());
    }
}

得到如下结果:

before gc: com.reference.test.TestWeakReference@15db9742
after  gc: null
finalize method executed

ThreadLocal的实现原理,每一个Thread维护一个ThreadLocalMap,key为使用弱引用的ThreadLocal实例,value为线程变量的副本。这些对象之间的引用关系如下,
”ThreadLocal“ 本地线程变量是什么,怎么用_第1张图片
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。

在ThreadLocal中,我们可以通过他Entity的源码看到在ThreadLocalMap去以ThreadLocal为key存储数据的时候,使用的就是弱引用。

/**
         * 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> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal k, Object v) {
                super(k);
                value = v;
            }
        }

4.2 既然弱引用会导致内存泄露,那为什么还要使用弱引用呢?

下面我们分两种情况讨论:
key 使用强引用:
引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
key 使用弱引用:
引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
比较两种情况,我们可以发现:
由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

4.3 如何避免内存泄露
每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。

一般在业务场景中,我们都会用到一个Spring的拦截器类:

public class TokenInterceptor implements HandlerInterceptor{}

他有三个重载方法:

  • preHandle
  • postHandle
  • afterCompletion

preHandle (HttpServletRequest request, HttpServletResponse response, Object handle) 方法,顾名思义,该方法将在请求处理之前进行调用。
postHandle (HttpServletRequest request, HttpServletResponse response, Object handle, ModelAndView modelAndView) 方法,由preHandle 方法的解释我们知道这个方法包括后面要说到的afterCompletion 方法都只能是在当前所属的Interceptor 的preHandle方法的返回值为true 时才能被调用。postHandle 方法,顾名思义就是在当前请求进行处理之后,也就是Controller 方法调用之后执行,但是它会在DispatcherServlet 进行视图返回渲染之前被调用,所以我们可以在这个方法中对Controller 处理之后的ModelAndView 对象进行操作。
afterCompletion (HttpServletRequest request, HttpServletResponse response, Object handle, Exception ex) 方法,该方法也是需要当前对应的Interceptor 的preHandle 方法的返回值为true 时才会执行。顾名思义,该方法将在整个请求结束之后,也就是在DispatcherServlet 渲染了对应的视图之后执行。这个方法的主要作用是用于进行资源清理工作的。

说到这里,大家应该都明白在什么时候去调用ThreadLocal的remove操作了吧,没错,最常用的Spring框架已经为我们提供了合适的调用时机,在afterCompletion中调用ThreadLocal的remove操作,才是业务中比较合适的场景。

你可能感兴趣的:(java后端)