1. 前言:

关于ThreadLocal, 有如下几个问题:

  1. 什么是ThreadLocal,能说说你的理解吗?
  2. ThreadLocal类有哪些方法,它们都是做什么的?
  3. ThreadLocal变量和synchronized关键字的区别是什么?
  4. 你能想到哪些使用ThreadLocal的典型场景?
  5. ThreadLocal为什么会导致内存泄露?

    2. ThreadLocal解释

    2.1 什么是ThreadLocal

    官方解释: ThreadLocal提供了线程的本地变量, 线程本地变量与与普通的变量不同, 每一个访问线程本地变量的线程都拥有一个自己的独立初始化的副本。白话一点就是, ThreadLocal会在每一个线程里设置属于每一个线程私有的变量。变量对于别的线程是数据隔离的, 多线程场景下防止自己变量被其他线程串改。
    ThreadLocal的核心机制:
    a. 对ThreadLocal对象进行set操作, 其实是操作一个ThreadLocalMap对象, 该对象存在于每个Thread线程内部。
    b. ThreadLocalMap其实是一个存放Entry对象的数组, (key,放的是ThreadLocal这个对象) (value是传入的值)。为什么是数组: 同一线程, 有不同的ThreadLocal对象进行存储数据。
    image.png

c. Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。
每个线程Thread都维护了自己的threadLocals变量,所以在每个线程创建ThreadLocal的时候,实际上数据是存在自己线程Thread的threadLocals变量里面的,别人没办法拿到,从而实现了隔离。
image.png

2.2 ThreadLocal的一些方法

2.2.1 get()方法

返回当前线程本地变量在当前线程里面的值, 如果当前线程没有该变量, 就先调用initiaValue()方法执行初始化, 然后返回这个值。(默认的话, 执行initiaValue方法返回的是null)

  1. public T get() {
  2. Thread t = Thread.currentThread();
  3. ThreadLocalMap map = getMap(t);
  4. if (map != null) {
  5. ThreadLocalMap.Entry e = map.getEntry(this);
  6. if (e != null)
  7. return (T)e.value;
  8. }
  9. return setInitialValue();
  10. }
  11. ThreadLocalMap getMap(Thread t) {
  12. return t.threadLocals;
  13. }
  14. private T setInitialValue() {
  15. T value = initialValue();
  16. Thread t = Thread.currentThread();
  17. ThreadLocalMap map = getMap(t);
  18. if (map != null)
  19. map.set(this, value);
  20. else
  21. createMap(t, value);
  22. return value;
  23. }
  24. protected T initialValue() {
  25. return null;
  26. }

2.2.1 remove()

删除某一个线程本地变量在当前线程的值, 删除之后, 再一次get的话, 会重新执行initiaValue方法。

  1. public void remove() {
  2. ThreadLocalMap m = getMap(Thread.currentThread());
  3. if (m != null)
  4. m.remove(this);
  5. }
  6. ThreadLocalMap getMap(Thread t) {
  7. return t.threadLocals;
  8. }

2.2.3 set()

设置线程本地变量在当前线程里面的值

  1. public void set(T value) {
  2. Thread t = Thread.currentThread();
  3. ThreadLocalMap map = getMap(t);
  4. if (map != null)
  5. map.set(this, value);
  6. else
  7. createMap(t, value);
  8. }
  9. ThreadLocalMap getMap(Thread t) {
  10. return t.threadLocals;
  11. }
  12. void createMap(Thread t, T firstValue) {
  13. t.threadLocals = new ThreadLocalMap(this, firstValue);
  14. }

2.3 ThreadLocal变量和synchronized关键字区别

ThreadLocal使得每个线程都有自己的局部变量资源,因此多个线程之间互不干扰,从而实现线程安全。synchronized关键字保证每个时刻只能有一个线程访问共享变量,从而保证了线程安全。

2.4 共享线程的ThreadLocal数据

使用InheritableThreadLocal可以实现多个线程访问ThreadLocal的值,我们在主线程中创建一个InheritableThreadLocal的实例,然后在子线程中得到这个InheritableThreadLocal实例设置的值。
image.png
传递逻辑: 在新建线程的时候, 如果父线程的inheritThreadLocals存在,那么就把父线程的inheritThreadLocals给当前线程的inheritThreadLocals.
image.png

2.5 ThreadLocalMap介绍

是ThreadLocal的一个内部类, 然后用Entry来保存K-V数据结构. 但是key只能是ThreadLocal对象。

  1. static class Entry extends WeakReference<ThreadLocal> {
  2. /** The value associated with this ThreadLocal. */
  3. Object value;
  4. Entry(ThreadLocal k, Object v) {
  5. super(k);
  6. value = v;
  7. }
  8. }
  9. Entry 继承自 WeakReference, 是弱引用(弱引用就是生命周期只能活到下一次gc前)。要注意的地方时
  10. key值是弱引用, value并不是弱引用。

