背景介紹
最近在聽朋友說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異常信息。再觀察下堆情況
但如果將上面提交到線程池的測試代碼換成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的,其繼承圖如下:
所以,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的繼承關係圖如下:
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先進行捕獲