ThreadLocal相当于一个装饰器,装饰一个变量,通常将ThreadLocal实例定义为静态的。

其作用是既使得各线程都可访问到该实例,又使各线程访问到的实际上是本线程私有的变量副本而不用进行同步。

若不使用ThreadLocal,要实现上述两个效果,前者可以通过声明一个静态变量实现,但此时存在线程安全问题;后者可以通过在每个线程中声明一个变量实现,但变量无法在不同线程中共享。

 

对于多线程资源共享的问题,传统的同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。

前者仅提供一份变量,让不同的线程排队访问;

而后者每一个线程都有自己的一份变量副本(逻辑上的,实际上并没有复制什么副本,因为每个线程有自己的ThreadLocalMap成员,key为ThreadLocal实例的弱引用、value为变量的值,操作ThreadLocal变量实际上是操作线程自己的ThreadLocalMap,如下图所示),所以线程间对该变量的访问也就不存在竞争了,因此可以同时访问而互不影响(感觉实际上只是个语法糖,因为可以为每个线程创建一个变量实现等价效果)。但ThreadLocal变量的这种隔离策略也不是任何情况下都能使用的。如果多个线程并发访问的对象实例只允许或只能创建一个,那只能使用同步机制来访问。

Java ThreadLocal

ThreadLocal的典型用途是存储上下文信息,避免在不同代码间来回传递,简化代码。比如在一个Web服务器中,一个线程执行用户的请求,在执行过程中,很多代码都会访问一些共同的信息,比如请求信息、用户身份信息、数据库连接、当前事务等,它们是线程执行过程中的全局信息,如果作为参数在不同代码间传递,代码会很啰嗦,这时,使用ThreadLocal就很方便(设置个static ThreadLocal变量),所以它被用于各种框架如Spring中。

真实实践示例:项目中使用到Jedis客户端,然而不想直接使用Jedis,因为使用到的地方就需要处理连接过程,因此打算对之封装一层——将连接处理封装起来,封装成JedisUtil,JedisUtil暴露少量接口供外调用。然而使用过程中遇到一个问题:在JedisUtil中维护一个公共连接时,llen、lpush等可以功能正常使用,然而对于brpop等就有问题了:后者阻塞等待连接返回数据,若与llen等共用一个连接,llen、brpop的返回会交杂在一起,从而出错。因此需要为brpop维护单独的连接来执行该命令,该连接与调用者线程挂钩,这时用ThreadLocal就很方便了。

JDK建议将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露。

 

更多详情:

http://www.jasongj.com/java/threadlocal/

http://mp.weixin.qq.com/s/Cuq75N6HKMYJNM04fYkCvw

 

ThreadLocalMap的memory leak问题

相关概念:

ThreadLocal Ref:ThreadLocal引用,或称ThreadLocal变量

TThreadLocal对象:ThreadLocal Ref所指向的堆上的对象

Thread Ref:线程变量

Thread对象

ThreadLocalMap、ThreadLocalMap.entry

 

Memory Leadk问题:(详情参阅:ThreadLocal Memry Leak问题

指对象无法被回收的问题,这里的对象包括:ThreadLocal对象,该ThrealLocal对象在各线程的ThreadLocaMap中对应的Entry对象、Value对象。ThreadLocal对象与对应的Enry的Key对象是同一个对象。

ThreadLocal Ref(即ThreadLocal变量)是强引用类型,要使得ThreadLocal对象被回收,直接将该引用变量置null(ThreadLocal变量通常是静态变量,故置1次即可)即可,垃圾回收器会进行回收。在此前提下:

1 若ThreadLocalMap的key是强引用(Strong Reference)类型,则存在ThreadLocal对象无法被回收的问题。

即使将ThreadLocal Ref 变量赋为了null,只要存在 用过该ThreadLocal变量 的线程尚未结束 且 此线程未删除该变量对应的Entry(即未调用该变量的remove方法),则就存在 CurrentThread Ref → CurrentThread对象 →Map(ThreadLocalMap)对象-> entry对象 -> ThreadLocal对象 的强引用链,从而ThreadLocal对象无法被回收。示意如下:

Java ThreadLocal

实际上,此时 该ThreadLocal对象在各线程的ThrealLocalMap中对应的Entry对象、Value对象也无法被回收,除非手动删除(即调用ThreadLocal的remove方法)

2 若ThreadLocalMap的key是弱引用(Weak Reference)类型 ,则不存在ThreadLocal对象无法被回收的问题,但仍存在对应的Entry对象、Value对象无法被回收的问题

将ThreadLocal Ref 变量赋为了null后,由于key是弱引用类型时,其引用的对象虽然有被引用但仍会在存活过一次GC后被回收,从而ThreadLocal对象可被正常回收,此时key变为null。

在未手动删除对应的Entry及用过该ThreadLocal变量的线程尚未结束的情况下,同样存在到该Entry对象、Value对象的强引用链,故这两者仍无法被回收。示意图如下:

Java ThreadLocal

Java ThreadLocal

 

可见导致Memory Leak(ThreadlLocal对象未被回收)的原因有两个:用到ThreadlLocal对象的线程还在指向、该对象对应的entry未删除,从而存在对该对象的强引用。

3 解决方案:在方案2的基础上增加额外处理逻辑——在 ThreadLocalMap 的put/get/remove/getEntry等方法中会判断key是否为null(即key指向的ThreadLocal是否被回收了),若是则会删除该key对应的entry、value,从而解决方案2存在的问题。相关源码:

        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                    int h = k.threadLocalHashCode & (len - 1);
                    if (h != i) {
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }
View Code

相关文章:

  • 2020-05-16
  • 2021-09-09
  • 2021-11-21
  • 2021-12-15
  • 2021-04-07
  • 2021-09-26
  • 2021-06-06
猜你喜欢
  • 2021-07-05
  • 2021-08-19
  • 2022-01-04
相关资源
相似解决方案