ThreadLocal介绍和应用

Posted 赵广陆

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ThreadLocal介绍和应用相关的知识,希望对你有一定的参考价值。

目录


1 ThreadLocal介绍

当多线程访问共享可变数据时,涉及到线程间同步的问题,并不是所有时候,都要用到共享数据,所以就需要线程封闭出场了。

数据都被封闭在各自的线程之中,就不需要同步,这种通过将数据封闭在线程中而避免使用同步的技术称为线程封闭。

本文主要介绍线程封闭中的其中一种体现:ThreadLocal,将会介绍什么是 ThreadLocal;从 ThreadLocal 源码角度分析,最后介绍 ThreadLocal 的应用场景。

ps:下面这本书可能是最好Java并发编程书籍了,趁着活动赶紧购买,错过就只能在等下一年了

什么是 ThreadLocal?

ThreadLocal 是 Java 里一种特殊变量,它是一个线程级别变量,每个线程都有一个 ThreadLocal 就是每个线程都拥有了自己独立的一个变量,竞态条件被彻底消除了,在并发模式下是绝对安全的变量。

可以通过 ThreadLocal value = new ThreadLocal(); 来使用。

会自动在每一个线程上创建一个 T 的副本,副本之间彼此独立,互不影响,可以用 ThreadLocal 存储一些参数,以便在线程中多个方法中使用,用以代替方法传参的做法。

ThreadLocal和Synchronized都是为了解决多线程中相同变量的访问冲突问题,不同的点是

  • Synchronized是通过线程等待,牺牲时间来解决访问冲突
  • ThreadLocal是通过每个线程单独一份存储空间,牺牲空间来解决冲突,并且相比于Synchronized,ThreadLocal具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值。

正因为ThreadLocal的线程隔离特性,使他的应用场景相对来说更为特殊一些。在android中Looper、ActivityThread以及AMS中都用到了ThreadLocal。当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal。

2 ThreadLocal使用

threadlocal使用方法很简单

static final ThreadLocal<T> sThreadLocal = new ThreadLocal<T>();
sThreadLocal.set()
sThreadLocal.get()

threadlocal而是一个线程内部的存储类,可以在指定线程内存储数据,数据存储以后,只有指定线程可以得到存储数据,官方解释如下。

/**
 * This class provides thread-local variables.  These variables differ from
 * their normal counterparts in that each thread that accesses one (via its
 * @code get or @code set method) has its own, independently initialized
 * copy of the variable.  @code ThreadLocal instances are typically private
 * static fields in classes that wish to associate state with a thread (e.g.,
 * a user ID or Transaction ID).
 */

大致意思就是ThreadLocal提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。通过get和set方法就可以得到当前线程对应的值。

做个不恰当的比喻,从表面上看ThreadLocal相当于维护了一个map,key就是当前的线程,value就是需要存储的对象。

这里的这个比喻是不恰当的,实际上是ThreadLocal的静态内部类ThreadLocalMap为每个Thread都维护了一个数组table,ThreadLocal确定了一个数组下标,而这个下标就是value存储的对应位置。

作为一个存储数据的类,关键点就在get和set方法。

//set 方法
public void set(T value) 
      //获取当前线程
      Thread t = Thread.currentThread();
      //实际存储的数据结构类型
      ThreadLocalMap map = getMap(t);
      //如果存在map就直接set,没有则创建map并set
      if (map != null)
          map.set(this, value);
      else
          createMap(t, value);
  
  
//getMap方法
ThreadLocalMap getMap(Thread t) 
      //thred中维护了一个ThreadLocalMap
      return t.threadLocals;
 
 
//createMap
void createMap(Thread t, T firstValue) 
      //实例化一个新的ThreadLocalMap,并赋值给线程的成员变量threadLocals
      t.threadLocals = new ThreadLocalMap(this, firstValue);

从上面代码可以看出每个线程持有一个ThreadLocalMap对象。每一个新的线程Thread都会实例化一个ThreadLocalMap并赋值给成员变量threadLocals,使用时若已经存在threadLocals则直接使用已经存在的对象。

Thread

/* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

Thread中关于ThreadLocalMap部分的相关声明,接下来看一下createMap方法中的实例化过程。

ThreadLocalMap

set方法

//Entry为ThreadLocalMap静态内部类,对ThreadLocal的若引用
//同时让ThreadLocal和储值形成key-value的关系
static class Entry extends WeakReference<ThreadLocal<?>> 
    /** The value associated with this ThreadLocal. */
    Object value;

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


