问到ThreadLocal,看这一篇就够了

ThreadLocal 是什么?

ThreadLocal 是线程本地变量。当使用 ThreadLocal 维护变量时,ThreadLocal 为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程。

ThreadLocal 使用场景

主要有以下三种场景。

  • 作为数据副本,当某些数据是以线程为作用域并且不同线程有不同数据副本时,可以考虑 ThreadLocal。

  • 保存线程上下文信息,在任意需要的地方可以获取,避免显示传参。

  • 解决线程安全问题,避免某些情况需要考虑线程安全必须同步带来的性能损失。

ThreadLocal的原理和实现

一句话总结:每个线程都有一个 ThreadLocalMap(ThreadLocal内部类),Map 中元素的键为 ThreadLocal,而值对应线程的变量副本。Map 是数组实现,使用线性探测解决hash冲突,需要手动调用set、get、remove防止内存泄漏。

ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。

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

一个线程内可以存在多个 ThreadLocal 对象,所以其实是 ThreadLocal 内部维护了一个 Map ,这个 Map 不是直接使用的 HashMap ,而是 ThreadLocal 实现的一个叫做 ThreadLocalMap 的静态内部类。而我们使用的 get()、set() 方法其实都是调用了这个ThreadLocalMap类对应的 get()、set() 方法。

调用 threadLocal.set() –> 调用 getMap(Thread) –> 返回当前线程的 ThreadLocalMap < ThreadLocal, value >–>map.set(this, value),this 是 ThreadLocal

调用 get() –> 调用getMap(Thread) –> 返回当前线程的 ThreadLocalMap<ThreadLocal, value>–>map.getEntry(this),返回value。

这个储值的Map并非ThreadLocal的成员变量,而是java.lang.Thread 类的成员变量。ThreadLocalMap实例是作为java.lang.Thread的成员变量存储的,每个线程有唯一的一个threadLocalMap。这个map以ThreadLocal对象为key,”线程局部变量”为值,所以一个线程下可以保存多个”线程局部变量”。对ThreadLocal的操作,实际委托给当前Thread,每个Thread都会有自己独立的ThreadLocalMap实例,存储的仓库是Entry[] table;Entry的key为ThreadLocal,value为存储内容;因此在并发环境下,对ThreadLocal的set或get,不会有任何问题。

由于Tomcat线程池的原因,我最初使用的”线程局部变量”保存的值,在下一次请求依然存在(同一个线程处理),这样每次请求都是在本线程中取值。所以在线程池的情况下,处理完成后主动调用该业务treadLocal的remove()方法,将”线程局部变量”清空,避免本线程下次处理的时候依然存在旧数据。

ThreadLocal 并不是用来解决共享资源的多线程访问的问题,因为每个线程中的资源只是副本,并不共享。因此ThreadLocal适合作为线程上下文变量,简化线程内传参。

