java線程池ThreadPoolExecutor通過submit提交runnable的task時,爲何不會拋出oom

背景介紹

最近在聽朋友說threadlocal會存在內存泄露,是因爲它的entry是弱引用導致的,然後我也自己百度了一下,看了下它存存泄露的原因。

但通過網上的文章描述中,很難看到對threadlocal會導致內存泄露有相關的示例代碼,而且還有很多文章都是你抄我、我抄你、互相借鑑的非常多。於是,我便想着自己來寫一個示例代碼。

threadlocal會造成內存泄露最簡單的方法就是寫一個線程池,線程不會回收,那麼threadlocal對應map中的entry中的value也就不會回收,因爲thread對於threadlocal的引用鏈會一直存在。

runnable通過線程池submit提交不oom重現代碼

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadLocalOomTest {

    public static void main(String[] args) {
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        for (int i = 0; i < 500; i++) {
            int finalI = i;
            executorService.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println("now,pre run" + Thread.currentThread().toString() + ":" + finalI);
                    try {
                        Thread.sleep(10_000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    ThreadLocal<Byte[]> threadLocal = new ThreadLocal<>();
                    Byte[] bytes = new Byte[1024 * 1024 * 9];
                    threadLocal.set(bytes);
                    System.out.println("----------------->" + Thread.currentThread().toString() + ":" + finalI);
                    if (finalI == 499) {
                        executorService.shutdown();
                    }
                    System.out.println(finalI+" run");
                }
            });
        }
    }
}

爲了測試的方便,我特意將最大堆的大小設置爲了128M

-Xmx128m

其輸出結果感覺很奇怪,前幾條輸出如下:

now,pre runThread[pool-1-thread-1,5,main]:0
----------------->Thread[pool-1-thread-1,5,main]:0
0 run
now,pre runThread[pool-1-thread-1,5,main]:1
----------------->Thread[pool-1-thread-1,5,main]:1
1 run
now,pre runThread[pool-1-thread-1,5,main]:2
now,pre runThread[pool-1-thread-1,5,main]:3
now,pre runThread[pool-1-thread-1,5,main]:4
now,pre runThread[pool-1-thread-1,5,main]:5
now,pre runThread[pool-1-thread-1,5,main]:6
----------------->Thread[pool-1-thread-1,5,main]:6
6 run
now,pre runThread[pool-1-thread-1,5,main]:7
now,pre runThread[pool-1-thread-1,5,main]:8
now,pre runThread[pool-1-thread-1,5,main]:9
now,pre runThread[pool-1-thread-1,5,main]:10

觀察上面的輸出片段,會發現只有0、1、6運行成功了,但並沒有拋出oom異常信息。再觀察下堆情況

gc堆信息

但如果將上面提交到線程池的測試代碼換成execute,其輸出情況如下:

now,pre runThread[pool-1-thread-1,5,main]:0
----------------->Thread[pool-1-thread-1,5,main]:0
0 run
now,pre runThread[pool-1-thread-1,5,main]:1
----------------->Thread[pool-1-thread-1,5,main]:1
1 run
now,pre runThread[pool-1-thread-1,5,main]:2
Exception in thread "pool-1-thread-1" now,pre runThread[pool-1-thread-2,5,main]:3
java.lang.OutOfMemoryError: Java heap space
	at com.example.javastu.threadlocaltest.ThreadLocalOomTest$1.run(ThreadLocalOomTest.java:22)
	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)
----------------->Thread[pool-1-thread-2,5,main]:3
3 run
now,pre runThread[pool-1-thread-2,5,main]:4
----------------->Thread[pool-1-thread-2,5,main]:4
4 run
now,pre runThread[pool-1-thread-2,5,main]:5
----------------->Thread[pool-1-thread-2,5,main]:5
5 run
now,pre runThread[pool-1-thread-2,5,main]:6
----------------->Thread[pool-1-thread-2,5,main]:6
6 run
now,pre runThread[pool-1-thread-2,5,main]:7
----------------->Thread[pool-1-thread-2,5,main]:7
7 run
now,pre runThread[pool-1-thread-2,5,main]:8
----------------->Thread[pool-1-thread-2,5,main]:8
8 run
now,pre runThread[pool-1-thread-2,5,main]:9
----------------->Thread[pool-1-thread-2,5,main]:9
9 run
now,pre runThread[pool-1-thread-2,5,main]:10
Exception in thread "pool-1-thread-2" java.lang.OutOfMemoryError: Java heap space
	at com.example.javastu.threadlocaltest.ThreadLocalOomTest$1.run(ThreadLocalOomTest.java:22)
	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)
