当前位置:   article > 正文

13]ThreadLocal是什么?有哪些用途?你了解多少?_refelectthreadlocals

refelectthreadlocals

本次给大家介绍重要的工具ThreadLocal。讲解内容如下,同时介绍什么场景下发生内存泄漏,如何复现内存泄漏,如何正确使用它来避免内存泄漏。

带着问题,我们一起来看看,如果你有什么见解,可以评论分享哈。

  • ThreadLocal是什么?有哪些用途?

  • ThreadLocal如何使用。

  • ThreadLocal原理。

  • ThreadLocal使用有哪些坑及注意事项。

 1. ThreadLocal是什么? 有哪些用途?

首先介绍Thread类中属性threadLocals: 我是这样理解的,他放在Thread,那是不是和线程有关联,什么关联,猜想共享,私有?

ThreadLocal.ThreadLocalMap threadLocals = null;

 看到源码,了解到是每个线程的实例 threadLocals

但是我们看没有操作threadLocals的方法,这个是个threadLocal就上场了,可以理解threadLocal是线程Thread中属性threadLocals的管理者。

也就是,我们操作threadLocal的get,set,remove的操作结果都是针对当前线程Thread实例的threadLocals存,取,删除操作。类次于一个开发者的任务,产品经理左右不了,产品经理只能通过技术leader来给开发者分配任务。

其实,我们看看threadLocal的源码,底层就是threadLocalMap组成,这不匹配上了 。

要了解到这个threadLocal的分配空间。

那ThreadLocal有哪些应用场景呢?

其实我们无意间已经时时刻刻在使用ThreadLocal提供的便利,如果说多数据源的切换你比较陌生,那么spring提供的声明式事务就再熟悉不过了,我们在研发过程中无时无刻不在使用,而spring声明式事务的重要实现基础就是ThreadLocal,只不过大家没有去深入研究spring声明式事务的实现机制。

原来ThreadLocal这么强大,但应用开发者使用较少,同时有些研发人员对于ThreadLocal内存泄漏,等潜在问题,不敢试用,恐怕这是对于ThreadLocal最大的误解,后面我们将会仔细分析,只要按照正确使用方式,就没什么问题。如果ThreadLocal存在问题,岂不是spring声明式事务是我们程序最大的潜在危险吗?

兄弟们,编程,先学习思想,但是发现,我了解一个ThreadLocal,里面涉及很多的知识点,这里就冒出了spring声明式事务的原理及实现机制,记在心思,可以自己学习,后面我也分我学习的。

2.ThreadLocal如何使用

为了更直观的体会ThreadLocal的使用我们假设如下场景

  1. 我们给每个线程生成一个ID。

  2. 一旦设置,线程生命周期内不可变化。

  3. 容器活动期间不可以生成重复的ID

我们创建一个ThreadLocal管理类:

  1. package demo.aqs;
  2. import java.util.concurrent.atomic.AtomicBoolean;
  3. import java.util.concurrent.atomic.AtomicInteger;
  4. public class ThreadLocalId {
  5. private static final AtomicInteger nextId = new AtomicInteger(0);
  6. private static final ThreadLocal<Integer> threadId = new ThreadLocal<Integer>() {
  7. protected Integer initialValue()
  8. {
  9. return nextId.getAndIncrement();
  10. }
  11. };
  12. public static int get() {
  13. return threadId.get();
  14. }
  15. public static void remove() {
  16. threadId.remove();
  17. }
  18. private static void increamentSameThreadId() {
  19. try{
  20. for(int i = 0;i < 5; i++) {
  21. System.out.println(Thread.currentThread()+"_"+i+",threadId:"+ThreadLocalId.get());
  22. }
  23. } finally {
  24. ThreadLocalId.remove();
  25. }
  26. }
  27. public static void main(String[] args) {
  28. increamentSameThreadId();
  29. //在搞2个线程
  30. new Thread(new Runnable() {
  31. @Override
  32. public void run() {
  33. increamentSameThreadId();
  34. }
  35. }).start();
  36. new Thread(()->{
  37. increamentSameThreadId();
  38. }).start();
  39. }
  40. }

 3.ThreadLocal原理

①ThreadLocal类结构及方法解析:

图片

 上图可知:ThreadLocal三个方法get, set , remove以及内部类ThreadLocalMap

