关于在线程池场景中使用ThreadLocal导致OOM的一点分析

1. OOM场景分析

测试oom使用的类

package com.concurrency.thread;

import com.concurrency.MyFixSizeThreadPool;
import org.junit.Test;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 使用jvm参数指定最大堆内存: 10M
 * -Xmx10m
 */
public class TestThreadLocalOOM {
    ThreadLocal<List<byte[]>> threadLocal = ThreadLocal.withInitial(() -> new ArrayList<>());
    AtomicInteger maxThreadCount = new AtomicInteger();
    volatile boolean isStop = false;

    Runnable runnable = () -> {
//        每次增加2M
        try{
            threadLocal.get().add(new byte[1 * 1024 * 1024]);
        }catch (OutOfMemoryError err) {
            err.printStackTrace();
            isStop = true;
        }

        int index = maxThreadCount.addAndGet(1);
        System.out.println("thread index: " + index + "..." + Thread.currentThread() + "...size=" + threadLocal.get().size());
    };

    Runnable runWithRemove = () -> {
//        每次增加2M
        threadLocal.get().add(new byte[1 * 1024 * 1024]);
        int index = maxThreadCount.addAndGet(1);
        System.out.println("thread index: " + index + "..." + Thread.currentThread() + "...size=" + threadLocal.get().size());
        threadLocal.remove();
    };

    /**
     * 测试threadlocal不使用线程池的多线程的场景
     */
    @Test
    public void testWithoutPool() throws IOException, InterruptedException {
        while (true) {
//            长时间运行不会oom
            new Thread(runnable).start();
            Thread.sleep(100);
        }

//        System.in.read();
    }

    @Test
    public void testWithFixThreadPool() throws InterruptedException {
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        while (!isStop) {
//            7次之后oom
            executorService.execute(runnable);
            Thread.sleep(100);
        }
    }


    @Test
    public void testWithFixThreadPoolAndRemove() throws InterruptedException {

        ExecutorService executorService = Executors.newFixedThreadPool(1);
        while (true) {
//            长时间运行不会oom
            executorService.execute(runWithRemove);
            Thread.sleep(100);
        }
    }


    @Test
    public void testWithMyFixedThreadPoolWithoutRemove() throws InterruptedException {
        MyFixSizeThreadPool myFixSizeThreadPool = new MyFixSizeThreadPool(1);

        while (true) {
//            长时间运行不会oom
            myFixSizeThreadPool.execute(runnable);
            Thread.sleep(100);
        }
    }

    public static void main(String[] args) throws IOException, InterruptedException {
        Thread.sleep(3*1000);
        TestThreadLocalOOM testThreadLocalOOM = new TestThreadLocalOOM();
        testThreadLocalOOM.testWithMyFixedThreadPoolWithoutRemove();
    }
}

一共分析三个场景

1. 不使用线程池的场景下, 使用new Thread方式创建线程

运行testWithoutPool方法, 运行期heap占用图如下, 在运行过程中一直未触发oom
testWithoutPool

2. 使用线程池的方式, 但不手动remove ThreadLocal, 测试oom

运行testWithFixThreadPool方法, 运行期heap占用图如下, 在循环第8次的时候就触发oom

thread index: 1...Thread[pool-1-thread-1,5,main]...size=1
thread index: 2...Thread[pool-1-thread-1,5,main]...size=2
thread index: 3...Thread[pool-1-thread-1,5,main]...size=3
thread index: 4...Thread[pool-1-thread-1,5,main]...size=4
thread index: 5...Thread[pool-1-thread-1,5,main]...size=5
thread index: 6...Thread[pool-1-thread-1,5,main]...size=6
thread index: 7...Thread[pool-1-thread-1,5,main]...size=7
thread index: 8...Thread[pool-1-thread-1,5,main]...size=7
java.lang.OutOfMemoryError: Java heap space
	at com.concurrency.thread.TestThreadLocalOOM.lambda$new$1(TestThreadLocalOOM.java:24)
	at com.concurrency.thread.TestThreadLocalOOM$$Lambda$2/2129789493.run(Unknown Source)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:748)

testWithFixThreadPool

3. 使用线程池的方式, 但手动remove ThreadLocal, 测试oom

运行testWithFixThreadPoolAndRemove方法, 手动调用remove, 运行期heap占用图如下, 在运行过程中一直未触发oom
testWithFixThreadPoolAndRemove

2. oom原因分析

1. 为什么使用new Thread不会导致OOM?

要分析为什么oom, 就要知道threadLocal对象是存储在什么地方?查看Thread源码, 可以看出每一个Thread维护了一个threadLocals对象:

/* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

每一个threaLocal对象都存储在这个map中, 已Entry(ThreadLocal<?> k, Object v)方式存储, 线程退出时, jvm会调用Thread的exit方法,

/**
 * This method is called by the system to give a Thread
 * a chance to clean up before it actually exits.
 */
private void exit() {
    if (group != null) {
        group.threadTerminated(this);
        group = null;
    }
    /* Aggressively null out all reference fields: see bug 4006245 */
    target = null;
    /* Speed the release of some of these resources */
    threadLocals = null; // 释放threadLocals这个map中存放的所有对象
    inheritableThreadLocals = null; 
    inheritedAccessControlContext = null;
    blocker = null;
    uncaughtExceptionHandler = null;
}

在这个方法中, 通过threadLocals = null; 将map这个对象释放, gc可以回收. 所以testWithoutPool不会oom

2. 为什么testWithFixThreadPool会oom

为什么使用线程池就有可能oom呢?这个和线程池的实现方式有关, 简单看一下ThreadPoolExecutor的实现方式(不考虑线程的退出情况), 线程池中的所有线程一直在循环监听一个BlockingQueue<Runnable>, 直到有队列中有Task, 取出并运行.

// java.util.concurrent.ThreadPoolExecutor这个类的方法
final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        Runnable task = w.firstTask;
        w.firstTask = null;
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
        // 一直死循环, 监听队列, 获取任务
            while (task != null || (task = getTask()) != null) {
                w.lock();
                // If pool is stopping, ensure thread is interrupted;
                // if not, ensure thread is not interrupted.  This
                // requires a recheck in second case to deal with
                // shutdownNow race while clearing interrupt
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                        task.run();
                    } catch (RuntimeException x) {
                        thrown = x; throw x;
                    } catch (Error x) {
                        thrown = x; throw x;
                    } catch (Throwable x) {
                        thrown = x; throw new Error(x);
                    } finally {
                        afterExecute(task, thrown);
                    }
                } finally {
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }

可见线程池中的线程一直并没有退出, 而是一直在自旋运行. 所以不会运行到Thread.exit这个方法. 在没有手动调用ThreadLocal.remove方法的时候, Thread中的threadLocals 这个Map会一直堆积, 而内存得不到释放, 导致了oom

3. 为什么手动调用了threadLocal.remove方法就不会导致oom

第2部分分析了为什么线程池中threadlocal会oom, 要解决这个问题, 在threadLoca对象不再需要使用的时候, 手动调用remove方法, 清除Thread中threadLocals 中当前的Entry, 以便gc回收.

// java.lang.ThreadLocal#remove
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

2. 一点思考和改进

既然在使用线程池的时候, 如果没有手动调用ThreadLocal的remove方法, 有潜在的oom风险. 那如果用户忘记了手动调用, 对程序员不友好.
那有没有方法可以改进一下这个线程池, 让线程池中的线程运行完一个任务后, 主动释放Thread.threadLocals?
答案肯定是可以的, 可以看到, testWithMyFixedThreadPoolWithoutRemove运行后, 一直不会oom, MyFixSizeThreadPool实现代码如下:

package com.concurrency;

import java.lang.reflect.Field;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicInteger;

public class MyFixSizeThreadPool {
    private final int core;
    private AtomicInteger currentThreadCount = new AtomicInteger(0);
    private LinkedBlockingQueue<Runnable> taskQueue = new LinkedBlockingQueue<>();

    public MyFixSizeThreadPool(int core) {
        this.core = core;
    }


    public void execute(Runnable command) {
        if(currentThreadCount.get() < core) {
            currentThreadCount.addAndGet(1);
            Thread thread = new Thread(new Worker());
            thread.start();
        }
        taskQueue.offer(command);
    }


    class Worker implements Runnable {
        Thread thread;

        @Override
        public void run() {
//            一只查询是否有值
            Runnable task = null;
            while ((task = getTask()) != null) {
                task.run();

//              运行完成后, 清空Thread.threadLocals, 确保下一个任务调用这个线程时, 不会使用者上一个任务留下的threadLocals
                Thread thread = Thread.currentThread();
                try {
                    Field threadLocalsFiled = Thread.class.getDeclaredField("threadLocals");
                    threadLocalsFiled.setAccessible(true);
                    threadLocalsFiled.set(thread, null);
                } catch (NoSuchFieldException e) {
                    e.printStackTrace();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private Runnable getTask() {
        Runnable take = null;
        while (true) {
            try {
                take = taskQueue.take();
            } catch (InterruptedException e) {
                e.printStackTrace();
                continue;
            }
            return take;
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值