ThreadLoacl全面解析

ThreadLocal是什么

ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量

ThreadLocal与synchronized的区别

虽然ThreadLocal模式与synchronized关键字都用于处理多线程并发访问变量的问题, 不过两者处理问题的角度和思路不同。
ThreadLocal强调的是线程数据隔离的问题,synchronized是多线程共享数据的问题

synchronized ThreadLocal
原理 同步机制采用’以时间换空间’的方式, 只提供了一份变量,让不同的线程排队访问 ThreadLocal采用’以空间换时间’的方式, 为每一个线程都提供了一份变量的副本,从而实现同时访问而相不干扰
侧重点 多个线程之间访问资源的同步 多线程中让每个线程之间的数据相互隔离

ThreadLocal的内部结构

早期设计

每个ThreadLocal都创建一个Map,然后用线程作为Map的key,要存储的局部变量作为Map的value,这样就能达到各个线程的局部变量隔离的效果。这是最简单的设计方法,JDK最早期的ThreadLocal 确实是这样设计的,但现在早已不是了

image

JDK8设计

JDK后面优化了设计方案,在JDK8中 ThreadLocal的设计是:每个Thread维护一个ThreadLocalMap,这个Map的key是ThreadLocal实例本身,value才是真正要存储的值Object。

具体的过程是这样的:

  • 每个Thread线程内部都有一个Map (ThreadLocalMap)
  • Map里面存储ThreadLocal对象(key)和线程的变量副本(value)
  • Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。
  • 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰

image

设计的好处

  • 这样设计之后每个Map存储的Entry数量就会变少。因为之前的存储数量由Thread的数量决定,现在是由ThreadLocal的数量决定。在实际运用当中,往往ThreadLocal的数量要少于Thread的数量。

  • Thread销毁之后,对应的ThreadLocalMap也会随之销毁,能减少内存的使用

弱引用和内存泄漏

弱引用(WeakReference),垃圾回收器一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

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

假设在业务代码中使用完ThreadLocal ,threadLocal Ref被回收了。

由于ThreadLocalMap只持有ThreadLocal的弱引用,没有任何强引用指向threadlocal实例, 所以threadlocal就可以顺利被gc回收,此时Entry中的key=null。

但是在没有手动删除这个Entry以及CurrentThread依然运行的前提下,也存在有强引用链 threadRef->currentThread->threadLocalMap->entry -> value ,value不会被回收, 而这块value永远不会被访问到了,导致value内存泄漏。

也就是说,ThreadLocalMap中的key使用了弱引用, 也有可能内存泄漏

ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏。

防止内存泄漏

  • 只要在使用完ThreadLocal,调用其remove方法删除对应的Entry,就能避免内存泄漏。

  • 由于ThreadLocalMap是Thread的一个属性,被当前线程所引用,所以它的生命周期跟Thread一样长。那么在使用完ThreadLocal之后,如果当前Thread也随之执行结束,ThreadLocalMap自然也会被gc回收,从根源上避免了内存泄漏。这种方式显然不好控制,特别是使用线程池的时候,线程结束是不会销毁的

为什么使用弱引用

ThreadLocalMap中的set/getEntry方法中,会对key为null(也即是ThreadLocal为null)进行判断,如果为null的话,那么是会对value置为null的。

这就意味着使用完ThreadLocal,CurrentThread依然运行的前提下,就算忘记调用remove方法,弱引用比强引用可以多一层保障:弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocalMap调用set,get,remove中的任一方法的时候会被清除,从而避免内存泄漏。

哈希冲突

ThreadLocalMap使用线性探测法(开放寻址)来解决哈希冲突的。

该方法一次探测下一个地址,直到有空的地址后插入,若整个空间都找不到空余的地址,则会进行rehash

举个例子,假设当前table长度为16,也就是说如果计算出来key的hash值为14,如果table[14]上已经有值,并且其key与当前key不一致,那么就发生了hash冲突,这个时候将14加1得到15,取table[15]进行判断,这个时候如果还是冲突会回到0,取table[0],以此类推,直到可以插入。

按照上面的描述,可以把Entry[] table看成一个环形数组

为什么使用开放寻址法

链表法指针需要额外的空间,故当结点规模较小时,开放寻址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放寻址法中的冲突,从而提高平均查找速度。

posted @ 2022-01-25 15:34  狻猊的主人  阅读(194)  评论(0编辑  收藏  举报