多线程中ThreadLocal详解

1. ThreadLocal是什么,是用来解决什么问题的?

  • ThreadLocal从字面意思来理解,是一个线程本地变量,也可以叫线程本地变量存储。有时候一个对象的变量会被多个线程所访问,这个时候就会有线程安全问题,当然可以使用synchronized关键字来为该变量加锁,进行同步处理来限制只能有一个线程来使用该变量,但是这样会影响程序执行的效率,这时ThreadLocal就派上了用场;
  • 使用ThreadLocal维护变量的时候,会为每一个使用该变量的线程提供一个独立的变量副本,即每个线程内部都会有一个当前变量。这样同时有多个线程访问该变量并不会相互影响,因为他们都是使用各自线程存储的变量,所以不会存在线程安全的问题。
  • 同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式,前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问且互不影响。

2. ThreadLocal源码讲解

多线程中ThreadLocal详解_第1张图片
源码解析前我们先分析一下ThreadLocal实现原理:每个Thread维护一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal实例本身,value是真正需要存储的Object。也就是说ThreadLocal本身不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。值得注意的是图中(图片摘自网络)的虚线,表示ThreadLocalMap是使用ThreadLocal的弱引用作为key的,弱引用的对象在GC时会被回收。

  • ThreadLocal中有四个主要的方法,如下所示:
public T get() {} //获取当前线程中ThreadLocal副本
public void set(T value) {} //用来设置当前线程中ThreadLocal副本
public void remove() {} //移除当前线程中ThreadLocal副本
protected T initialValue() {} //是一个protected方法,一般使用时需要重写,默认返回为null
  • 详细看一下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的inheritableThreadLocals变量
        ThreadLocalMap map = getMap(t);
        //如果当前线程中ThreadLocalMap为null,则返回初始化值
        if (map != null) {
	        //获取map中ThreadLocal副本
            ThreadLocalMap.Entry e = map.getEntry(this);
            //获取存储的变量值
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //返回初始化值,一般setInitialValue()需要重写,自定义初始化值
        return setInitialValue();
    }

方法注释内容大致为: 返回当前线程的此线程局部变量副本中的值。 如果变量没有当前线程的值,则首先将其初始化为调用initialValue()方法返回的值。

ThreadLocalMap.Entry e = map.getEntry(this);

以上Entry类是内部类ThreadLocalMap中的内部类,集成了WeakReference类,如下所示:

   /**
         * 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. */
            //与此ThreadLocal关联的值
            Object value;

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

该类注释的大致意思是: 此hash map的entries使用它的“主引用”字段作为键(它始终是ThreadLocal对象)继承自WeakReference类。 请注意,null键(即entry.get()== null)表示不再引用该键,因此可以从table中删除该entry。 这些entries在下面的代码中称为“陈旧entry”。
既然Entry继承了WeakReference,说明ThreadLocal使用了弱引用,如果entry.get()==null,当前ThreadLocal副本会立即被GC掉,避免高并发情况下出现内存溢出。

  • 详细看一下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中获取存储的当前副本的值
        ThreadLocalMap map = getMap(t);
        //如果map不为空,覆盖当前副本中的值,否则新建副本
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

该方法的大致意思是: 将此线程局部变量的当前线程副本设置为指定值。 大多数子类都不需要重写此方法,仅依靠initialValue()方法来设置线程局部变量值。

  • 详细看一下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() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

该方法注释的大概意思是: 删除当前线程ThreadLocal的变量值。 如果当前线程随后通过该线程从get()方法中获取ThreadLocal的变量值,那么它的值将通过调用initialValue()方法重新获取初始值,除非它的值是当前线程临时调用set()方法重新设置了值。 这可能导致在当前线程中多次调用initialValue()方法。

  • 接下来就看看上面总提到的initialValue()方法:
/**
     * Returns the current thread's "initial value" for this
     * thread-local variable.  This method will be invoked the first
     * time a thread accesses the variable with the {@link #get}
     * method, unless the thread previously invoked the {@link #set}
     * method, in which case the {@code initialValue} method will not
     * be invoked for the thread.  Normally, this method is invoked at
     * most once per thread, but it may be invoked again in case of
     * subsequent invocations of {@link #remove} followed by {@link #get}.
     *
     * 

This implementation simply returns {@code null}; if the * programmer desires thread-local variables to have an initial * value other than {@code null}, {@code ThreadLocal} must be * subclassed, and this method overridden. Typically, an * anonymous inner class will be used. * * @return the initial value for this thread-local */ protected T initialValue() { return null; }

这个方法默认返回是null,注意看一下它的修饰符是protected,一般情况这个方法是要使用者去实现的,设置默认的初始值。

3. ThreadLocal使用场景

