ThreadLocal 详解及使用

ThreadLocal 详解及使用

文章目录

  • ThreadLocal 详解及使用
      • 简介
      • 实现原理简介
        • 结构
        • 初始化时机
        • 执行过程
      • 使用示例
      • ThreadLocal 的内存泄漏问题
        • JAVA引用类型
          • 示例
        • ThreadLocal 内存泄漏分析
      • ThreadLocal 的衍生类

简介

被用作在同一个线程中存取信息的对象;该对象存储的信息仅在该线程中可以访问,做到了线程间隔离。

常用在当请求进入拦截器时,在该请求线程中存入一些信息(如根据用户请求确定其用户的一些基本信息并存入,方便该线程的方法随时取出利用),在请求结束时调用其 remove() 方法清空信息。

实现原理简介

结构

每个 Thread 内都会个 ThreadLocal.ThreadLocalMap threadLocals = null 对象

public class Thread implements Runnable {
       
        private static native void registerNatives();
        static {
            registerNatives();
        }
    // ...
    
        ThreadLocal.ThreadLocalMap threadLocals = null;
    
        ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    // ...
    }

ThreadLocalMapThreadLocal 的静态内部类,也就意味着这个 ThreadLocalMap 是应用中公用的。
ThreadLocalMap 内部存储结构类似于 Map.Entry ,而这个 Entry 结构的 keyWeakReference> ,这也是会发生内存泄漏的原因(后文解释)

public class ThreadLocal {
    // ...
    static class ThreadLocalMap {

        static class Entry extends WeakReference> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

        private static final int INITIAL_CAPACITY = 16;

        private Entry[] table;
        
        // ...
    }
初始化时机

它会在当前线程第一次调用的时候初始化(当 threadlocals == null

// 初始化
private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
    if (this instanceof TerminatingThreadLocal) {
        TerminatingThreadLocal.register((TerminatingThreadLocal) this);
    }
    return value;
}

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}
执行过程

实际操作是在每次调用 ThreadLocal 方法时都会传入当前的线程,根据这个线程找到这个线程对应的ThreadLocalMap ,而ThreadLocalMap 内的 Entry 属性,这就是用来存储这个线程公用的 key,value。也就是说 ThreadLocal 是操作 ThreadLocalMap 的桥梁。

每个 ThreadLocalMap 都是这个线程独有的,所以是线程隔离的。

// get方法示例
public T get() {
    Thread t = Thread.currentThread();
    // 传入当前线程作参数,得到对应的 ThreadLocal
    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();
}

使用示例

  1. 定义存储信息类
@Data
public class ThreadLocalUser {

    /**
     * 用户id
     */
    private Integer userId;

    /**
     * 用户昵称
     */
    private String nickname;

    /**
     * 用户头像地址
     */
    private String avatarUrl;

    /**
     * 用户权限集合
     */
    private List authorities;

}
  1. 定义操作该存储信息对象的 ThreadLocal
/**
 * ThreadLocal用户信息管理对象
 */
@Getter
@Setter
public class RequestThreadHolder {

    private final static String USERID = "userId";

    private final static String NICKNAME = "nickname";

    private final static String ROLES = "authorities";

    private final static ThreadLocal THREAD_LOCAL = ThreadLocal.withInitial(ThreadLocalUser::new);

    /**
     * 根据请求的jwt解析用户信息初始化请求的用户信息
     * @param jwt
     */
    public static void init(String jwt){
        DecodedJWT decode = JWT.decode(jwt);
        Map claims = decode.getClaims();
        Integer userId = Objects.isNull(claims.get(USERID)) ? 0 : claims.get(USERID).asInt();
        String nickname = StringUtils.isEmpty(claims.get(NICKNAME).asString()) ? "佚名" : claims.get(NICKNAME).asString();
        List roles = Objects.isNull(claims.get(ROLES)) ? Collections.emptyList() : claims.get(ROLES).asList(String.class);
        THREAD_LOCAL.get().setUserId(userId);
        THREAD_LOCAL.get().setNickname(nickname);
        THREAD_LOCAL.get().setAuthorities(roles);
    }

    /**
     * 获取请求用户信息
     * @return
     */
    public static ThreadLocalUser getUserInfo(){
        return THREAD_LOCAL.get();
    }

    /**
     * 获取请求用户id
     * @return
     */
    public static Integer getLocalUserId(){
        return THREAD_LOCAL.get().getUserId();
    }

    /**
     * 获取请求用户的昵称
     * @return
     */
    public static String getLocalUserNickname(){
        return THREAD_LOCAL.get().getNickname();
    }

    /**
     * 获取请求用户角色列表
     * @return
     */
    public static List getLocalUserRoles(){
        return THREAD_LOCAL.get().getAuthorities();
    }

    /**
     * 清除ThreadLocal信息
     */
    public static void clean(){
        THREAD_LOCAL.remove();
    }
}
  1. 拦截器编写
/**
 * 请求用户信息拦截器
 */
public class RequestThreadHolderInterceptor extends HandlerInterceptorAdapter {