PS: ThreadLocalMap其实并不是一个map, 而是一个数组. 在每次set的时候, 都会根据当前ThreadLocal对象的HashCode进行hash, 确定了要存储的位置, 如果该位置不为空并且该位置所存储的ThreadLocal和当前对象不一致, 那么继续往后寻找数组的位置(做法其实就是依次往后寻找)。直至找到能够存放的位置。
image.png
这样的话,在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该位置Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置。

2.5.1 ThreadLocalMap的问题

ThreadLocalMap中key是弱引用, value是强引用, 这样就导致一些问题, 在发生GC时候, key值会被回收, 但是value不会被回收。如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露所以在调用ThreadLocal的get()、set()方法时完成后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。

  1. ThreadLocal<Session> threadLocal = new ThreadLocal<Session>();
  2. try {
  3. threadLocal.set(new Session(1, "xxx"));
  4. // 其它业务逻辑
  5. } finally {
  6. threadLocal.remove();
  7. }
  8. -- remove方法如下:
  9. private void remove(ThreadLocal<?> key) {
  10. Entry[] tab = table;
  11. int len = tab.length;
  12. int i = key.threadLocalHashCode & (len-1);
  13. for (Entry e = tab[i];
  14. e != null;
  15. e = tab[i = nextIndex(i, len)]) {
  16. if (e.get() == key) {
  17. // 找到对应的键值对, expungeStaleEntry 这个方法就是删除的操作。
  18. e.clear();
  19. expungeStaleEntry(i);
  20. return;
  21. }
  22. }
  23. }

2.5.2 程序为防止内存泄露做了什么

  1. public T get() {
  2. ...
  3. if (map != null) {
  4. // 在get方法中 从ThreadLocalMap中获得键值对 , key是当前的ThreadLocal对象
  5. ThreadLocalMap.Entry e = map.getEntry(this);
  6. ...
  7. }
  8. ...
  9. }
  10. private Entry getEntry(ThreadLocal<?> key) {
  11. int i = key.threadLocalHashCode & (table.length - 1);
  12. Entry e = table[i];
  13. if (e != null && e.get() == key)
  14. return e;
  15. else
  16. //在getEntry方法中,如果Entry中的key发现是null,
  17. //会继续调用getEntryAfterMiss(key, i, e)方法
  18. return getEntryAfterMiss(key, i, e);
  19. }
  20. private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
  21. Entry[] tab = table;
  22. int len = tab.length;
  23. while (e != null) {
  24. ThreadLocal<?> k = e.get();
  25. if (k == key)
  26. return e;
  27. if (k == null)
  28. // 这里操作与手动执行remove方法时如出一辙.
  29. expungeStaleEntry(i);
  30. else
  31. i = nextIndex(i, len);
  32. e = tab[i];
  33. }
  34. return null;
  35. }

PS: 即使程序做了处理, 但是也要养成良好的习惯, 在使用完毕后, 要执行ThreadLocal.remove()方法, 以避免内存泄露。

2.5.3 ThreadLocalMap的key为什么使用弱引用而不使用强引用

// 新建一个ThreadLocal对象, 会将ThreadLocal对象作为key值放到当前线程的ThreadLocalMap中
ThreadLocal local = new ThreadLocal();
Thread t = Thread.currentThread();
// 如图所示, 当前线程中ThreadLocalMap中key值确实指向的是local对象。

image.png

local = null;
System.gc();
// 这里断开ThreadLocal类和对象的强连接, 并执行gc
t = Thread.currentThread();
// 这里是弱引用, 在没有其他强引用的情况下, 执行一次GC,就会被回收掉。
// 如果这里是强引用, 即使这里local = null; 那么referent连接也会存在, 也会造成内存泄露。
// 当然了这里仅可以做到key值回收, 做不到value值回收, 也会内存泄露.

image.png

local.remove();
// 这里调用了remove(), 相当于把ThreadLocalMap里的这个元素干掉了. 这样避免了内存泄露。

image.png

2.6 ThreadLocal使用场景

3. 补充

3.1 java引用类型

java有四种引用类型, 强引用(StrongReference), 软引用(SoftReference),弱引用(WeakReference)
以及虚引用(PhantomReference). 其中最常用到的引用类型是强引用,StrongRerence这个类并不存在,而是在JVM底层实现。默认的对象都是强引用类型。对于软引用, 弱引用, 虚引用都继承自Reference。
image.png
强引用: new String(“123”);
软引用: new SoftReference(new String(“123”));
弱引用: new WeakReference(new String(“123”));
虚引用: new PhantomReference(new String(“123”));