美文网首页收藏alreadyJava
面试官:不懂ThreadLocal,还谈什么并发编程?

面试官:不懂ThreadLocal,还谈什么并发编程?

作者: 马小莫QAQ | 来源:发表于2022-03-08 15:03 被阅读0次

    前言

    什么是ThreadLocal?

    查阅源码注释得知,此类提供线程局部变量。何为线程局部变量?意思就是ThreadLocal存储的变量属于当前线程,对其他线程是隔离的。ThreadLocal为每个线程通过它的 get() 或 set() 方法来创建独立初始化的变量副本;ThreadLocal实例通常是类中希望将状态与线程相关联的私有静态字段(例如,用户ID 或 事务ID);只要线程处于活动状态且ThreadLocal实例是可访问的,每个线程都持有对其线程局部变量副本的隐式引用;在线程消失后,它的所有线程本地实例副本都将受到垃圾回收(除非存在对这些副本的其他引用)。

    使用场景

    1. 线程间的数据隔离
    2. 进行事务操作,存储线程事务信息
    3. 数据库连接、Session会话管理
    4. 在进行对象跨层传递时,打破层次间的约束
    5. ...

    源码部分

    ThreadLocal是一个泛型类,通过泛型可以指定要存储的类型,源码中只提供了一个构造方法,这个构造方法通常是单独使用的,也可以配合 initialValue() 方法使用,重写该方法是在ThreadLocal实例化时提供一个初始值。实现方式如下面代码所示:

    构造方法

    //方式一:实例化 ThreadLocal 并设置一个初始值
    ThreadLocal<Integer> threadId = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 1;
        }
    };
    
    //方式二:相比方式一ThreadLocal提供了一个静态方法 withInitial 代码看起来更简洁,更优雅
    ThreadLocal<Integer> threadId = ThreadLocal.withInitial(() -> 1);
    
    //查看源码:Supplier接口提供了一个get方法并且标记为@FunctionalInterface 支持Lambda表达式写法
    public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
        return new SuppliedThreadLocal<>(supplier);
    }
    
    //查看源码:SuppliedThreadLocal是ThreadLocal中finnal修饰的静态内部类,继承了ThreadLocal并且重写了initialValue()
    static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {
    
        private final Supplier<? extends T> supplier;
    
        SuppliedThreadLocal(Supplier<? extends T> supplier) {
            this.supplier = Objects.requireNonNull(supplier);
        }
    
        @Override
        protected T initialValue() {
            return supplier.get();
        }
    }
    

    set()方法

    设置当前线程的局部变量值

    源码中注释的意思是:set(T value) 方法设置指定值,大多数情况下子类不需要重写此方法,可依靠 initialValue() 方法来设置线程局部变量的初始值。

    //设置当前线程局部变量
    public void set(T value) {
        //当前线程实例
        Thread t = Thread.currentThread();
        //拿到当前线程中的ThreadLocalMap
        ThreadLocal.ThreadLocalMap map = getMap(t);
        if (map != null)//存在则覆盖
            map.set(this, value);
        else //创建一个ThreadLocalMap,将当前线程t作为Map的key对应value存储到ThreadLocalMap中
            createMap(t, value);
    }
    
    /**
     *
     * 拿到当前线程中的ThreadLocalMap
     *
     * @param  t 当前线程
     * @return the map 存储当前线程局部变量副本
     */
    ThreadLocal.ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    
    /**
     * 作用是给传递的线程创建一个对应的 ThreadLocalMap 并把值存进去,
     * 可以看到新创建的 ThreadLocalMap 被赋值给了线程中的 threadLocals 变量,
     * 这也说明对应的数据都是存储在各个线程中的,所以每个线程对数据的操作自然不会影响其它线程的数据
     *
     * @param t 当前线程
     * @param firstValue map中的初始值
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue);
    }
    
    //ThreadLocalMap 是一个定制的哈希映射,仅适用于维护当前线程本地值。感兴趣的同学可以看一下源码是怎么实现存储当前线程局部变量的
    static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /**
             * 与此ThreadLocal关联的值
             */
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    }
    

    get()方法

    /**
     * 返回当前线程本地值(局部变量的值),如果当前线程没有本地值,则调用初始化 initialValue() 方法返回的值
     *
     * @return 当前线程的本地值
     */
    public T get() {
        //t:当前线程
        Thread t = Thread.currentThread();
        //map:当前线程局部变量副本
        ThreadLocal.ThreadLocalMap map = getMap(t);
        if (map != null) {
            //this:代表当前ThreadLocal对象,拿到ThreadLocal对应的值
            ThreadLocal.ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;//ThreadLocal对应的值
                return result;
            }
        }
        return setInitialValue();//调用setInitialValue方法返回初始值
    }
    
    /**
     * set() 的另一种实现方式,用于建立初始值。如果用户重写了 set() 方法,则使用它代替 set()。
     *
     * @return 返回初始值
     */
    private T setInitialValue() {
        T value = initialValue();//如果是重写了initialValue() 方法这里也可以拿到初始值
        Thread t = Thread.currentThread();//当前线程
        ThreadLocal.ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }
    

    emove()方法

    删除当前线程的局部变量值,如果这个线程局部变量随后被当前线程get读取,它的值将通过调用它的 initialValue() 方法重新初始化,除非它的值被当前线程在set时,这可能会导致在当前线程中多次调用 initialValue() 方法。

    public void remove() {
        ThreadLocal.ThreadLocalMap m = getMap(Thread.currentThread());
        if (m != null)
            m.remove(this);
    }
    
    private void remove(ThreadLocal<?> key) {
        ThreadLocal.ThreadLocalMap.Entry[] tab = table;
        int len = tab.length;
        int i = key.threadLocalHashCode & (len-1);
        for (ThreadLocal.ThreadLocalMap.Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            if (e.get() == key) {
                e.clear();
                expungeStaleEntry(i);
                return;
            }
        }
    }
    
    //clear() GC特殊处理
    private T referent;         /* Treated specially by GC */
    public void clear() {
        this.referent = null;
    }
    

    实战

    业务场景

    假设你的项目中有一个 UserController 类中调用了 userService.getUserInfo(Integer uid) 方法,此方法是用来根据用户ID获取用户信息的,此时业务需求发生了变更,需要你根据前端传入的字符串token来查询用户信息,但是你发现这个方法在你的项目中已经被多个地方调用了,此时去更改该方法的入参结构或者是重写一个根据token来查询就会导致牵一发动全身之前引用的地方也会随之更改,有没有一种方式能够减少受影响的范围,同时满足uid和token这两种方式来查询呢?

    前面说到 ThreadLocal 是可以打破层次间的约束,跨层传递对象的,且数据隔离级别是线程级的,我们是不是可以尝试用 ThreadLocal 来传递用户的token,话不多说,上代码

    代码实现

    UserController 中的get方法用来模拟查询用户信息

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.http.MediaType;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class UserController {
    
        @Autowired
        private IUserService userService;
    
        @GetMapping(path = "/user/get", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
        public UserInfoDTO get(Integer uid) {
    
            return userService.getUserInfo(uid);
        }
    }
    

    UserServiceImpl 实现了 IUserService 接口,完成测试数据初始化,getUserInfo实现了根据uid和token获取用户信息

    import lombok.extern.slf4j.Slf4j;
    import org.springframework.stereotype.Service;
    
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Optional;
    
    @Slf4j
    @Service
    public class UserServiceImpl implements IUserService {
    
        static ThreadLocal<String> tokenStore = new ThreadLocal<>();
    
        /**
         * 临时数据,模拟存储在数据库表的数据
         */
        private List<UserInfoDTO> data = new ArrayList<>();
    
        public UserServiceImpl() {
            initData();
        }
    
        @Override
        public UserInfoDTO getUserInfo(Integer uid) {
    
            if (null != uid) {
                //模拟根据Id查询用户信息
                Optional<UserInfoDTO> optionalByUid = data.stream().filter(u -> u.getUid().equals(uid)).findFirst();
                if (optionalByUid.isPresent()) {
                    log.info("根据用户ID {} 查到了信息", uid);
                    return optionalByUid.get();
                }
            }
    
            //模拟从ThreadLocal中拿到用户的token
            String token = tokenStore.get();
            log.info("UserServiceImpl 线程:{} 获取了token:{}", Thread.currentThread().getName(), token);
            if (null != token) {
                //模拟根据token查询用户信息
                Optional<UserInfoDTO> optionalByToken = data.stream().filter(u -> u.getToken().equals(token)).findFirst();
                if (optionalByToken.isPresent()) {
                    return optionalByToken.get();
                }
            }
    
            //用户信息不存在
            return null;
        }
    
        /**
         * 生产临时数据
         */
        private void initData() {
            this.data = new ArrayList<>();
            this.data.add(new UserInfoDTO(1, "zhangsan", "张三", "4b86736c73674b7c910657a9c6786470"));
            this.data.add(new UserInfoDTO(2, "lisi", "里斯", "4b86736c73674b7c910657a9c6786471"));
            this.data.add(new UserInfoDTO(3, "jack", "杰克", "4b86736c73674b7c910657a9c6786473"));
            log.info("数据初始化完成");
        }
    }
    

    UserFilter 用户过滤器用来获取前端放在Header中的token,在UserFilter中将token放入UserServiceImpl定义的(ThreadLocal) tokenStore中完成跨层级传递对象

    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.lang.StringUtils;
    import org.springframework.stereotype.Component;
    
    import javax.servlet.*;
    import javax.servlet.annotation.WebFilter;
    import javax.servlet.http.HttpServletRequest;
    import java.io.IOException;
    
    @Slf4j
    @WebFilter
    @Component
    public class UserFilter implements Filter {
        @Override
        public void init(FilterConfig filterConfig) throws ServletException {
    
        }
    
        @Override
        public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
            HttpServletRequest httpServletRequest = (HttpServletRequest) request;
            log.info("进入UserFilter...");
            //获取前端传的token
            String token = httpServletRequest.getHeader("Token");
            if (StringUtils.isNotBlank(token)) {
                //存储到ThreadLocal中
                UserServiceImpl.tokenStore.set(token);
                log.info("UserFilter 线程:{} 存储了token:{}", Thread.currentThread().getName(), token);
            }
    
            chain.doFilter(request, response);
        }
    
        @Override
        public void destroy() {
    
        }
    }
    

    代码实测

    根据用户uid查询用户信息

    根据token查询用户信息,可以从控制台打印中看到使用 ThreadLocal 完成了对token的跨层传递,也没有影响方法原始对外的入参结构

    总结

    根据上面的代码实践,ThreadLocal 使用起来也没有那么的复杂,但是在多线程编程中合理的使用 ThreadLocal 能够让你在工作中提高效率,代码更加简洁优雅。在使用 ThreadLocal 时需要注意的点,就是可能会产生 内存泄露 的问题,下面这张图将揭示了 ThreadLocal 和 Thread 以及 ThreadLocalMap 三者的关系。

    1. 在Thread类的源码中有一个 threadLocals,就是ThreadLocalMap
    2. ThreadLocalMap的Entry中的key是ThreadLocal,值是我们自己设定的
    3. ThreadLocal是一个弱引用,当为null时,会被当成垃圾被JVM回收
    4. 敲重点,如果ThreadLocal是null了,也就是要被垃圾回收器回收了,但此时ThreadLocalMap生命周期和Thread是一样,它不会回收,这时候就出现了一个现象,ThreadLocalMap的key没了,但是value还在,这就造成了内存泄漏。

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

    问题探讨

    ThreadLocal在实践中还会遇到哪些问题?

    作者:云飞飞飞
    链接:https://juejin.cn/post/7072539663185133598
    来源:稀土掘金

    相关文章

      网友评论

        本文标题:面试官:不懂ThreadLocal,还谈什么并发编程?

        本文链接:https://www.haomeiwen.com/subject/cnyarrtx.html