问到ThreadLocal,看这一篇就够了
//ThreadLocal
public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
//ThreadLocal.ThreadLocalMap
private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    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)]) {
        ThreadLocal<?> k = e.get();

      if (k == key) {
            e.value = value;
            return;
        }

  // 代表key弱引用已被回收
        
        if (k == null) {
          // 如果被回收 需要替换
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot)
 
{
            Entry[] tab = table;
            int len = tab.length;
            Entry e;

            // Back up to check for prior stale entry in current run.
            // We clean out whole runs at a time to avoid continual
            // incremental rehashing due to garbage collector freeing
            // up refs in bunches (i.e., whenever the collector runs).
           // 向前遍历 检查是否有已经被回收的key
            int slotToExpunge = staleSlot;
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;

            // Find either the key or trailing null slot of run, whichever
            // occurs first
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();

                // If we find key, then we need to swap it
                // with the stale entry to maintain hash table order.
                // The newly stale slot, or any other stale slot
                // encountered above it, can then be sent to expungeStaleEntry
                // to remove or rehash all of the other entries in run.
              // 遇到相同的需要替换value
                if (k == key) {
                    e.value = value;

                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    // Start expunge at preceding stale entry if it exists
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                // If we didn't find stale entry on backward scan, the
                // first stale entry seen while scanning for key is the
                // first still present in the run.
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            // If key not found, put new entry in stale slot
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            // If there are any other stale entries in run, expunge them
            // 如果有其他的过期对象会清理
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

ThreadLocal 内存泄露问题

在ThreadLocal中内存泄漏是指ThreadLocalMap中的Entry中的key为null,而value不为null。因为key为null导致value一直访问不到,而根据可达性分析导致在垃圾回收的时候进行可达性分析的时候,value可达从而不会被回收掉,但是该value永远不能被访问到,这样就存在了内存泄漏。

解决方案:对应的 value 在下一次 ThreadLocalMap 调用 set、get、remove 方法时被清除,但是这几个方法需要被显式调用。

为什么要使用弱引用

一句总结:key如果是强引用会导致线程不被回收,key对应ThreadLocal也不被回收,所以要改为弱引用。至于value一定是强引用,所以必须用完调用remove方法。

Map中的key为一个threadlocal实例.如果使用强引用,当ThreadLocal对象(假设为ThreadLocal@123456)的引用被回收了,ThreadLocalMap本身依然还持有ThreadLocal@123456的强引用,如果没有手动删除这个key,则ThreadLocal@123456不会被回收,所以只要当前线程不消亡,ThreadLocalMap引用的那些对象就不会被回收,可以认为这导致Entry内存泄漏。

如果使用弱引用,那指向ThreadLocal@123456对象的引用就两个:ThreadLocal强引用和ThreadLocalMap中Entry的弱引用。一旦ThreadLocal强引用被回收,则指向ThreadLocal@123456的就只有弱引用了,在下次gc的时候,这个ThreadLocal@123456就会被回收。

虽然上述的弱引用解决了key,也就是线程的ThreadLocal能及时被回收,但是value却依然存在内存泄漏的问题。当把threadlocal实例置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收.map里面的value却没有被回收.而这块value永远不会被访问到了. 所以存在着内存泄露,因为存在一条从current thread连接过来的强引用.只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收.所以当线程的某个localThread使用完了,马上调用threadlocal的remove方法,就不会发生这种情况了。

另外其实只要这个线程对象及时被gc回收,这个内存泄露问题影响不大,但在threadLocal设为null到线程结束中间这段时间不会被回收的,就发生了我们认为的内存泄露。最要命的是线程对象不被回收的情况,这就发生了真正意义上的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用,就可能出现内存泄露。

ThreadLocalMap 和 HashMap 区别

ThreadLocalMap 和HashMap的功能类似,但是实现上却有很大的不同:

HashMap 的数据结构是数组+链表,HashMap 是通过链地址法解决hash 冲突的问题,HashMap 里面的Entry 内部类的引用都是强引用。

ThreadLocalMap的数据结构仅仅是数组,ThreadLocalMap 是通过开放地址法来解决hash 冲突的问题,ThreadLocalMap里面的Entry 内部类中的key 是弱引用,value 是强引用

链地址法和开放地址法

jdk 中大多数的类都是采用了链地址法来解决hash 冲突,为什么ThreadLocalMap 采用开放地址法来解决哈希冲突呢?首先我们来看看这两种不同的方式

链地址法

这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。列如对于关键字集合{12,67,56,16,25,37, 22,29,15,47,48,34},我们用前面同样的12为除数,进行除留余数法:

问到ThreadLocal,看这一篇就够了

开放地址法

这种方法的基本思想是一旦发生了冲突,就去寻找下一个空的散列地址(这非常重要,源码都是根据这个特性,必须理解这里才能往下走),只要散列表足够大,空的散列地址总能找到,并将记录存入。

比如说,我们的关键字集合为{12,33,4,5,15,25},表长为10。我们用散列函数f(key) = key mod l0。当计算前S个数{12,33,4,5}时,都是没有冲突的散列地址,直接存入(蓝色代表为空的,可以存放数据):

问到ThreadLocal,看这一篇就够了

计算key = 15时,发现f(15) = 5,此时就与5所在的位置冲突。于是我们应用上面的公式f(15) = (f(15)+1) mod 10 =6。于是将15存入下标为6的位置。这其实就是房子被人买了于是买下一间的作法:

问到ThreadLocal,看这一篇就够了

链地址法和开放地址法的优缺点

开放地址法:

容易产生堆积问题,不适于大规模的数据存储。

散列函数的设计对冲突会有很大的影响,插入时可能会出现多次冲突的现象。

删除的元素是多个冲突元素中的一个,需要对后面的元素作处理,实现较复杂。

链地址法:

处理冲突简单,且无堆积现象,平均查找长度短。

链表中的结点是动态申请的,适合构造表不能确定长度的情况。

删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。

指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间。

ThreadLocalMap 采用开放地址法原因

ThreadLocal 中看到一个属性 HASH_INCREMENT = 0x61c88647 ,0x61c88647 是一个神奇的数字,让哈希码能均匀的分布在2的N次方的数组里, 即 Entry[] table,关于这个神奇的数字google 有很多解析,这里就不重复说了

ThreadLocal 往往存放的数据量不会特别大(而且key 是弱引用又会被垃圾回收,及时让数据量更小),这个时候开放地址法简单的结构会显得更省空间,同时数组的查询效率也是非常高,加上第一点的保障,冲突概率也低

推荐:

最全的java面试题库

问到ThreadLocal,看这一篇就够了
PS:因为公众号平台更改了推送规则,如果不想错过内容,记得读完点一下“在看”,加个“星标”,这样每次新文章推送才会第一时间出现在你的订阅列表里。“在看”支持我们吧!

原文始发于微信公众号(Java笔记虾):问到ThreadLocal,看这一篇就够了

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/47570.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!