now,pre runThread[pool-1-thread-3,5,main]:11
----------------->Thread[pool-1-thread-3,5,main]:11
11 run
now,pre runThread[pool-1-thread-3,5,main]:12
----------------->Thread[pool-1-thread-3,5,main]:12
12 run

當用execute方法提交後,又會報出oom異常了。
先拋開輸出的具體細節,只看submit和execute方法就感覺有一些疑惑的地方:都是在線程池中運行,爲啥輸出結果就不一樣了呢?

爲了瞭解其原理,我決定還是看一下源碼,瞭解下execute爲啥可能拋出oom,而submit不會拋出oom的原因

ThreadPoolExecutor execute方法

    /**
     * Executes the given task sometime in the future.  The task
     * may execute in a new thread or in an existing pooled thread.
     *
     * If the task cannot be submitted for execution, either because this
     * executor has been shutdown or because its capacity has been reached,
     * the task is handled by the current {@code RejectedExecutionHandler}.
     *
     * @param command the task to execute
     * @throws RejectedExecutionException at discretion of
     *         {@code RejectedExecutionHandler}, if the task
     *         cannot be accepted for execution
     * @throws NullPointerException if {@code command} is null
     */
    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        /*
         * Proceed in 3 steps:
         *
         * 1. If fewer than corePoolSize threads are running, try to
         * start a new thread with the given command as its first
         * task.  The call to addWorker atomically checks runState and
         * workerCount, and so prevents false alarms that would add
         * threads when it shouldn't, by returning false.
         *
         * 2. If a task can be successfully queued, then we still need
         * to double-check whether we should have added a thread
         * (because existing ones died since last checking) or that
         * the pool shut down since entry into this method. So we
         * recheck state and if necessary roll back the enqueuing if
         * stopped, or start a new thread if there are none.
         *
         * 3. If we cannot queue task, then we try to add a new
         * thread.  If it fails, we know we are shut down or saturated
         * and so reject the task.
         */
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }

正常情況下,會再進入addWorker方法,其代碼如下:

    /*
     * Methods for creating, running and cleaning up after workers
     */

    /**
     * Checks if a new worker can be added with respect to current
     * pool state and the given bound (either core or maximum). If so,
     * the worker count is adjusted accordingly, and, if possible, a
     * new worker is created and started, running firstTask as its
     * first task. This method returns false if the pool is stopped or
     * eligible to shut down. It also returns false if the thread
     * factory fails to create a thread when asked.  If the thread
     * creation fails, either due to the thread factory returning
     * null, or due to an exception (typically OutOfMemoryError in
     * Thread.start()), we roll back cleanly.
     *
     * @param firstTask the task the new thread should run first (or
     * null if none). Workers are created with an initial first task
     * (in method execute()) to bypass queuing when there are fewer
     * than corePoolSize threads (in which case we always start one),
     * or when the queue is full (in which case we must bypass queue).
     * Initially idle threads are usually created via
     * prestartCoreThread or to replace other dying workers.
     *
     * @param core if true use corePoolSize as bound, else
     * maximumPoolSize. (A boolean indicator is used here rather than a
     * value to ensure reads of fresh values after checking other pool
     * state).
     * @return true if successful
     */
    private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // Check if queue empty only if necessary.
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;

            for (;;) {
                int wc = workerCountOf(c);
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                c = ctl.get();  // Re-read ctl
                if (runStateOf(c) != rs)
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }

        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                    // Recheck while holding lock.
                    // Back out on ThreadFactory failure or if
                    // shut down before lock acquired.
                    int rs = runStateOf(ctl.get());

                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        if (t.isAlive()) // precheck that t is startable
                            throw new IllegalThreadStateException();
                        workers.add(w);
                        int s = workers.size();
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                if (workerAdded) {
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }

觀察上面的代碼,會發現,先會創建一個Worker對象,將先會將傳入的task傳入到新建的Worker中,然後再從Worker中拿到這個Worker的thread變量,再調用了這個Worker的thread的start方法,那麼start()方法運行的是哪裏的代碼呢?

