关于ThreadLocal的用法和说明及注意事项
作者:二旬老者丶
ThreadLocal
ThreadLocal是用于解决Java并发安全性问题的一个类。
其主要作用是防止不同线程中的数据冲突。
原理图
如下:
原理说明
创建一个ThreadLocal<V>类的对象,默认会在每一个线程中都开启一小片区域,该片区域可以理解为kay value格式的(实质上是在Thread中有内部类ThreadLocalMap,每声明了一个ThreadLocal,就相当于在这个ThreadLocalMap中设置了一个<key,value>,因为线程是相互独立的,所以ThreadLocalMap也是独立的),ThreadLocalMap中以ThreadLocal实例引用的变量名为key,V为value。
每一个V都是线程独有的!
使用
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。
实例!
public final static ThreadLocal<String> threadLocal= new ThreadLocal<String>();
threadLocal代表一个能够存放String类型的ThreadLocal对象。
此时不论什么一个线程能够并发访问这个变量,对它进行写入、读取操作,都是线程安全的。
注意!!!
ThreadLocal如果应用不妥当会导致内存泄漏。
先来说下什么是内存泄漏和内存溢出,内存泄漏是指某个变量申请了内存的资源,但是引用释放了,这样就导致占用着内存却不能访问到(俗话叫占着茅坑不拉屎!);
内存溢出是指某个变量在申请内存空间资源的时候需要的空间大于实际的空间,即为内存空间不足了(人太多坑不够了!)
如图解:
当写下 o=null时,只是表示o不再指向堆中object的对象实例,不代表这个对象实例不存在了。
下面来说明下Java中创建引用的几种方法
- 强引用就是指在程序代码之中普遍存在的,类似“Object obj=new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象实例。
- 软引用是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象实例列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。
- 弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象实例只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象实例。在JDK 1.2之后,提供了WeakReference类来实现弱引用。
- 虚引用也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象实例是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象实例被收集器回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。
这里只举一个软引用的例子:
SoftReference<String> ref = new SoftReference<String>("Hello world");
这样就设置了 ref 对内存中 "Hello world"的软引用。
ThreadLocal产生内存泄漏的原因
根据我们前面对ThreadLocal的分析,我们可以知道每个Thread 拥有一个 ThreadLocalMap,这个映射表的 key 是 ThreadLocal实例本身,value 是真正需要存储的 Object,也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。
仔细观察ThreadLocalMap,这个map是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。
图中的虚线表示弱引用。
这样,当把threadlocal变量置为null以后,没有任何强引用指向threadlocal实例,所以threadlocal将会被gc回收。这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,而这块value永远不会被访问到了,所以存在着内存泄露。
只有当前thread结束以后,current thread就不会存在栈中,强引用断开,Current Thread、Map value将全部被GC回收。最好的做法是不在需要使用ThreadLocal变量后,都调用它的remove()方法,清除数据。
所以回到我们前面的实验场景,场景3中,虽然线程池里面的任务执行完毕了,但是线程池里面的5个线程会一直存在直到JVM退出,我们set了线程的localVariable变量后没有调用localVariable.remove()方法,导致线程池里面的5个线程的threadLocals变量里面的new LocalVariable()实例没有被释放。
其实考察ThreadLocal的实现,我们可以看见,无论是get()、set()在某些时候,调用了expungeStaleEntry方法用来清除Entry中Key为null的Value,但是这是不及时的,也不是每次都会执行的,所以一些情况下还是会发生内存泄露。只有remove()方法中显式调用了expungeStaleEntry方法。
从表面上看内存泄漏的根源在于使用了弱引用,但是另一个问题也同样值得思考:为什么使用弱引用而不是强引用?
下面我们分两种情况讨论
- key 使用强引用:对ThreadLocal对象实例的引用被置为null了,但是ThreadLocalMap还持有这个ThreadLocal对象实例的强引用,如果没有手动删除,ThreadLocal的对象实例不会被回收,导致Entry内存泄漏。
- key 使用弱引用:对ThreadLocal对象实例的引用被被置为null了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal的对象实例也会被回收。value在下一次ThreadLocalMap调用set,get,remove都有机会被回收。
比较两种情况,我们可以发现:
由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障。
因此,ThreadLocal内存泄漏的根源是:
由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。