ThreadLocal 原理以及使用

本文详细介绍了Java中的ThreadLocal用法,如何在线程中存储私有对象,避免多线程并发问题。同时,分析了ThreadLocal底层结构,包括其弱引用键的设计,以防止内存泄漏。文章还探讨了ThreadLocalMap的碰撞处理策略,以及为何不采用HashMap的处理方式。最后,强调了在使用ThreadLocal后调用remove方法的重要性,以防止内存泄漏问题。
摘要由CSDN通过智能技术生成


在多线程(java web)情况下,在线程类中使用 ThreadLocal 可以为每个线程配置私有的对象

    ThreadLocal<Object> threadLocal = new ThreadLocal<>();

用法

比如可以这样使用,用 ThreadLocal 保存用户信息。在用户登录拦截时,通过校验的用户可以将该用户常用信息放进 ThreadLocal,在这次请求时可以随时取出来使用,不是公用属性不会存在多线程并发问题:

public class RequestContextCache {

    private static final ThreadLocal<CrmUserInfo> USER_INFO_CACHE = new ThreadLocal<>();

    public static HttpServletRequest getRequest() {
        return (RequestContextHolder.getRequestAttributes()) == null ?
                null : ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
    }

    public static HttpServletResponse getResponse() {
        return ((ServletWebRequest) Objects.requireNonNull(RequestContextHolder.getRequestAttributes())).getResponse();
    }

    public static CrmUserInfo getUser() {
        return USER_INFO_CACHE.get();
    }

    public static void updateStatus(CrmUserStatusEnum crmUserStatusEnum) {
        USER_INFO_CACHE.get().setStatus(crmUserStatusEnum);
    }

    public static void register(CrmUserInfo loginUserInfo) {
        if (USER_INFO_CACHE.get() != null) {
            return;
        }
        USER_INFO_CACHE.set(loginUserInfo);
    }

    public static void clear() {
        USER_INFO_CACHE.remove();
    }
}

底层结构

这个类中只有一个 ThreadLocal 对象,但是每一个线程中都有不同的 ThreadLocalMap

最终的变量是放在了当前线程的 ThreadLocalMap 中,并将 ThreadLocal 这个对象的弱引用作为键。而 ThreadLocalMap 被定义成了 Thread 类的成员变量

	// ThreadLocal 的 set 方法可以说明一切
    public void set(T value) {
    	// 该方法用于获取当前线程对象
        Thread t = Thread.currentThread();
        // ThreadLocalMap 是 ThreadLocal 的内部类,这让每个线程都有一个 map
        ThreadLocalMap map = getMap(t);
        // map 没有得到,创建一个当前线程的 map
        if (map != null) {
            map.set(this, value);
        } else {
            // 有 map 则向 map 中存值
            createMap(t, value);
        }
    }

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    
		// new ThreadLocalMap 构造函数
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        	// INITIAL_CAPACITY = 16,private Entry[] table; table 为散列表
            table = new Entry[INITIAL_CAPACITY];
            // 哈希函数
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            // 扩容阈值,散列表中的值超过这个数会触发扩容
            setThreshold(INITIAL_CAPACITY);
        }

get 方法如下,如果原先 ThreadLocalMap 中没有值会返回 null,ThreadLocalMap 中的 key 是 ThreadLocal 对象本身,值是我们调用接口存入的值

    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();
    }

存入 map 中的 entry 继承了弱引用

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

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

因此 Entry 可以转换为:

        static class Entry {
            Object value;
            private ThreadLocal referent;

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

为啥要使用弱引用

让一个弱引用当值主要是为了防止内存泄漏,当 ThreadLocal 需要被回收的时候,如果在 map 中的键是强引用,那么这个对象是无法被回收掉的

我们都知道 ThreadLocal 变量对 ThreadLocal 对象是有强引用存在的。即使 ThreadLocal 变量生命周期完了,设置成 null 了,但由于这个 ThreadLocalMap 中的 Entry 对 ThreadLocal 还是强引用,此时,这个 ThreadLocal 是不会被销毁的

当然将 ThreadLocal 设置成 static 则是例外,此时它被存放在方法区里

就算这么做还是有问题,因为 ThreadLocalMap 的键为弱引用,值为强引用,当所有的线程都没有引用这个对象并且发生 GC 后,键的指向都为 null,值的指向对象依然没有被回收,产生了 OOM 问题

java 为了处理这种问题,定义了方法 replaceStaleEntry,如果系统中 ThreadLocal 变量,调用了它的 get、set 或 remove,三个方法中的任何一个方法,都会自动触发清理机制,将 key 为 null 的 value 值清空。如果 key 和 value 都是 null,那么 Entry 对象会被 GC 回收。如果所有的 Entry 对象都被回收了,属于线程的 ThreadLocalMap 也会被回收了

但是就算 java 做了这样的处理,也是有可能发生内存泄漏问题的。如果 ThreadLocal 被回收后,一直没有其他的 ThreadLocal 调用 get、set 或 remove 方法,就一定会存在 value 的引用

所以在开发的时候,需要使用完 ThreadLocal 之后习惯性的调用 ThreadLocal 对象的 remove 方法(本身来说,直接用 ThreadLocal = null 这种用法就是错误的,应当使用 remove 方法来清除数据)

那么我们回头来看看 hashMap,弱引用这么好,为什么 hashMap 不使用弱引用优化一下 key 呢?因为没必要,我们不可能使用 map = null 这种语句来删除 key,我们都是调用 remove 方法,因此根本不可能发生内存泄漏

碰撞处理

ThreadLocalMap 类似 hashmap,但是所使用的 hash 函数、碰撞处理等方法大不相同

碰撞处理使用动态寻址法,当哈希碰撞发生时,从发生碰撞的那个单元起,按照一定的次序,从哈希表中寻找一个空闲的单元,然后把发生冲突的元素存入到该单元。这个空闲单元又称为开放单元或者空白单元

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    //判断Entry对象如果不为空,则一直循环
    while (e != null) {
        ThreadLocal<?> k = e.get();
        //如果当前Entry的key正好是我们所需要寻找的key
        if (k == key)
            //说明这次真的找到数据了
            return e;
        if (k == null)
            //如果key为空,则清理脏数据
            expungeStaleEntry(i);
        else
            //如果还是没找到数据,则继续往后找
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

为什么不和 hashMap 一样使用列表法呢?它定义了一个 Entry[] 做散列表,最直观的原因是数据量不会像 hashmap 一样,它的数据量少,因此可以用简单的方法实现

在 ThreadLocalMap.set() 方法的最后,如果执行完启发式清理工作后,未清理到任何数据,且当前散列数组中 Entry 的数量已经达到了列表的扩容阈值(len*2/3),就开始执行 rehash() 逻辑,也就是扩容处理

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值