再來看一下Worker的源碼:

    /**
     * Class Worker mainly maintains interrupt control state for
     * threads running tasks, along with other minor bookkeeping.
     * This class opportunistically extends AbstractQueuedSynchronizer
     * to simplify acquiring and releasing a lock surrounding each
     * task execution.  This protects against interrupts that are
     * intended to wake up a worker thread waiting for a task from
     * instead interrupting a task being run.  We implement a simple
     * non-reentrant mutual exclusion lock rather than use
     * ReentrantLock because we do not want worker tasks to be able to
     * reacquire the lock when they invoke pool control methods like
     * setCorePoolSize.  Additionally, to suppress interrupts until
     * the thread actually starts running tasks, we initialize lock
     * state to a negative value, and clear it upon start (in
     * runWorker).
     */
    private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable
    {
        /**
         * This class will never be serialized, but we provide a
         * serialVersionUID to suppress a javac warning.
         */
        private static final long serialVersionUID = 6138294804551838833L;

        /** Thread this worker is running in.  Null if factory fails. */
        final Thread thread;
        /** Initial task to run.  Possibly null. */
        Runnable firstTask;
        /** Per-thread task counter */
        volatile long completedTasks;

        /**
         * Creates with given first task and thread from ThreadFactory.
         * @param firstTask the first task (null if none)
         */
        Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }

        /** Delegates main run loop to outer runWorker  */
        public void run() {
            runWorker(this);
        }

        // Lock methods
        //
        // The value 0 represents the unlocked state.
        // The value 1 represents the locked state.

        protected boolean isHeldExclusively() {
            return getState() != 0;
        }

        protected boolean tryAcquire(int unused) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        protected boolean tryRelease(int unused) {
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        public void lock()        { acquire(1); }
        public boolean tryLock()  { return tryAcquire(1); }
        public void unlock()      { release(1); }
        public boolean isLocked() { return isHeldExclusively(); }

        void interruptIfStarted() {
            Thread t;
            if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
                try {
                    t.interrupt();
                } catch (SecurityException ignore) {
                }
            }
        }
    }

觀察上面的這部分:

        /**
         * Creates with given first task and thread from ThreadFactory.
         * @param firstTask the first task (null if none)
         */
        Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }

在新建Worker的時候,會將Worker中的thread也一併創建好,並將Worker自己塞到了自己 的Thread中,因爲Worker實現了Runnable接口

所以上面t.start()方法實際上運行的也就是Worker中的run()方法,Worker的run方法調用的是runWorker,其代碼如下:

    /**
     * Main worker run loop.  Repeatedly gets tasks from queue and
     * executes them, while coping with a number of issues:
     *
     * 1. We may start out with an initial task, in which case we
     * don't need to get the first one. Otherwise, as long as pool is
     * running, we get tasks from getTask. If it returns null then the
     * worker exits due to changed pool state or configuration
     * parameters.  Other exits result from exception throws in
     * external code, in which case completedAbruptly holds, which
     * usually leads processWorkerExit to replace this thread.
     *
     * 2. Before running any task, the lock is acquired to prevent
     * other pool interrupts while the task is executing, and then we
     * ensure that unless pool is stopping, this thread does not have
     * its interrupt set.
     *
     * 3. Each task run is preceded by a call to beforeExecute, which
     * might throw an exception, in which case we cause thread to die
     * (breaking loop with completedAbruptly true) without processing
     * the task.
     *
     * 4. Assuming beforeExecute completes normally, we run the task,
     * gathering any of its thrown exceptions to send to afterExecute.
     * We separately handle RuntimeException, Error (both of which the
     * specs guarantee that we trap) and arbitrary Throwables.
     * Because we cannot rethrow Throwables within Runnable.run, we
     * wrap them within Errors on the way out (to the thread's
     * UncaughtExceptionHandler).  Any thrown exception also
     * conservatively causes thread to die.
     *
     * 5. After task.run completes, we call afterExecute, which may
     * also throw an exception, which will also cause thread to
     * die. According to JLS Sec 14.20, this exception is the one that
     * will be in effect even if task.run throws.
     *
     * The net effect of the exception mechanics is that afterExecute
     * and the thread's UncaughtExceptionHandler have as accurate
     * information as we can provide about any problems encountered by
     * user code.
     *
     * @param w the worker
     */
    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);
        }
    }

看了一下runWorker代碼後,就進入線程池運行task的關鍵代碼了

task.run()