图片

从这张图我们可以直观的看到Thread中属性threadLocals,作为一个特殊的Map,它的key值就是我们ThreadLocal实例,而value值这是我们设置的值。

③ThreadLocal的操作过程:

 其中getMap(t) 返回的就是当前线程的threadlocals,如下图,然后根据当前ThreadLocal实例对象作为key获取ThreadLocalMap中的value,如果首次进来调用setInitialValue();

set的过程也类似:

4.ThreadLocal使用有哪些坑及注意事项

我经常在网上看到骇人听闻的标题,ThreadLocal导致内存泄漏,这通常让一些刚开始对ThreadLocal理解不透彻的开发者,不敢贸然使用。越不用,越陌生。这样就让我们错失了更好的实现方案,所以敢于引入新技术,敢于踩坑,才能不断进步。

我们来看下为什么说ThreadLocal会引起内存泄漏,什么场景下会导致内存泄漏?

先回顾下什么叫内存泄漏,对应的什么叫内存溢出

  • Memory overflow:内存溢出,没有足够的内存提供申请者使用。

  • Memory leak:内存泄漏,程序申请内存后,无法释放已申请的内存空间,内存泄漏的堆积终将导致内存溢出。

显然是TreadLocal在不规范使用的情况下导致了内存没有释放。

 

 看到ThreadLocalMap的Entry继承了WeakReference(弱引用)

知识点来了:

图片

 既然WeakReference在下一次gc即将被回收,那么我们的程序为什么没有出问题呢?

①所以我们测试下弱引用的回收机制:

  1. package demo;
  2. import java.lang.ref.WeakReference;
  3. /**
  4. * 弱引用测试
  5. */
  6. public class ThreadLocalLeakTest {
  7. public static void main(String[] args) {
  8. String str = new String("weakReference1");
  9. //弱应用回收
  10. WeakReference<String> weakReference = new WeakReference<String>(str);
  11. System.gc();
  12. if(weakReference.get() == null) {
  13. System.out.println("weak1已被回收");
  14. } else {
  15. System.out.println(weakReference.get());
  16. }
  17. //存在强引用
  18. WeakReference<String> weakReference2 = new WeakReference<String>(new String("weakReference1"));
  19. System.gc();
  20. if(weakReference2.get() == null) {
  21. System.out.println("weak2已被回收");
  22. } else {
  23. System.out.println(weakReference2.get());
  24. }
  25. }
  26. }

weakReference1
weak2已被回收

上面演示了弱引用的回收情况,下面我们看下ThreadLocal的弱引用回收情况。

②ThreadLocal的弱引用回收情况

图片

 如上图所示,我们在作为key的ThreadLocal对象没有外部强引用,下一次gc必将产生key值为null的数据,若线程没有及时结束必然出现,一条强引用链Threadref–>Thread–>ThreadLocalMap–>Entry,所以这将导致内存泄漏