//ThreadLocalMap构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) 
        //内部成员数组,INITIAL_CAPACITY值为16的常量
        table = new Entry[INITIAL_CAPACITY];
        //位运算,结果与取模相同,计算出需要存放的位置
        //threadLocalHashCode比较有趣
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY);

通过上面的代码不难看出在实例化ThreadLocalMap时创建了一个长度为16的Entry数组。通过hashCode与length位运算确定出一个索引值i,这个i就是被存储在table数组中的位置。

前面讲过每个线程Thread持有一个ThreadLocalMap类型的实例threadLocals,结合此处的构造方法可以理解成每个线程Thread都持有一个Entry型的数组table,而一切的读取过程都是通过操作这个数组table完成的。

显然table是set和get的焦点,在看具体的set和get方法前,先看下面这段代码。

//在某一线程声明了ABC三种类型的ThreadLocal
ThreadLocal<A> sThreadLocalA = new ThreadLocal<A>();
ThreadLocal<B> sThreadLocalB = new ThreadLocal<B>();
ThreadLocal<C> sThreadLocalC = new ThreadLocal<C>();

由前面我们知道对于一个Thread来说只有持有一个ThreadLocalMap,所以ABC对应同一个ThreadLocalMap对象。为了管理ABC,于是将他们存储在一个数组的不同位置,而这个数组就是上面提到的Entry型的数组table。

那么问题来了,ABC在table中的位置是如何确定的?为了能正常够正常的访问对应的值,肯定存在一种方法计算出确定的索引值i,show me code。

  //ThreadLocalMap中set方法。
  private void set(ThreadLocal<?> key, Object value) 

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            //获取索引值,这个地方是比较特别的地方
            int i = key.threadLocalHashCode & (len-1);

            //遍历tab如果已经存在则更新值
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) 
                ThreadLocal<?> k = e.get();

                if (k == key) 
                    e.value = value;
                    return;
                

                if (k == null) 
                    replaceStaleEntry(key, value, i);
                    return;
                
            
            
            //如果上面没有遍历成功则创建新值
            tab[i] = new Entry(key, value);
            int sz = ++size;
            //满足条件数组扩容x2
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        

在ThreadLocalMap中的set方法与构造方法能看到以下代码片段。

  • int i = key.threadLocalHashCode & (len-1)
  • int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)
    简而言之就是将threadLocalHashCode进行一个位运算(取模)得到索引i,threadLocalHashCode代码如下。
    //ThreadLocal中threadLocalHashCode相关代码.
    
    private final int threadLocalHashCode = nextHashCode();

    /**
     * The next hash code to be given out. Updated atomically. Starts at
     * zero.
     */
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    /**
     * The difference between successively generated hash codes - turns
     * implicit sequential thread-local IDs into near-optimally spread
     * multiplicative hash values for power-of-two-sized tables.
     */
    private static final int HASH_INCREMENT = 0x61c88647;

    /**
     * Returns the next hash code.
     */
    private static int nextHashCode() 
        //自增
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    

因为static的原因,在每次new ThreadLocal时因为threadLocalHashCode的初始化,会使threadLocalHashCode值自增一次,增量为0x61c88647。

0x61c88647是斐波那契散列乘数,它的优点是通过它散列(hash)出来的结果分布会比较均匀,可以很大程度上避免hash冲突,已初始容量16为例,hash并与15位运算计算数组下标结果如下:

hashCode数组下标
0x61c886477
0xc3910c8e14
0x255992d55
0x8722191c12
0xe8ea9f633
0x4ab325aa10
0xac7babf11
0xe4432388
0x700cb87f15

总结如下:

  1. 对于某一ThreadLocal来讲,他的索引值i是确定的,在不同线程之间访问时访问的是不同的table数组的同一位置即都为table[i],只不过这个不同线程之间的table是独立的。
  2. 对于同一线程的不同ThreadLocal来讲,这些ThreadLocal实例共享一个table数组,然后每个ThreadLocal实例在table中的索引i是不同的。

get()方法

//ThreadLocal中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();

    
//ThreadLocalMap中getEntry方法
private Entry getEntry(ThreadLocal<?> key) 
       int i = key.threadLocalHashCode & (table.length - 1);
       Entry e = table[i];
       if (e != null && e.get() == key)
            return e;
       else
            return getEntryAfterMiss(key, i, e);
   

