ThreadLoacal简介
ThreadLocal类是修饰变量的,重点是在控制变量的作用域,初衷可不是为了解决线程并发和线程冲突的,而是为了让变量的种类变的更多更丰富,方便人们使用罢了。
根据变量的作用域,可以将变量分为全局变量,局部变量。简单的说,类里面定义的变量是全局变量,函数里面定义的变量是局部变量。
还有一种作用域是线程作用域,线程一般是跨越几个函数的。为了在几个函数之间共用一个变量,所以才出现:线程变量,这种变量在Java中就是ThreadLocal变量。
ThreadLocal变量,不同于它们的普通对应物,因为访问某个变量(通过其get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal为每个使用该变量的线程分配一个独立的变量副本。所以每一个线程都可以独立地改变自己的副本,而不会影响其他线程所对应的副本。所以我们说,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。
ThreadLocal的用处
Web开发中常见到的一个问题:多用户session问题。
假设有多个用户需要获取用户信息,一个线程对应一个用户。在mybatis中,session用于操作数据库,那么设置、获取操作分别是session.set()、session.get(),如何保证每个线程都能正确操作达到想要的结果呢?
假如我们要设置一个变量,作为各个线程共享的变量,来存储session信息,那么当我们需要让每个线程独立地设置session信息而不被其它线程打扰,要怎么做呢?很容易想到了加锁,譬如synchronized,互斥同步锁synchronized自JDK1.5经过优化后,不会很消耗资源了,但当成千上万个操作来临之时,扛高并发能力不说,数据返回延迟带来的用户体验变差又如何解决?
那么,就上文提出的问题,引申出来,像mybatis,hibernate一类的框架是如何解决这个session问题的呢?
来看一下,mybatis的SqlSessionManager类:
1 | public class SqlSessionManager implements SqlSessionFactory, SqlSession { |
留意到,mybatis里的localSqlSession就是用的ThreadLocal变量来实现。
从内存模型出发看ThreadLocal:
我们知道,在虚拟机中,堆内存就是用于存储共享数据,也就是这里所说的主内存。
每个线程将会在堆内存中开辟一块空间叫做线程的工作内存,附带一块缓存区用于存储共享数据副本。那么,共享数据在堆内存当中,线程通信就是通过主内存为中介,线程在本地内存读并且操作完共享变量操作完毕以后,把值写入主内存。
- ThreadLocal被称为线程局部变量,说白了就是线程工作内存的一小块内存,用于存储数据。
- 那么,ThreadLocal.set()、ThreadLocal.get()方法,就相当于把数据存储于线程本地,取也是在本地内存读取。就不会像synchronized需要频繁的修改主内存的数据,再把数据复制到工作内存,也大大提高访问效率。
那么,我们再来回答上面引出的问题,mybatis为什么要用ThreadLocal来存储session?
首先,因为线程间的数据交互是通过工作内存与主存的频繁读写完成通信,然而存储于线程本地内存,提高访问效率,避免线程阻塞造成cpu吞吐率下降。再者,在多线程中,每一个线程都各自维护session,轻易完成对线程独享资源的操作。
理解ThreadLocal的关键源码
首先,要理解ThreadLocal的数据结构,我们可以看它的set/get方法:
ThreadLocal.java
1 | public void set(T value) { |
Thread.java
1 | ThreadLocal.ThreadLocalMap threadLocals = null; |
- ThreadLocalMap作为ThreadLocal的静态内部类,用于存储多个ThreadLocal对象
- ThreadLocal对象作为ThreadLocalMap的key来存储,我们set进去的独享数据作为value存储
- 留意到它里边调到的getMap(Thread)方法,得知ThreadLocalMap的获取跟当前Thread有关,仔细看threadLocals其实就是当前线程的一个ThreadLocalMap变量。也就是说,一个线程对应一个ThreadLocalMap,get()就是当前程获取自己的ThreadLocalMap。
1 | public T get() { |
线程根据使用那一小块的线程本地内存,以ThreadLocal对象作为key,去获取存储于ThreadLocalMap中的值。
ThreadLocal内存泄露
引用关系图
先引用一张经典的引用关系图来说明当前线程(currentThread)以及threadLocalMap、key、threadLocal实例几个之间的引用关系:
利用这图来回顾总结一下ThreadLocal的实现:
每个Thread 维护一个 ThreadLocalMap 映射表,这个映射表的 key 是 ThreadLocal实例本身,value 是真正需要存储的 Object。
也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。值得注意的是图中的虚线,表示 ThreadLocalMap 是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。
ThreadLocal为什么会内存泄漏
我们可以理解到,每个线程都会创建一块工作内存,每个线程都有一个ThreadLocalMap,而ThreadLocalMap可以有多个key,也就是说可以存储多个ThreadLocal。那么假设,开启1万个线程,每个线程创建1万个ThreadLocal,也就是每个线程维护1万个ThreadLocal小内存空间!
那么,当线程执行结束以后,如果一个ThreadLocal没有外部强引用来引用它而是用弱引用来引用,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。
Key使用什么引用才好?
如上,key对ThreadLocal使用弱引用会发生内存泄露。
那么,如果使用强使用,问题是否就得以解决?
若 key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
那么如果 key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的引用是弱引用,即使没有手动删除,ThreadLocal也会被回收。至于value,则在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
所以比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,而对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
ThreadLocal防内存泄露最佳实践
综上,我们可以理解ThreadLocal为避免内存泄露的设计大致上是:
- JVM利用ThreadLocalMap的Key为弱引用,来避免ThreadLocal内存泄露。
- 由于Key设置为弱引用,那么,当ThreadLocal存储很多Key为null的Entry的时候,而不再去调用remove、get、set方法,那么将导致内存泄漏。
所以,每次使用完ThreadLocal,都调用它的remove()方法,清除数据,则可以达到回收弱引用的结果,这是最佳的使用实践。否则,在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。
Reference
扫描二维码,分享此文章