task.run方法便就是task運行的關鍵代碼,通過觀察其代碼可以發現,task.run()方法是有層層異常捕獲的,那麼如果有oom異常時,就會被其中的Error異常捕獲起來,並拋出來,因爲oom異常是繼承與Error的,其繼承圖如下:
oom類繼承關係

所以,execute方法之所以會拋出oom異常的原因,我們通過上面的分析就已經知道了

ThreadPoolExecutor submit方法

接着,再來了解下線程池的submit方法,看下submit是如何對task運行過程中的異常是如何處理的

還是先從submit方法入口處開始看

    /**
     * @throws RejectedExecutionException {@inheritDoc}
     * @throws NullPointerException       {@inheritDoc}
     */
    public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }

進入newTaskFor方法

    /**
     * Returns a {@code RunnableFuture} for the given runnable and default
     * value.
     *
     * @param runnable the runnable task being wrapped
     * @param value the default value for the returned future
     * @param <T> the type of the given value
     * @return a {@code RunnableFuture} which, when run, will run the
     * underlying runnable and which, as a {@code Future}, will yield
     * the given value as its result and provide for cancellation of
     * the underlying task
     * @since 1.6
     */
    protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
        return new FutureTask<T>(runnable, value);
    }

newTaskFor會新建一個FutureTask,但大家都知道FutureTask是用來包裝callable對象的,那麼這裏傳人的是一個runnable的對象,它會怎麼處理呢?

    /**
     * Creates a {@code FutureTask} that will, upon running, execute the
     * given {@code Runnable}, and arrange that {@code get} will return the
     * given result on successful completion.
     *
     * @param runnable the runnable task
     * @param result the result to return on successful completion. If
     * you don't need a particular result, consider using
     * constructions of the form:
     * {@code Future<?> f = new FutureTask<Void>(runnable, null)}
     * @throws NullPointerException if the runnable is null
     */
    public FutureTask(Runnable runnable, V result) {
        this.callable = Executors.callable(runnable, result);
        this.state = NEW;       // ensure visibility of callable
    }

它會調用Executors.callable方法

    /**
     * Returns a {@link Callable} object that, when
     * called, runs the given task and returns the given result.  This
     * can be useful when applying methods requiring a
     * {@code Callable} to an otherwise resultless action.
     * @param task the task to run
     * @param result the result to return
     * @param <T> the type of the result
     * @return a callable object
     * @throws NullPointerException if task null
     */
    public static <T> Callable<T> callable(Runnable task, T result) {
        if (task == null)
            throw new NullPointerException();
        return new RunnableAdapter<T>(task, result);
    }


    /**
     * A callable that runs given task and returns given result
     */
    static final class RunnableAdapter<T> implements Callable<T> {
        final Runnable task;
        final T result;
        RunnableAdapter(Runnable task, T result) {
            this.task = task;
            this.result = result;
        }
        public T call() {
            task.run();
            return result;
        }
    }

Executors.callable的方法最終會調用一個RunnableAdapter,通過這個適配器它會返回一個Callable的對象

那麼Callable、RunnableAdapter、FutureTask這三個類有什麼關係呢?我們來看一下繼承關係圖
FutureTask的繼承關係圖如下:

FutureTask繼承關係

RunnableAdapter的繼承關係圖如下:
RunnableAdapter繼承關係

通過查看繼承關係可知道,FutureTask與RunnableAdapter都實現了Callable接口,根據多態的原則,那麼RunnableAdapter用FutureTask去接收也是可以的

源碼看到這裏,大家應該都知道了線程池中的submit方法如何將一個Runnable的task轉爲FutureTask了,那麼Java線程池又是如何執行這個FutureTask的呢?

繼續看剛開始的submit方法:

   public Future<?> submit(Runnable task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<Void> ftask = newTaskFor(task, null);
        execute(ftask);
        return ftask;
    }