ThreadLocal常用于解决数据库连接、session管理等,我在项目中用于做注解切面日志,记录了日志时间变量,不同线程中使用自己的变量副本,来统计方法的耗时,以下是我的实现代码:

  • 首先定义一个注解类
/**
 * @Author: Helon
 * @Description: 自定义注解(切面日志)
 * @Data: Created in 2018/4/16 13:39
 * @Modified By:
 */
//target:注解的作用目标
//ElementType.TYPE:接口、类、枚举、注解
//ElementType.FIELD:字段、枚举的常量
//ElementType.METHOD:方法
//ElementType.CONSTRUCTOR:构造函数
//ElementType.LOCAL_VARIABLE:局部变量
//ElementType.ANNOTATION_TYPE:注解
//ElementType.PACKAGE:包
@Target({ElementType.METHOD, ElementType.TYPE})
//保留策略:
// RetentionPolicy.SOURCE:注解只保留在源文件,编译成class文件时候,注解被遗弃;
//RetentionPolicy.CLASS:注解被保留在class文件,但jvm加载class时候会被遗弃,默认值;
//RetentionPolicy.CLASS:注解不仅被保存到class文件中,jvm加载之后仍然存在;
@Retention(RetentionPolicy.RUNTIME)
//Documented:注解表明这个注解应该被 javadoc工具记录.
// 默认情况下,javadoc是不包括注解的.
// 但如果声明注解时指定了 @Documented,则它会被 javadoc 之类的工具处理, 所以注解类型信息也会被包括在生成的文档中
@Documented
public @interface AspectLog {
    String description() default "操作日志";
}
  • 接下来定义一个切面类
/**
 * @Author: Helon
 * @Description: 日志切面
 * @Data: Created in 2018/4/16 10:36
 * @Modified By:
 */
@Aspect
@Component
public class WebLogAspect {

    /***
     * 用于计算时间
     */
    ThreadLocal<Long> startTime = new ThreadLocal<>();

    /***
     * 选取按包路径进行切入
     */
 /*   @Pointcut("execution(public * com.chtwm.qyjr.controller..*.*(..))")
    public void webLog(){}*/
	
	//选取切入点为自定义注解
    @Pointcut("@annotation(com.chtwm.qyjr.aspect.AspectLog)")
    //切入点的加载顺序,值越小优先级越高
    @Order(1)
    public void webLog(){}
	
	/***
     * 方法执行前切入,且从aspectLog对象中获取描述信息
     */
    @Before("webLog()&&@annotation(aspectLog)")
    public void doBefore(JoinPoint joinPoint, AspectLog aspectLog) throws Throwable {
        ServletRequestAttributes attributes = (ServletRequestAttributes)RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        //记录开始时间
        startTime.set(System.currentTimeMillis());
        LogUtil.KAFKA.info("[切面日志doBefore]-日志描述:【{}】,开始执行", aspectLog.description());
        LogUtil.KAFKA.info("[切面日志doBefore]-请求URL:{}", request.getRequestURL().toString());
        LogUtil.KAFKA.info("[切面日志doBefore]-HTTP请求方式:{}", request.getMethod());
        LogUtil.KAFKA.info("[切面日志doBefore]-请求参数:{}", Arrays.toString(joinPoint.getArgs()));
    }
	
	/***
     * 方法执行完成切入,且从aspectLog对象中获取描述信息,returning = "ret"获取响应信息
     */
    @AfterReturning(returning = "ret", pointcut = "webLog()&&@annotation(aspectLog)")
    public void doAfterReturning(Object ret, AspectLog aspectLog) throws Throwable {
        try {
            LogUtil.KAFKA.info("[切面日志doAfterReturning]-响应数据:{}", JSONObject.toJSONString(ret));
            LogUtil.KAFKA.info("[切面日志doAfterReturning]-耗时:{}", (System.currentTimeMillis() - startTime.get()) + "ms");
            LogUtil.KAFKA.info("[切面日志doAfterReturning]-日志描述:【{}】,执行结束", aspectLog.description());
        } finally {
            //清除当前线程的ThreadLocal副本
            startTime.remove();
        }
    }
}

4. ThreadLocal内存泄漏问题

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么虚拟机GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程一直不结束的话,那么这些key为null的Entry,它的value就会一直存在一条强引用链(如本文开始的结构图):Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施,在调用ThreadLocal的get(),set(),remove()方法的时候都会清除掉该线程ThreadLocalMap里所有key为null的value。但是,这些被动的预防措施并不能保证不会泄露,例如以下情况:

  • 使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏;
  • 分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏;
    因此,每次使用完ThreadLocal,都要调用它的remove()方法,清除数据。

你可能感兴趣的:(多线程)