线程本地变量
为什么要使用ThreadLocal:
- 多个线程之间访问自己的变量副本,互不干扰。
- 一个线程执行过程涉及多个方法,方法和方法之间省去参数传递(spring事务)
(一)ThreadLocal的使用
ThreadLocal类接口很简单,只有4个方法,我们先来了解一下:
• void set(Object value)
设置当前线程的线程局部变量的值。
• public Object get()
该方法返回当前线程所对应的线程局部变量。
• public void remove()
将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
• protected Object initialValue()
返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。
(二)ThreadLocal的实现
2.1 实现分析
怎么实现ThreadLocal,既然说让每个线程都拥有自己变量的副本,最容易的方式就是用一个Map将线程的副本存放起来,Map里key就是每个线程的唯一性标识,比如线程ID,value就是副本值,实现起来也很简单:
考虑到并发安全性,对数据的存取用synchronize关键字加锁,但是DougLee在《并发编程实战》中为我们做过性能测试
可以看到ThreadLocal的性能远超类似synchronize的锁实现ReentrantLock,比我们后面要学的AtomicInteger也要快很多,即使我们把Map的实现更换为Java中专为并发设计的ConcurrentHashMap也不太可能达到这么高的性能。
怎么样设计可以让ThreadLocal达到这么高的性能呢?最好的办法则是让变量副本跟随着线程本身,而不是将变量副本放在一个地方保存,这样就可以在存取时避开线程之间的竞争。
同时,因为每个线程所拥有的变量的副本数是不定的,有些线程可能有一个,有些线程可能有2个甚至更多,则线程内部存放变量副本需要一个容器,而且容器要支持快速存取,所以在每个线程内部都可以持有一个Map来支持多个变量副本,这个Map被称为ThreadLocalMap。
2.2 具体实现
上面先取到当前线程,然后调用getMap方法获取对应的ThreadLocalMap,ThreadLocalMap是一个声明在ThreadLocal的静态内部类,然后Thread类中有一个这样类型成员变量,也就是ThreadLocalMap实例化是在Thread内部,所以getMap是直接返回Thread的这个成员。
看下ThreadLocal的内部类ThreadLocalMap源码,这里其实是个标准的Map实现,内部有一个元素类型为Entry的数组,用以存放线程可能需要的多个副本变量。
(三)引发内存泄漏
内存泄露为程序在申请内存后,无法释放已申请的内存空间,一次内存泄露危害可以忽略,但内存泄露堆积后果很严重,无论多少内存,迟早会被占光,
广义并通俗的说,就是:不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。
3.1 强引用与弱引用
强引用,使用最普遍的引用,一个对象具有强引用,不会被垃圾回收器回收。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不回收这种对象。
如果想取消强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样可以使JVM在合适的时间就会回收该对象。
弱引用,JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。可以在缓存中使用弱引用。
3.2 ThreadLocal的内存泄露分析
ThreadLocal的实现原理,每一个Thread维护一个ThreadLocalMap,key为使用 弱引用的ThreadLocal实例,value为线程变量的副本。这些对象之间的引用关系如下,实线箭头表示强引用,虚线箭头表示弱引用
从上图中可以看出,hreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal不存在外部 强引用时,Key(ThreadLocal)势必会被GC回收,这样就会导致ThreadLocalMap中key为null, 而value还存在着强引用,只有thead线程退出以后,value的强引用链条才会断掉。
但如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value
永远无法回收,造成内存泄漏。
3.2 为什么建议把ThreadLocal修饰成static
- 防止内存泄漏:ThreadLocal不会被GC给回收,也就随时可以进行remove
- ThreadLocal能够实现线程数据隔离,不在于它自己本身,而在于Thread的ThreadLocalMap,所以ThreadLocal只需要初始化一次即可
- ThreadLocal作为ThreadLocalMap的key存在,Map的key又不能重复存在,所以只初始化一次的ThreadLocal还能避免不必要的内存泄漏问题。