通過代碼可知,在將runnable的task轉爲了RunnableFuture後,由execute方法進行執行了

    /**
     * Executes the given task sometime in the future.  The task
     * may execute in a new thread or in an existing pooled thread.
     *
     * If the task cannot be submitted for execution, either because this
     * executor has been shutdown or because its capacity has been reached,
     * the task is handled by the current {@code RejectedExecutionHandler}.
     *
     * @param command the task to execute
     * @throws RejectedExecutionException at discretion of
     *         {@code RejectedExecutionHandler}, if the task
     *         cannot be accepted for execution
     * @throws NullPointerException if {@code command} is null
     */
    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        /*
         * Proceed in 3 steps:
         *
         * 1. If fewer than corePoolSize threads are running, try to
         * start a new thread with the given command as its first
         * task.  The call to addWorker atomically checks runState and
         * workerCount, and so prevents false alarms that would add
         * threads when it shouldn't, by returning false.
         *
         * 2. If a task can be successfully queued, then we still need
         * to double-check whether we should have added a thread
         * (because existing ones died since last checking) or that
         * the pool shut down since entry into this method. So we
         * recheck state and if necessary roll back the enqueuing if
         * stopped, or start a new thread if there are none.
         *
         * 3. If we cannot queue task, then we try to add a new
         * thread.  If it fails, we know we are shut down or saturated
         * and so reject the task.
         */
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }

仔細看一下上面的代碼

java.util.concurrent.ThreadPoolExecutor#execute

仔細看一下之後就會發現,這就和上面分析的execute提交runnable的代碼一樣的,就是同一個方法

通過上面的分析我們已近知道了它的處理流程大致是這樣的:
它會先執行一個addWorker方法,然後會新建一個Worker,然後會執行Worker裏的runWorker方法,通過runWorker方法會去執行原來傳進去的task中的run方法

我們知道在runnable是通過submit提交時,那麼這個runnable對象就是FutureTask,那麼runWorker最終執行的也就是java.util.concurrent.FutureTask#run方法了,其源碼如下:

    public void run() {
        if (state != NEW ||
            !UNSAFE.compareAndSwapObject(this, runnerOffset,
                                         null, Thread.currentThread()))
            return;
        try {
            Callable<V> c = callable;
            if (c != null && state == NEW) {
                V result;
                boolean ran;
                try {
                    result = c.call();
                    ran = true;
                } catch (Throwable ex) {
                    result = null;
                    ran = false;
                    setException(ex);
                }
                if (ran)
                    set(result);
            }
        } finally {
            // runner must be non-null until state is settled to
            // prevent concurrent calls to run()
            runner = null;
            // state must be re-read after nulling runner to prevent
            // leaked interrupts
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }

通過上面的代碼可知,如果task在運行過程中有異常拋出,會先在FutureTask裏被先捕獲起來,然後再通過

setException(ex);

方法進行設置,setException源碼如下:

    /**
     * Causes this future to report an {@link ExecutionException}
     * with the given throwable as its cause, unless this future has
     * already been set or has been cancelled.
     *
     * <p>This method is invoked internally by the {@link #run} method
     * upon failure of the computation.
     *
     * @param t the cause of failure
     */
    protected void setException(Throwable t) {
        if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
            outcome = t;
            UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
            finishCompletion();
        }
    }

最終,原來的異常信息就變成outcome的值了,所以當Worker想再像以前去捕獲異常的時候,就根本捕獲不到異常的,因爲FutureTask早已搶先一步

那麼如何獲取outcome的值呢?
在源碼裏搜索outcome的關聯地方即可,會找到report方法

    /**
     * Returns result or throws exception for completed task.
     *
     * @param s completed state value
     */
    @SuppressWarnings("unchecked")
    private V report(int s) throws ExecutionException {
        Object x = outcome;
        if (s == NORMAL)
            return (V)x;
        if (s >= CANCELLED)
            throw new CancellationException();
        throw new ExecutionException((Throwable)x);
    }

再通過report方法尋找調用它的方法,最終會找到get方法

    /**
     * @throws CancellationException {@inheritDoc}
     */
    public V get() throws InterruptedException, ExecutionException {
        int s = state;
        if (s <= COMPLETING)
            s = awaitDone(false, 0L);
        return report(s);
    }

所以看到這裏,我們便知道,對於使用submit提交的任務如果想要獲取它運行過程中的異常,應該用get()方法去接收

總結

來簡短總結一下

ThreadPoolExecutor如果傳入的task是runnable的話,如果讓它拋出異常,就用execute方法提交

如果是用submit方法提交的,那麼就用futureTask的get方法去接收

那麼對於本文的原始問題:

問題:
java線程池ThreadPoolExecutor通過submit提交runnable時,爲何不會拋出oom?

回答:因爲如果使用submit提交的話,如果task中的異常,會被FutureTask先進行捕獲

發佈了111 篇原創文章 · 獲贊 246 · 訪問量 65萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章