1.jpg

ThreadLocal使用不规范,师傅两行泪


前置条件:
场景代码

  1. public class ThreadPoolDemo {
  2. // 线程工厂,用于为线程池中的每条线程命名 setUncaughtExceptionHandler:发生异常时打印异常信息
  3. private static ThreadFactory namedThreadFactory = new ThreadFactoryBuilder()
  4. .setNameFormat("judge-pool-%d")
  5. .setUncaughtExceptionHandler((thread, throwable) -> logger.error("ThreadPool {} got exception", thread, throwable))
  6. .build();
  7. // 创建线程池,使用有界阻塞队列防止内存溢出
  8. private static final ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
  9. 10,10,1,
  10. TimeUnit.MINUTES,
  11. new LinkedBlockingQueue<>(),namedThreadFactory);
  12. public static void main(String[] args) throws InterruptedException {
  13. for (int i = 0; i < 100; ++i) {
  14. poolExecutor.execute(() -> {
  15. @Override
  16. public void run() {
  17. ThreadLocal<BigObject> threadLocal = new ThreadLocal<>();
  18. threadLocal.set(new BigObject());
  19. // 其他业务代码
  20. }
  21. });
  22. Thread.sleep(1000);
  23. }
  24. }
  25. static class BigObject {
  26. // 100M
  27. private byte[] bytes = new byte[100 * 1024 * 1024];
  28. }
  29. }

抛出异常:
java.lang.OutOfMemoryError: Java heap space
代码分析:

  • 创建线程工厂,用于为线程池中的每条线程命名 setUncaughtExceptionHandler:发生异常时打印异常信息
  • 创建一个核心线程数和最大线程数都为10的线程池,保证线程池里一直会有10个线程在运行。
  • 使用for循环向线程池中提交了100个任务。
  • 定义了一个ThreadLocal类型的变量,Value类型是大对象。
  • 每个任务会向threadLocal变量里塞一个大对象,然后执行其他业务逻辑。
  • 由于没有调用线程池的shutdown方法,线程池里的线程还是会在运行。

乍一看这代码好像没有什么问题,那为什么会导致服务GC后内存还高居不下呢?
代码中给threadLocal赋值了一个大的对象,但是执行完业务逻辑后没有调用remove方法,最后导致线程池中10个线程的threadLocals变量中包含的大对象没有被释放掉,出现了内存泄露。

ThreadLocal的value值存在哪里?


本以为线程任务结束了threadLocal赋值的对象会被JVM垃圾回收,很疑惑为什么会出现内存泄露?

ThreadLocal类提供set/get方法存储和获取value值,但实际上ThreadLocal类并不存储value值,真正存储是靠ThreadLocalMap这个类,ThreadLocalMap是ThreadLocal的一个静态内部类,它的key是ThreadLocal实例对象,value是任意Object对象。

  1. static class ThreadLocalMap {
  2. // 定义一个table数组,存储多个threadLocal对象及其value值
  3. private Entry[] table;
  4. ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
  5. table = new Entry[INITIAL_CAPACITY];
  6. int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
  7. table[i] = new Entry(firstKey, firstValue);
  8. size = 1;
  9. setThreshold(INITIAL_CAPACITY);
  10. }
  11. // 定义一个Entry类,key是一个弱引用的ThreadLocal对象
  12. // value是任意对象
  13. static class Entry extends WeakReference<ThreadLocal<?>> {
  14. /** The value associated with this ThreadLocal. */
  15. Object value;
  16. Entry(ThreadLocal<?> k, Object v) {
  17. super(k);
  18. value = v;
  19. }
  20. }
  21. // 省略其他
  22. }

到这里,有点蒙,很正常,进一步分析ThreadLocal类的代码,看set和get方法如何与ThreadLocalMap静态内部类关联上。

ThreadLocal类set方法

set的逻辑比较简单 获取当前线程的ThreadLocalMap,然后往map里添加KV,K是当前ThreadLocal实例,V是我们传入的value。这里需要注意一下,map的获取是需要从Thread类对象里面取,代码如下:

  1. public class ThreadLocal<T> {
  2. public void set(T value) {
  3. Thread t = Thread.currentThread();
  4. ThreadLocalMap map = getMap(t);
  5. if (map != null)
  6. map.set(this, value);
  7. else
  8. createMap(t, value);
  9. }
  10. ThreadLocalMap getMap(Thread t) {
  11. return t.threadLocals;
  12. }
  13. void createMap(Thread t, T firstValue) {
  14. t.threadLocals = new ThreadLocalMap(this, firstValue);
  15. }
  16. // 省略其他方法
  17. }

再来看一下Thread类的定义,Thread类维护了一个ThreadLocalMap的变量引用。

  1. public class Thread implements Runnable {
  2. ThreadLocal.ThreadLocalMap threadLocals = null;
  3. //省略其他
  4. }

ThreadLocal类get方法

get获取当前线程的对应的私有变量,是之前set或者通过initialValue的值,代码如下:

  1. class ThreadLocal<T> {
  2. public T get() {
  3. Thread t = Thread.currentThread();
  4. ThreadLocalMap map = getMap(t);
  5. if (map != null) {
  6. ThreadLocalMap.Entry e = map.getEntry(this);
  7. if (e != null)
  8. return (T)e.value;
  9. }
  10. return setInitialValue();
  11. }
  12. }

代码逻辑分析:

  • 获取当前线程的ThreadLocalMap实例;
  • 如果不为空,以当前ThreadLocal实例为key获取value;
  • 如果ThreadLocalMap为空或者根据当前ThreadLocal实例获取的value为空,则执行setInitialValue();

    ThreadLocal相关类的关系总结