    /**
     * 请求进入前向 ThreadLocal 注入用户信息
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String bearerToken = request.getHeader(HttpHeaders.AUTHORIZATION);
        String jwt = StringUtils.substringAfter(bearerToken,TokenConstant.TOKEN_PREFIX);
        RequestThreadHolder.init(jwt);
        if (StringUtils.isEmpty(jwt)){
            throw new InnerException("token不存在");
        }
        return super.preHandle(request, response, handler);
    }

    /**
     * 请求结束后调用清理 ThreadLocal
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        RequestThreadHolder.clean();
        super.afterCompletion(request, response, handler, ex);
    }
}
  1. 加入到拦截器
@Configuration
public class InterceptorConfigurer implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new RequestThreadHolderInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns("/swagger-resources/**", "/v2/**", "/test/**");
    }
}

ThreadLocal 的内存泄漏问题

内存泄漏:内存中存在没有被程序使用的对象,但是无法被垃圾回收器回收。

ThreadLocal 的内存泄漏涉及到 Java 的弱引用。

JAVA引用类型
弱引用/软引用解释:
	对于以下的各类引用,new出来的只是对传入进去的对象声明一个弱/软引用,当这个对象还存在强引用时,即使显示调用System.gc()也无法回收该对象。
	
	public static void main(String[] args) throws InterruptedException {
        Object object = new Object();
        WeakReference<Object> weak2 = new WeakReference<>(object);
        System.out.println("weak:" + weak2.get());	//输出object对象
        object = null;
        System.gc();
    	//调用gc:输出null;否则输出object对象。
    	//因为若不执行object=null,则new object()这个对象一直存在强引用,无法回收。
        System.out.println("weak gc:" + weak2.get());
    }
1.强引用:
	强引用是使用最普遍的引用。如:Object obj = new Object();
	这时对这个Object对象就存在一个强引用。对于强引用如果一直存在,GC不会对其进行回收,即使内存溢出。所以如果对象确定不在使用,需要及时弱化,如:obj = null;

2.软引用(SoftReference):
	一个对象若只有软引用,则内存空间充足时,GC就不会回收它;如果内存空间不足了,就会回收这些对象的内存。
	String str = new String("abc");
	//声明一个软引用对象,引用的是new String("abc")
    SoftReference<String> softReference = new SoftReference<String>(str);

3.弱引用(WeakReference)//对于弱引用,GC不管当前内存空间足够与否,都会回收它的内存。
	String str = new String("abc");
    WeakReference<String> weakReference = new WeakReference<>(str);
    
4.虚引用(PhantomReference):
	如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
示例

弱引用gc简单测试

public static void main(String[] args) {
    User user1 = new User();
    user1.setUsername("张三");
    User user2 = new User();
    user2.setUsername("李四");
    WeakReference<User> weak1 = new WeakReference<>(user1);
    WeakReference<User> weak2 = new WeakReference<>(user2);
    HashMap<WeakReference<User>, Integer> map = new HashMap<>();
    map.put(weak1,1);
    map.put(weak2,2);
    user1 = null;
    System.gc();
    for (Map.Entry entry:map.entrySet()){
        System.out.println(entry.getKey() + ":" + entry.getValue());
    }
}

ThreadLocal 详解及使用_第1张图片

ThreadLocal 内存泄漏分析
  1. 构建弱引用对象

    @Getter
    @Setter
    // 继承弱引用对象,并指定泛型(非必须)
    public class WeakRefUser extends WeakReference {
    
        private String name;
    
        // WeakReference的构造必须初始化referent属性,即标明这个对象是弱引用对象
        public WeakRefUser(User referent,String name) {
            super(referent);
            this.name = name;
        }
    }
    
  2. 代码测试

    public static void main(String[] args) {
        WeakRefUser weakRefUser1 = new WeakRefUser(new User(), "张三");
        WeakRefUser weakRefUser2 = new WeakRefUser(new User(), "李四");
        HashMap map = new HashMap<>();
        map.put(weakRefUser1,1);
        map.put(weakRefUser2,2);
        System.gc();
        System.out.println();
    }
    

ThreadLocal 详解及使用_第2张图片

ThreadLocal 详解及使用_第3张图片

  1. ThreadLocal 内部构造查看

    ThreadLocal 的方法实际是操作其内部属性 ThreadLocalMap;而 ThreadLocalMap 的主要结构其实是一个弱引用的 Entry

static class ThreadLocalMap {

        static class Entry extends WeakReference<ThreadLocal<?>> {

            Object value;

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

        private static final int INITIAL_CAPACITY = 16;
    // ...
}

看到上述结构就能明白,平时我们操作的 Map 结构就是一个 Entry ;而 ThreadLocalMapEntrykey 是弱引用的 referent 属性,这个属性只要经过 gc 就会被回收,使其指向 null ;这就会导致这个 Entry 中存在 keynull 的但其 EntryThreadLocalMap(静态类) 存在强关联,使得这样的 Entry 无法被 gc 回收,而这种元素又是无法被利用的,如果不及时清理,使这种类型元素大量堆积,就出现常说的内存泄漏(存在大量无法引用的对象)

  1. 避免 ThreadLocal 内存泄漏

    ThreadLocal 在调用 get()set()remove() 时都会处理这些为 nullkey ,但依旧建议在使用完及时 remove() 来保证一定移除这个 Entry

ThreadLocal 的衍生类

  • InheritableThreadLocal
    ThreadLoca l区别是 可以传递值给子线程。

    存在的问题:

    1. 线程不安全:如果说线程本地变量是只读变量不会受到影响,但是如果是可写的,那么任意子线程针对本地变量的修改都会影响到主线程的本地变量。
    2. 线程池中可能失效:在使用线程池的时候,在执行异步任务时可能不需要创建新的线程了(即子线程),因此也就不会再传递父线程的ThreadLocalMap 给子线程了。
  • TransmittableThreadLocal
    阿里开源的,很好的解决 InheritableThreadLocal 在线程池情况下,父子线程值传递问题。

你可能感兴趣的:(java,数据结构)