下面我们模拟复现ThreadLocal导致内存泄漏:

  1. package com.example.demo.threadLoclLeak;
  2. import java.lang.reflect.Field;
  3. import java.util.ArrayList;
  4. import java.util.List;
  5. import org.springframework.util.ReflectionUtils;
  6. public class TestThreadLoclLeak {
  7. public static void main(String[] args) throws InterruptedException {
  8. // 为了复现key被回收的场景,我们使用临时变量
  9. ThreadLocalMemory memeory = new ThreadLocalMemory();
  10. // 调用
  11. incrementSameThreadId(memeory);
  12. System.out.println("GC前:key:" + memeory.threadId);
  13. System.out.println("GC前:value-size:" + refelectThreadLocals(Thread.currentThread()));
  14. // 设置为null,调用gc并不一定触发垃圾回收,但是可以通过java提供的一些工具进行手工触发gc回收。
  15. memeory.threadId = null;
  16. System.gc();
  17. System.out.println("GC后:key:" + memeory.threadId);
  18. System.out.println("GC后:value-size:" + refelectThreadLocals(Thread.currentThread()));
  19. // 进一步回收,保证ThreadLocal实例被回收
  20. memeory = null;
  21. System.gc();
  22. // 模拟线程一直运行
  23. while (true) {
  24. }
  25. }
  26. /**
  27. * 使用threadlocal
  28. *
  29. * @param memeory
  30. */
  31. private static void incrementSameThreadId(final ThreadLocalMemory memeory) {
  32. try {
  33. for (int i = 0; i < 5; i++) {
  34. System.out.println(Thread.currentThread() + "_" + i + ",threadId:" + memeory.get().size());
  35. }
  36. } finally {
  37. // 使用后请清除,这里为了复现内存泄漏,故意不回收
  38. // ThreadLocalMemory.remove();
  39. }
  40. }
  41. /**
  42. * 利用反射获取ThreadLocal对应的值
  43. *
  44. * @param t
  45. * @return
  46. */
  47. public static Object refelectThreadLocals(Thread t) {
  48. try {
  49. // Thread
  50. Field field = ReflectionUtils.findField(Thread.class, "threadLocals");
  51. field.setAccessible(true);
  52. Object localmap = ReflectionUtils.getField(field, t);
  53. // ThreadLocalMap.Entry[]
  54. Field entryField = ReflectionUtils.findField(localmap.getClass(), "table");
  55. entryField.setAccessible(true);
  56. Object[] entry = (Object[]) ReflectionUtils.getField(entryField, localmap);
  57. List<Object> list = new ArrayList<>();
  58. for (Object o : entry) {
  59. if (o != null)
  60. list.add(o);
  61. }
  62. List<Object> result = new ArrayList<>();
  63. for (Object o : list) {
  64. // Entry.value
  65. Field entryValue = ReflectionUtils.findField(o.getClass(), "value");
  66. entryValue.setAccessible(true);
  67. Object keyvalue = ReflectionUtils.getField(entryValue, o);
  68. if (keyvalue instanceof ArrayList) {
  69. result.add(keyvalue);
  70. }
  71. }
  72. return ((ArrayList<?>) result.get(0)).size();
  73. } catch (Exception e) {
  74. e.printStackTrace();
  75. return null;
  76. }
  77. }
  78. }
  79. class ThreadLocalMemory {
  80. // Thread local variable containing each thread's ID
  81. public ThreadLocal<List<Object>> threadId = new ThreadLocal<List<Object>>() {
  82. @Override
  83. protected List<Object> initialValue() {
  84. List<Object> list = new ArrayList<Object>();
  85. for (int i = 0; i < 10000; i++) {
  86. list.add(String.valueOf(i));
  87. }
  88. return list;
  89. }
  90. };
  91. // Returns the current thread's unique ID, assigning it if necessary
  92. public List<Object> get() {
  93. return threadId.get();
  94. }
  95. // remove currentid
  96. public void remove() {
  97. threadId.remove();
  98. }
  99. }

此时我们如何知道内存中存在memory leak呢?

我们可以借助jdk提供的一些命令dump当前堆内存,命令如下:

jmap -dump:live,format=b,file=heap.bin <pid>

然后我们借助MAT可视化分析工具,来查看对内存,分析对象实例的存活状态:

图片

图片

首先打开我们工具提示我们的内存泄漏分析:

图片

这里我们可以确定的是ThreadLocalMap实例的Entry.value是没有被回收的。

最后我们要确定Entry.key是否还在?打开Dominator Tree,搜索我们的ThreadLocalMemory,发现并没有存活的实例。

图片

图片

以上我们复现了ThreadLocal不正当使用,引起的内存泄漏。

所以我们总结了使用ThreadLocal时会发生内存泄漏的前提条件

  • ThreadLocal引用被设置为null,且后面没有set,get,remove操作。

  • 线程一直运行,不停止。(线程池)

  • 触发了垃圾回收。(Minor GC或Full GC)

我们看到ThreadLocal出现内存泄漏条件还是很苛刻的,所以我们只要破坏其中一个条件就可以避免内存泄漏,单但为了更好的避免这种情况的发生我们使用ThreadLocal时遵守以下两个小原则:

①ThreadLocal申明为private static final。

  • Private与final 尽可能不让他人修改变更引用,

  • Static 表示为类属性,只有在程序结束才会被回收。

②ThreadLocal使用后务必调用remove方法。

  • 最简单有效的方法是使用后将其移除。

以上。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/凡人多烦事01/article/detail/480137
推荐阅读
相关标签
  

闽ICP备14008679号