理解了set方法,get方法也就清楚明了,无非是通过计算出索引直接从数组对应位置读取即可。

ThreadLocal实现主要涉及Thread,ThreadLocal,ThreadLocalMap这三个类。关于ThreadLocal的实现流程正如上面写的那样,实际代码还有许多细节处理的部分并没有在这里写出来。

3 ThreadLocal应用统一处理token

在之前的开发中,我们会在每一个Service中对token做处理,相同的逻辑一定是要进行统一处理的,接下来我们将使用拦截器+ThreadLocal的方式进行解决。

3.1 编写UserThreadLocal

package com.tanhua.server.utils;

import com.tanhua.server.pojo.User;

public class UserThreadLocal 
	//线程 变量 隔离  thread1  ThreadLocal(aa=10 )  thread2 aa=20
    private static final ThreadLocal<User> LOCAL = new ThreadLocal<User>();

    private UserThreadLocal() 

    

    public static void set(User user) 
        LOCAL.set(user);
    

    public static User get() 
        return LOCAL.get();
    



3.1.1 讲解ThreadLocal,线程安全的一种策略

从名字我们就可以看到ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

内存泄露问题:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3w4haErB-1610066910992)(img/image-20201107211712573.png)]

上面这张图详细的揭示了ThreadLocal和Thread以及ThreadLocalMap三者的关系。

1、Thread中有一个map,就是ThreadLocalMap

2、ThreadLocalMap的key是ThreadLocal,值是我们自己设定的。

3、ThreadLocal是一个弱引用,当为null时,会被当成垃圾回收

重点来了,突然我们ThreadLocal是null了,也就是要被垃圾回收器回收了,但是此时我们的ThreadLocalMap生命周期和Thread的一样,它不会回收,这时候就出现了一个现象。那就是ThreadLocalMap的key没了,但是value还在,这就造成了内存泄漏。

解决办法:使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。

补充内容:线程安全:原子性(lock,synchronized),可见性(volatile,synchronized),顺序性(synchronized,volatile),ThreadLocal(变量隔离)

3.2、编写TokenInterceptor

package com.tanhua.server.interceptor;

import com.tanhua.server.pojo.User;
import com.tanhua.server.service.UserService;
import com.tanhua.server.utils.NoAuthorization;
import com.tanhua.server.utils.UserThreadLocal;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 统一完成根据token查询用User的功能
 */
@Component
public class TokenInterceptor implements HandlerInterceptor 

    @Autowired
    private UserService userService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
            throws Exception 

        if (handler instanceof HandlerMethod) 
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            NoAuthorization noAnnotation = handlerMethod.getMethod().getAnnotation(NoAuthorization.class);
            if (noAnnotation != null) 
                // 如果该方法被标记为无需验证token,直接返回即可
                return true;
            
        

        String token = request.getHeader("Authorization");
        if (StringUtils.isNotEmpty(token)) 
            User user = this.userService.queryUserByToken(token);
            if (null != user) 
                UserThreadLocal.set(user); //将当前对象,存储到当前的线程中
                return true;
            
        

        //请求头中如不存在Authorization直接返回false
        response.setStatus(401); //无权限访问
        return false;
    


3.3 编写注解NoAuthorization

package com.tanhua.server.utils;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented //标记注解
public @interface NoAuthorization 


3.4 注册拦截器

package com.tanhua.server.config;

import com.tanhua.server.interceptor.RedisCacheInterceptor;
import com.tanhua.server.interceptor.TokenInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebConfig implements WebMvcConfigurer 

    @Autowired
    private RedisCacheInterceptor redisCacheInterceptor;
    @Autowired
    private TokenInterceptor tokenInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) 
        // 注意拦截器的顺序
        registry.addInterceptor(this.tokenInterceptor).addPathPatterns("/**");
        registry.addInterceptor(this.redisCacheInterceptor).addPathPatterns("/**");
    

以上是关于ThreadLocal介绍和应用的主要内容,如果未能解决你的问题,请参考以下文章

深入浅出多线程编程实战ThreadLocal详解(介绍使用原理应用场景)

一文搞懂 ThreadLocal 原理

事务管理在三层架构中应用以及使用ThreadLocal再次重构

如何将乘数应用于在 nib 文件上创建的所有约束?

ThreadLocal 难点解析

ThreadLocal