多线程基础(三)ThreadLocal

多个线程共享变量可以使用static实现,那如果想实现每个线程有自己的共享变量怎样实现呢?
我们先来讨论一个实际问题:(当然这个方案并不是一个好主意因为发生异常时可能导致value无法被清理-详见倒数第二句话)
web后台项目用户登录后操作网数据库插入一条数据,我们不希望在controller、service、dao之间显式的定义参数传递用户名等信息,我们可以在登录拦截器里把校验后的用户信息保存到一个地方,然后再dao层直接取出,这样插入数据的时候就可以知道是谁操作的,这个功能可以使用ThreadLocal实现。
第一步:定义一个用户全局上下文对象

public class UserContextHolder {
    public static ThreadLocal<String> context = new ThreadLocal<>();
}

第二部:登录拦截器保存用户名到ThreadLocal

public class MyInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //从sso系统获取用户名,这里假设是tom
        UserContextHolder.context.set("tom");
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

第三步:dao中获取用户名

    public void insert(){
        String name = UserContextHolder.context.get();
        System.out.println("dao层拿到用户名"+name);
    }

- 源码
第一次设置值时调用的set方法

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

可以看到取不到ThreadLocalMap时会执行 createMap(t, value);

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

这里可以看到,方法是把当前线程对象的threadlocals属性初始化了,ThreadLocalMap我们先把它理解成一个map后面再详细解释他,可以看到map的key和当前ThreadLocal对象有关,值就是我们自己设置的值
先看一下Thread类确实有一个threadlocals属性,这个属性创建线程时不会自己初始化,只有当调用ThreadLocal的set、get方法时才会初始化

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

接下来看一下ThreadLocalMap的构造方法

        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

ThreadLocalmap中有一个Entry数组private Entry[] table;
Entry是一个弱引用对象

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

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

不熟悉弱引用的同学可以先把它理解为一个key-value对象帮助理解
table[i] = new Entry(firstKey, firstValue);设置数组[i]位置为一个Entry对象,对象的key为当前ThreadLocal对象,value是我们设置的值。
在同一个线程中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();
    }

可以看到是从Thread对象中获取threadlocals(其实是一个ThreadLocalMap),然后map不为空就获取当前ThreadLocal对象为key的Entry对象,然后获取值,最后如果都没有的话会执行初始化方法setInitialValue();

    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);
        return value;
    }
    protected T initialValue() {
        return null;
    }

初始化默认会设置一个null到ThreadLocalMap,我们可以自定义一个类继承ThreadLocal重写initialValue()方法设置初始值。
分析完以上部分我们大概就可以知道ThreadLocal的原理了,看图
在这里插入图片描述

3. ThreadLocal,内存泄漏问题
前面我们说到ThreadLocalMap内部有一个Entry数组,Entry的key为ThreadLocal对象的弱引用,弱引用对象在没有强引用的情况下在系统gc时会被回收。
在这里插入图片描述
如果再一个执行时间很长(甚至和应用生命周期一样长)的线程中Threadlocal对象一但没有强引用那么JVM在Gc时会回收该ThreadLocal对象,也就是Thread->threadLocals(ThreadLocalMap)->table(Entry[])->Entry->key将为null,这种情况下value已无法访问但是却还是强引用不会被回收。
所以前面我们把ThreadLocal定义成了static的,static也代表了ThreadLocal对象和该类的生命周期一样长,使用remove方法可以手动移除相关线程的value及ThreadLocal引用

        private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }
        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            。。。。。。
        }

因此我们可以在value使用结束后remove一下,所以我们最开始的拦截器案例好多人会加这样一句

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        UserContextHolder.context.remove();
    }

如果每次请求响应都是一个新的线程,响应结束后该线程也销毁的话,我们就可以不必清空value。当然tomcat使用线程池处理用户请求,当然这个方案并不是一个好主意因为发生异常时可能导致value无法被清理,因为线程执行不到postHandle方法。
在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值