ThreadLocal 的学习理解及实战中应用
阅读原文时间:2021年04月20日阅读:1
  • ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
  • 设计初衷:提供线程内部的局部变量,在本线程内随时随地可取,隔离其他线程。

ThreadLocal基本操作:

  1. initialValue函数 : initialValue函数用来设置ThreadLocal的初始值,该函数在调用get函数的时候会第一次调用,但是如果一开始就调用了set函数,则该函数不会被调用。

  2. get函数:该函数用来获取与当前线程关联的ThreadLocal的值,如果当前线程没有该ThreadLocal的值,则调用initialValue函数获取初始值返回。

  3. set函数: set函数用来设置当前线程的该ThreadLocal的值。

  4. remove函数:remove函数用来将当前线程的ThreadLocal绑定的值删除。

    public class thread_local2 {

    private static final ThreadLocal<Integer> value = new ThreadLocal<Integer>(){
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };
    static class MyThread extends Thread{
    private int index;
    
    public MyThread(int index){
        this.index = index;
    }
    
    public void run(){
        System.out.println("线程"+index+":的初始value="+value.get());
        for (int i = 0; i &lt; 10; i++) {
            value.set(value.get()+i);
            if(i == 5){
                value.remove();
            }
        }
        System.out.println("线程"+index+":的累加始value="+value.get());
    }
    } public static void main(String[] args) { for (int i = 0; i < 5; i++) { new Thread(new MyThread(i)).start(); } }

    }

输出:

线程3:的初始value=0
线程3:的累加始value=30
线程0:的初始value=0
线程4:的初始value=0
线程2:的初始value=0
线程0:的累加始value=30
线程1:的初始value=0
线程1:的累加始value=30
线程4:的累加始value=30
线程2:的累加始value=30

ThreadLocal底层原理:

  1. JDK8的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();
    }

其中getMap()的源码:

ThreadLocalMap getMap(Thread t) {
      return t.threadLocals;
 }

简单解析一下,get方法的流程是这样的:

  1. 首先获取当前线程
  2. 根据当前线程获取一个Map
  3. 如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的value e,否则转到5
  4. 如果e不为null,则返回e.value,否则转到5
  5. Map为空或者e为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map

设计思路每个Thread维护一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal实例本身,value是真正需要存储的Object。

ThreadLocal底层深入:

  • ThreadLocalMap是使用ThreadLocal的弱引用作为Key的

    ThreadLocal会引发内存泄露,他们的理由是这样的:

    如上图,ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用引用他,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:

    ThreadLocal Ref -> Thread -> ThreaLocalMap -> Entry -> value

    永远无法回收,造成内存泄露。

  • JDK设计中的防范: 如ThreadLocalMap的getEntry函数的流程:(如果e为null或者key不一致则向下一个位置查询,如果下一个位置的key和当前需要查询的key相等,则返回对应的Entry,否则,如果key值为null,则擦除该位置的Entry,否则继续向下一个位置查询)set操作也有类似的思想,将key为null的这些Entry都删除,防止内存泄露。

  • 以上有一个前提,要调用ThreadLocalMap的*genEntry函数或者set函数。* 1. 所以很多情况下需要使用者手动调用ThreadLocal的remove函数,手动删除不再需要的ThreadLocal,防止内存泄露。2. JDK建议将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。

参考:https://www.zhihu.com/question/23089780 winwill2012

项目中的应用

  • 在一个网站中,我们输入用户名,密码登陆进去后服务端是怎么知道当前用户的信息的,这边就可以应用上Threallocal了。
使用hostHolder来包装当前用户:
@Component
public class HostHolder {
    private static ThreadLocal<User> users = new ThreadLocal<User>();

    public User getUser() {
        return users.get();
    }

    public void setUser(User user) {
        users.set(user);
    }

    public void clear() {
        users.remove();;
    }
}

当用户登陆的时候,使用拦截器将当前用户的信息set进去。

@Component
public class PassportInterceptor implements HandlerInterceptor {

    @Autowired
    private LoginTicketDAO loginTicketDAO;

    @Autowired
    private UserDAO userDAO;

    @Autowired
    private HostHolder hostHolder;

    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
        String ticket = null;
        if (httpServletRequest.getCookies() != null) {
            for (Cookie cookie : httpServletRequest.getCookies()) {
                if (cookie.getName().equals("ticket")) {
                    ticket = cookie.getValue();
                    break;
                }
            }
        }

        if (ticket != null) {
            LoginTicket loginTicket = loginTicketDAO.selectByTicket(ticket);
            if (loginTicket == null || loginTicket.getExpired().before(new Date()) || loginTicket.getStatus() != 0) {
                return true;
            }

            User user = userDAO.selectById(loginTicket.getUserId());
            hostHolder.setUser(user);
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {
        if (modelAndView != null && hostHolder.getUser() != null) {
            modelAndView.addObject("user", hostHolder.getUser());
        }
    }

    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {
        hostHolder.clear();
    }

这样,当我们需要使用当前用户信息时,直接从这个hostHolder中取出就行了。

   @Autowired
    HostHolder hostHolder;

    hostHolder.getUser().......