重点

TheadLocal:用于线程间变量隔离,仅能在当前线程内访问,无法使用ThreadLocal进行父子线程变量传递。
ThreadLocalMap定义在ThreadLocal中的内部类,Thread中存在一个ThreadLocalMap属性
ThreadLocal内部维护的ThreadLocalMap是与当前线程绑定(ThreadLocalMap是当前线程的一个属性)
添加数据时,map的key为当前ThreadLocal实例;一个线程可中可进行多个ThreadLocal创建
当元素冲突时,采用开放寻址方式,而非链表方式。

一、应用场景

在使用多线程情况下,程序在处理用户请求的时候,通常后端服务器是有一个线程池,对每一个请求就分配一个线程来处理,那为了防止多线程并发处理请求的时,发生线程数据安全问题(串数据),利用ThreadLocal实现线程间变量隔离。

使用案例

  1. //存放用户信息的ThreadLocal,一般使用Util类进行封装
  2. private static final ThreadLocal<UserInfo> userInfoThreadLocal = new ThreadLocal<>();
  3. public Response handleRequest(UserInfo userInfo) {
  4. Response response = new Response();
  5. try {
  6. // 1.用户信息set到线程局部变量中
  7. userInfoThreadLocal.set(userInfo);
  8. doHandle();
  9. } finally {
  10. // 3.使用完移除掉
  11. userInfoThreadLocal.remove();
  12. }
  13. return response;
  14. }
  15. //业务逻辑处理
  16. private void doHandle () {
  17. // 2.实际用的时候取出来
  18. UserInfo userInfo = userInfoThreadLocal.get();
  19. //处理业务操作....
  20. }

二、实现细节

实现总结

ThreadLocal

单个线程可以进行多次执行new ThreadLocal()操作,每次调用ThreadLocal的set()方法或者setInitialValue()方法,都会在内部的ThreadLocalMap中添加一条数据(类似于执行了一次put操作)
key:当前ThreadLocal对象
value:需要存储的线程局部变量

ThreadLocalMap【Thread的一个成员变量】

image.png
当发生hash冲突时,采用开放地址法进行解决冲突

ThreadLocalMap底层使用Entry数组,
初始大小16。其中Entry继承WeakReference,仅将Entry的key设置为弱类型**。
image.png

  1. static class Entry extends WeakReference<ThreadLocal<?>> {
  2. /** The value associated with this ThreadLocal. */
  3. Object value;
  4. Entry(ThreadLocal<?> k, Object v) {
  5. //调用父类构造方法,将key设置为弱引用类型
  6. super(k);
  7. value = v;
  8. }
  9. }
  10. public WeakReference(T referent) {
  11. super(referent);
  12. }

key设置为弱引用原因

为了尽最避免内存泄漏,但是如果仅仅这样,还是无法避免内存泄漏。ThreadLocalMap是根据key是否为null来判断是否清理Entry???(没找到依据)。
系统一般使用线程池将线程进行复用,线程生命周期很长。根据GC root搜索方式,进行垃圾回收,Thread—>ThreadLocalMap-Entry会一直存在。
**
ThreadLocal的设计者认为只要ThreadLocal 所在的作用域结束了工作被清理了(但有时ThreadLocal并不会顺利清理),GC回收的时候就会把key引用对象回收,key置为null,ThreadLocal会尽力保证Entry清理掉来最大可能避免内存泄漏。

ThreadLocal 所在的作用域结束,并没有清理时,TheadLocal会一直被强引用关联
image.png
**
正常初始化时引用结构image.png

弱引用

只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描内存区域时,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

具体分析

  1. 实例化方式1
  2. /**
  3. * Creates a thread local variable.
  4. * @see #withInitial(java.util.function.Supplier)
  5. */
  6. public ThreadLocal() {
  7. }
  8. 实例化方式2
  9. public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
  10. return new SuppliedThreadLocal<>(supplier);
  11. }
  12. //在ThreadLocal中设置变量
  13. private T setInitialValue() {
  14. T value = initialValue();
  15. Thread t = Thread.currentThread();
  16. //获取当前线程关联的ThreadLocalMap实例
  17. ThreadLocalMap map = getMap(t);
  18. if (map != null)
  19. map.set(this, value);
  20. else
  21. //初始化ThreadLocalMap
  22. createMap(t, value);
  23. return value;
  24. }
  25. ThreadLocalMap getMap(Thread t) {
  26. return t.threadLocals;
  27. }
  28. void createMap(Thread t, T firstValue) {
  29. t.threadLocals = new ThreadLocalMap(this, firstValue);
  30. }

三、存在问题与避免

内存泄漏

Entry 继承了WeakReference类,Entry 中的 key 是WeakReference类型的(Value为强引用),在Java 中当对象只被 WeakReference 引用,没有其他对象引用时,被WeakReference 引用的对象发生GC 时会直接被回收掉。

解决方案

手动调用remove函数

  1. class Threadlocal {
  2. public void remove() {
  3. //获取当前线程的ThreadLocalMap
  4. ThreadLocalMap m = getMap(Thread.currentThread());
  5. if (m != null)
  6. m.remove(this); //this就是ThreadLocal对象,移除,方法在下面
  7. }
  8. }
  9. class ThreadlocalMap {
  10. private void remove(ThreadLocal<?> key) {
  11. Entry[] tab = table;
  12. int len = tab.length;
  13. //获取存储位置
  14. int i = key.threadLocalHashCode & (len-1);
  15. for (Entry e = tab[i];
  16. e != null;
  17. e = tab[i = nextIndex(i, len)]) {
  18. //清理
  19. if (e.get() == key) {
  20. //调用父类方法,置空引用
  21. e.clear();
  22. expungeStaleEntry(i); //清理空槽
  23. return;
  24. }
  25. }
  26. }
  27. }
  28. //清理数组元素
  29. private int expungeStaleEntry(int staleSlot) {
  30. Entry[] tab = table;
  31. int len = tab.length;
  32. //把staleSlot的value置为空,然后数组元素置为空
  33. tab[staleSlot].value = null;
  34. tab[staleSlot] = null;
  35. size--; //元素个数-1
  36. // Rehash until we encounter null
  37. Entry e;
  38. int i;
  39. for (i = nextIndex(staleSlot, len);
  40. (e = tab[i]) != null;
  41. i = nextIndex(i, len)) {
  42. ThreadLocal<?> k = e.get();
  43. //k 为null代表引用对象被GC回收掉了
  44. if (k == null) {
  45. e.value = null;
  46. tab[i] = null;
  47. size--;
  48. } else {
  49. //因为元素个数减少了,就把后面的元素重新hash
  50. int h = k.threadLocalHashCode & (len - 1);
  51. //hash地址不相等,就代表这个元素之前发生过hash冲突(本来应该放在这没放在这),
  52. //现在因为有元素被移除了,很有可能原来冲突的位置空出来了,重试一次
  53. if (h != i) {
  54. tab[i] = null;
  55. //继续采用链地址法存放元素
  56. while (tab[h] != null)
  57. h = nextIndex(h, len);
  58. tab[h] = e;
  59. }
  60. }
  61. }
  62. return i;
  63. }

参考:https://mp.weixin.qq.com/s/JUb2GR4CmokO0SklFeNmwg