每日一道面试题(第10期)---谈谈对HandlerThread与AsyncTask的理解

零零碎碎的东西老是记不长久,仅仅学习别人的文章也只是他人咀嚼后留下的残渣。无心中发现了这个每日一道面试题 ,想了想若是只是简单地去思考,那么不只会收效甚微,甚至难一点的题目本身可能都懒得去想,坚持不下来。因此不如把每一次的思考、理解以及别人的看法记录下来。不只加深本身的理解,更要激励本身坚持下去。java

前言

前几天的那个面试弄得我一脸懵逼,如今有点空闲时间,仔细的探讨一下当时的问题。印象最深的就是关于 HandlerThread 与 AsyncTask 的问题。首先列一下探讨过程当中想到的问题以及面试时问的问题。android

  • HandlerThread 与 AsyncTask 的大体实现原理
  • HandlerThread 与 AsyncTask 完成耗时任务后会怎么样
  • HandlerThread 与 AsyncTask 中的耗时事件处理是异步仍是同步,可不能够变为另外一种处理
  • HandlerThread 与 AsyncTask 内存泄漏的可能性
  • HandlerThread 与 AsyncTask 使用中的注意事项
  • HandlerThread 与 AsyncTask 的实际应用场景
  • 给定大量的耗时任务,耗时操做并不连续,耗时时长长短不一,用哪一个

源码解析

先简单讲讲这两个的源码吧,都挺简单的,并无那么复杂。如下源码来自于 Android-28git

HandlerThread

HandlerThread 其实是 Thread+Looper+Handler 的一个简单封装。github

HandlerThread 类继承 Thread 类,因此须要 start() 方法来开启线程。面试

HandlerThread handlerThread = new HandlerThread("workThread");
    handlerThread.start();
    Handler threadHandler = new Handler(handlerThread.getLooper()){
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            //根据不一样的 message 类型处理不一样的耗时任务
        }
    };
复制代码

这是简单的使用方法。建立 Handler 时须要传入 HandlerThread 中的 Looper 对象,这样 handlerMessage 方法就会在子线程中处理耗时任务。须要耗时任务能够经过 threadHandler.sendMessage() 发送消息,而后下 handlerMessage 方法中进行处理。网络

源码中最重要的就是一个重写的 run 方法。并发

@Override
    public void run() {
        mTid = Process.myTid();
        Looper.prepare();
        synchronized (this) {
            mLooper = Looper.myLooper();
            notifyAll();//唤醒线程能够获取 Looper 对象了
        }
        Process.setThreadPriority(mPriority);
        onLooperPrepared();
        Looper.loop();
        mTid = -1;
    }
复制代码

也很简单,在咱们执行 handlerThread.start() 开启一个线程后,就会执行此方法。经过 Looper.prepare() 在此线程中建立一个 Looper 对象,而后通知其余线程能够获取 Looper 对象,设置线程优先级。onLooperPrepared() 是一个空的方法,咱们能够重写此方法进行一些 Looper 开启 loop 循环以前的准备。less

一切都准备好以后,就是经过 Handler 发送消息,而后在 handlerMessage() 中进行耗时操做。简单说明一下,Handler 类中的 handlerMessage() 方法在哪一个线程中执行,是由 Handler 中的 Looper 对象所在的线程决定的,这是由于在 loop 循环中经过 msg.target.dispatchMessage()--->handleMessage() 间接地调用了 handlerMessage 方法,而 Looper.loop 是在子线程中执行的。具体可看 android 的消息机制详解---每日一道面试题(第 9 期)---谈谈 Handler 机制和原理 异步

AsyncTask

AsyncTask 则是线程池与 Handler 的封装ide

一些基本使用就不在详细说明了,主要来看看源码。首先是构造方法,有三个。无参、Handler、Looper,前两个都会调用第三个构造方法。

public AsyncTask(@Nullable Looper callbackLooper) {
        mHandler = callbackLooper == null || callbackLooper == Looper.getMainLooper()
            ? getMainHandler()
            : new Handler(callbackLooper);

        mWorker = new WorkerRunnable<Params, Result>() {
            public Result call() throws Exception {
                mTaskInvoked.set(true);
                Result result = null;
                try {
                    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
                    //noinspection unchecked
                    result = doInBackground(mParams);
                    Binder.flushPendingCommands();
                } catch (Throwable tr) {
                    mCancelled.set(true);
                    throw tr;
                } finally {
                    postResult(result);
                }
                return result;
            }
        };

        mFuture = new FutureTask<Result>(mWorker) {
            @Override
            protected void done() {
                try {
                    postResultIfNotInvoked(get());
                } catch (InterruptedException e) {
                    android.util.Log.w(LOG_TAG, e);
                } catch (ExecutionException e) {
                    throw new RuntimeException("An error occurred while executing doInBackground()",
                            e.getCause());
                } catch (CancellationException e) {
                    postResultIfNotInvoked(null);
                }
            }
        };
    }
复制代码

有点长,但其实就三步操做。

  • 初始化 mHandler。mHandler 是用来转换线程的。 当传入的 Looper 对象不为空且不是主线程的 Looper 时,就建立一个新的 Handler,不然就获取主线程的 handler。getMainHandler() 就是一个简单的建立新的 Handler 对象并将主线程的 Looper 对象传入进去的操做。
  • 初始化 mWorker。mWorker 是一个实现了 Callable 接口的类的对象。初始化时重写了 call 方法,耗时任务 doINbackGround 被封装在这里面执行。
  • 初始化 mFuture。mFuture 是 FutureTask 对象,是 Runnable 与 Future 的子类。将 mWorker 传入 mFuture 对象中。后面就是将此对象传入线程池中进行调度。

一般使用时经过 execute 方法开启任务,看看源码中干了什么。

public final AsyncTask<Params, Progress, Result> execute(Params... params) {
        return executeOnExecutor(sDefaultExecutor, params);
    }
    
    public final AsyncTask<Params, Progress, Result> executeOnExecutor(Executor exec, Params... params) {
        if (mStatus != Status.PENDING) {
            switch (mStatus) {
                case RUNNING:
                    throw new IllegalStateException("Cannot execute task:"
                            + " the task is already running.");
                case FINISHED:
                    throw new IllegalStateException("Cannot execute task:"
                            + " the task has already been executed "
                            + "(a task can be executed only once)");
            }
        }
        mStatus = Status.RUNNING;
        onPreExecute();
        mWorker.mParams = params;
        exec.execute(mFuture);
        return this;
    }
复制代码

execute 中执行的是 executeOnExecutor 方法,并传入 sDefaultExecutor 与耗时任务须要的参数。首先是检查状态,mStatus 是一个枚举变量,有 PENDING、RUNNING、FINSHED 三种状态,这三种状态都是惟一的,按 PENDING---RUNNING---FINISHED 顺序,初始化对象时是 PENDING,在 executeOnExecutor 中变为 RUNNING,在 finish 方法中更新为 FINSHED 状态。所以能够看出 executor 方法只能在一个对象中执行一次,屡次执行就会抛出异常。而后更新状态,调用 onPreExecute 方法,咱们能够种重写此方法作些进行耗时操做前的准备。传入参数,而后就是 exec.execute 提交任务,也就是构造函数中包装好的 FutureTask 对象。

这个 exec 是成员变量 sDefaultExecutor,是 AsyncTask 内部定义的静态类,实现了 Executor 接口。

private static class SerialExecutor implements Executor {
        //双端队列,按照先进先出的原则储存 FutureTask 对象
        final ArrayDeque<Runnable> mTasks = new ArrayDeque<Runnable>();
        Runnable mActive;

        public synchronized void execute(final Runnable r) {
            //对传入的 mFuture 又进行了一次封装,以便于串行处理任务
            mTasks.offer(new Runnable() {
                public void run() {
                    try {
                        r.run();
                    } finally {
                        //执行完上一个耗时任务后,选取下一个任务
                        scheduleNext();
                    }
                }
            });
            //选取任务
            if (mActive == null) {
                scheduleNext();
            }
        }

        protected synchronized void scheduleNext() {
            if ((mActive = mTasks.poll()) != null) {
                //由此可看出 SerialExecutor 只负责任务的串行处理,真正的耗时任务操做是交给 THREAD_POOL_EXECUTOR 线程池进行调度
                THREAD_POOL_EXECUTOR.execute(mActive);
            }
        }
    }
复制代码

这是一个静态类,也就是说全部的耗时任务都要通过此类进行串行处理。SerialExecutor 就是为了使耗时任务可以串行的被处理才存在的,真正处理耗时任务的则是 THREAD_POOL_EXECUTOR 线程池。

//得到没有睡眠的 CPU 数量
    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    // We want at least 2 threads and at most 4 threads in the core pool,
    // preferring to have 1 less than the CPU count to avoid saturating
    // the CPU with background work
    private static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 4));
    private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
    private static final int KEEP_ALIVE_SECONDS = 30;

    private static final ThreadFactory sThreadFactory = new ThreadFactory() {
        private final AtomicInteger mCount = new AtomicInteger(1);//cas 操做的 int 变量
        public Thread newThread(Runnable r) {
            //记录建立线程的数量
            return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
        }
    };

    private static final BlockingQueue<Runnable> sPoolWorkQueue =
            new LinkedBlockingQueue<Runnable>(128);
    
    public static final Executor THREAD_POOL_EXECUTOR;

    static {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
                sPoolWorkQueue, sThreadFactory);
        //设置核心线程池也受设置的存活时间的影响
        threadPoolExecutor.allowCoreThreadTimeOut(true);
        THREAD_POOL_EXECUTOR = threadPoolExecutor;
    }
复制代码

以上就是建立了一个新的线程池,并赋值给静态变量 THREAD_POOL_EXECUTOR,这部分操做是在静态代码块中进行的,也就是说只会在被类加载的时候执行一次。全部的耗时任务都是在这仅有的一个线程池中执行任务。简单说下这个线程池中的参数。

  • CORE_POOL_SIZE:核心线程池数量,这个定义的有点复杂,官方解释说,老是但愿核心线程数量在 2-4 之间,而且更但愿比正在工做的 CPU 数量少 1.
  • MAXIMUM_POOL_SIZE:所存在的最大线程数量,也就是核心线程与非核心线程之和的数量。默认为正在工做的 CPU 数量的 2 倍+1。
  • KEEP_ALIVE_SECONDS:非核心线程完成任务后保持存活的时间,超时将被销毁。若是设置了 allowCoreThreadTimeOut(true) 属性,则核心线程也受此约束。默认为 30
  • TimeUnit.SECONDS:上一个属性的单位,这里是秒。
  • sPoolWorkQueue:储存耗时任务的队列,这里用的 LinkBlockingQueue,基于链表实现的阻塞队列,并将储存数量控制在了 128 个。
  • sThreadFactory:线程工厂,为线程池提供新线程的建立。ThreadFactory 是一个接口,里面只有一个 newThread 方法。 默认为 DefaultThreadFactory 类。

剩下的就是线程池中调度 mFuture 执行耗时任务,执行其中的 mFuture 中 Callable 接口的 call 方法。其实就是上面说到的构造方法中初始化的 mWorker,其中对 call 方法进行了重写,在来了解下。

mWorker = new WorkerRunnable<Params, Result>() {
            public Result call() throws Exception {
                //标记耗时任务已被执行
                mTaskInvoked.set(true);
                Result result = null;
                try {
                    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
                    //noinspection unchecked
                    result = doInBackground(mParams);
                    Binder.flushPendingCommands();
                } catch (Throwable tr) {
                    //若发生异常则设置任务为取消状态
                    mCancelled.set(true);
                    throw tr;
                } finally {
                    //不管如何,处理结果
                    postResult(result);
                }
                return result;
            }
        };
复制代码

能够看出,在进行耗时操做后,不管是处理完,还有发生异常,都要 postResult() 方法进行收尾。

private Result postResult(Result result) {
        @SuppressWarnings("unchecked")
        Message message = getHandler().obtainMessage(MESSAGE_POST_RESULT,
                new AsyncTaskResult<Result>(this, result));
        message.sendToTarget();
        return result;
    }
复制代码

用 Handler 包装了一个信息,发送了出去。标记为 MESSAGE_POST_RESULT,意思就是耗时任务的结果。这个 Handler,就是构造函数中初始化的那个 Handler 对象,只不过在经过 getMainHandler 中是用自定义的静态内部 Handler 类进行了包装。

private static Handler getMainHandler() {
        synchronized (AsyncTask.class) {
            if (sHandler == null) {
                sHandler = new InternalHandler(Looper.getMainLooper());
            }
            return sHandler;
        }
    }
    
    private static class InternalHandler extends Handler {
        public InternalHandler(Looper looper) {
            super(looper);
        }

        @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"})
        @Override
        public void handleMessage(Message msg) {
            //AsyncTask 的静态内部类,方便传递结果数据与对应的 AsyncTask 对象
            AsyncTaskResult<?> result = (AsyncTaskResult<?>) msg.obj;
            switch (msg.what) {
                case MESSAGE_POST_RESULT:
                    // There is only one result
                    result.mTask.finish(result.mData[0]);
                    break;
                case MESSAGE_POST_PROGRESS:
                    result.mTask.onProgressUpdate(result.mData);
                    break;
            }
        }
    }
复制代码

发送的消息就在这里被处理了。若是是 MESSAGE_POST_RESULT,就调用 finish 方法;若是是 MESSAGE_POST_PROGRESS,就是 onProgressUpdate(result.mData) 方法,也就是咱们能够用来重写更新进度的操做。MESSAGE_POST_PROGRESS 类消息只有在你调用 publishProgress 方法时才会被调用。

private void finish(Result result) {
        if (isCancelled()) {
            onCancelled(result);
        } else {
            onPostExecute(result);
        }
        mStatus = Status.FINISHED;
    }
复制代码

finish 方法中会根据 mCancel 的状态决定调用 onCancelled(result) 仍是 onPostExecute(result),也就是说这两个只会调用其中一个方法,这两个方法也是咱们在使用 AsyncTask 须要重写的方法。mCanael 咱们能够经过调用 cancel() 方法改变状态。

public final boolean cancel(boolean mayInterruptIfRunning) {
        mCancelled.set(true);
        //在 mFuture 中中断线程
        return mFuture.cancel(mayInterruptIfRunning);
    }
复制代码

整个内部大体的流程就差不过了。将耗时任务封装进 FutureTask,SerialExecutor 对 FutureTask 在进行包装使耗时任务能够串行执行,最后由 THREAD_POOL_EXECUTOR 线程池进行真正的耗时任务调度处理。

关于HandlerThread与AsyncTask的FAQ

大体实现原理

  • HandlerThread:开启一个子线程,建立新的 Looper 并开启 looper 循环,使子线程一直存在,直到 loop 循环退出。 使用时初始化 HandlerThread,并 new 一个 Handler 并传入 Handler 中的 Looper,就能够经过 handler 发送消息,在 handlerMessage 中接受消息并执行相应的耗时任务。
  • AsyncTask:经过 executor 将封装了 doInBackground 中的耗时任务的 FutureTask 对象传入到 SerialExecutor 中,SerialExecutor 串行的将任务发送给 THREAD_POOL_EXECUTOR 线程池进行调度。

完成耗时任务后会怎么样

  • HandlerThread 是 loop 循环+Handler 消息处理机制,也就是说,只要 loop 循环不退出,那么线程就不会中止,须要处理耗时任务只须要 Handler 发送对应类型的消息便可。
  • AsyncTask 的耗时任务是交给线程池去调度,耗时任务完成后线程的存活与否有线程池的特性决定。而 AsyncTask 的 execute 方法执行一次后,就不能够在此调用。由于内部状态是严格按照 PENDING、RUNNING、FINISHED 顺序变化,不可逆转。在 executeOnExecutor 方法中会检查状态并抛出异常。

耗时事件处理是异步仍是同步,可不能够变为另外一种处理

HandlerThread

HandlerThread 是在子线程的 loop 循环中进行的耗时操做,只有当前的耗时操做完成,才能获取下一个消息处理,因此是串行的。至于变为并行的,不能够。简单验证下

SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss",Locale.US);
    Handler threadHandler;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_login);

        HandlerThread handlerThread = new HandlerThread("workThread");
        handlerThread.start();
        threadHandler = new Handler(handlerThread.getLooper()){
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                Log.e("HandlerThread", "message" + msg.what + ":" + df.format(new Date()));
            }
        };
    }
    
    public void onLoginClick(View v){
        int i = 0;
        threadHandler.sendEmptyMessage(++i);
        threadHandler.sendEmptyMessage(++i);
        threadHandler.sendEmptyMessage(++i);
        threadHandler.sendEmptyMessage(++i);
        threadHandler.sendEmptyMessage(++i);
    }
复制代码

AsyncTask

自定义一个简单的 AsyncTask

static class MyAsyncTask extends AsyncTask<Void, Void, String>{
        String name = "AsyncTask";

        private MyAsyncTask(String name){
            super();
            this.name = name;
        }
        
        @Override
        protected String doInBackground(Void... voids) {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            return name;
        }

        @Override
        protected void onPostExecute(String string) {
            super.onPostExecute(string);
            Log.e("AsyncTask", string + ":" + df.format(new Date()));
        }
    }
复制代码

当咱们进行一连串的耗时任务时,就会串行处理

new MyAsyncTask("AsyncTask#1").execute();
    new MyAsyncTask("AsyncTask#2").execute();
    new MyAsyncTask("AsyncTask#3").execute();
    new MyAsyncTask("AsyncTask#4").execute();
    new MyAsyncTask("AsyncTask#5").execute();
    new MyAsyncTask("AsyncTask#6").execute();
    new MyAsyncTask("AsyncTask#7").execute();
    new MyAsyncTask("AsyncTask#8").execute();
复制代码

从结果中能够看出严格的按照先入先出串行处理任务。那么能不能变为并发的呢?在源码分析中咱们知道串行是受 SerialExecutor 对象进行控制的,而此对象是在 execute 中经过 return executeOnExecutor(sDefaultExecutor, params) 传入进去的。而恰好咱们能够调用 executeOnExecutor 方法。因此咱们能够自定义线程池甚至传入 AsyncTask 的 THREAD_POOL_EXECUTOR 线程池跳过 SerialExecutor 的串行控制,直接用线程池进行并发处理任务。

new MyAsyncTask("AsyncTask#1").executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    new MyAsyncTask("AsyncTask#2").executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    new MyAsyncTask("AsyncTask#3").executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    new MyAsyncTask("AsyncTask#4").executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    new MyAsyncTask("AsyncTask#5").executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    new MyAsyncTask("AsyncTask#6").executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    new MyAsyncTask("AsyncTask#7").executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
    new MyAsyncTask("AsyncTask#8").executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
复制代码

从结果中能够看出,是四个并发进行的任务处理。为何是 4 个呢?这个是线程池的调度问题。测试用的手机正在工做 CPU 数目是 8,因此核心线程池数量是 Math.max(2, Math.min(CPU_COUNT - 1, 4)) = 4,最大的线程数量是 CPU_COUNT * 2 + 1 = 17。线程池首先会用核心线程处理任务,核心线程满了后,后面的认为就会被放入阻塞队列,当队列也满后,再来任务的话,就会建立非核心线程,取队首任务进行处理,后来的任务放入队尾。若是非核心线程也满了,在来任务就会抛出异常。有个图可能看着会更明白些。
因此说任务少是就只是核心线程在并发处理任务。

那么为何要默认串行处理任务,那样岂不是丢失了线程池的最大优点?

这是由于默认线程池其实所能容下的线程并很少,就拿 8 核的例子来看,最大线程数为 17,阻塞队列容量为 128,加起来所能容下的最大线程数为 17+128=145,在高并发的状况下很容易就会满,而且 THREAD_POOL_EXECUTOR 对象在整个应用程序中是惟一的。因此默认是串行处理,若是真的有高并发处理的状况,能够根据需求自定义线程池进行并发处理。

内存泄漏的可能性

  • HandlerThread 的内存泄漏在于必定要记得在 Activity 销毁时手动退出线程。由于 loop 循环是一个死循环,若是不手动退出,就会一直存在。
  • AsyncTask 的内存泄漏在于必定要用静态内部类的形式,内部类会默认持有 Activity 的引用,而 Activity 与 AsyncTask 的生命周期并不能肯定 Activity 更长。

使用中的注意事项

  • 关于 HandlerThread,我没怎么用过,查了网上资料,只有一个说法。给 HandlerThread 设置不一样的优先级,cpu 会根据不一样的线程优先级对全部线程进行优化。
  • 之前的资料都是说 AsyncTask 的对象要在主线程建立,可能后来源码作了修改,如今这个在我看来并不须要,由于内部转换线程的 Handler 的建立并不依赖当前线程的 Looper 对象,而是经过 Looper.getMainLooper() 获取的主线程 Looper。固然,onPreExecute 方法是会受 AsyncTask 对象建立时的线程影响的,由于此方法并非经过 Handler 的消息传递而执行的。
  • 还有一个就是 AsyncTask 取消线程执行的问题,cancel 并不能真正的马上终止程序的执行,它只是改变了标记状态的变量,固然 cancel 方法中的 mFuture.cancel(mayInterruptIfRunning),若是传入 true 也会在 FutureTask 层进行终止线程的操做,但对于一些不可中止的操做,也只能等待任务完成而后根据标记变量状态调用 onCancelled 仍是 onPostExecute。因此说咱们应该在 doInBackground 方法中尽量的不断进行状态的检验,在须要返回时尽早的退出线程。

应用场景

  • HandlerThread 实际上就是 Thread+Looper 的结合,因此也就适用于单线程+多个耗时任务的场景,好比网络请求、文件读写。而对于耗时长的多个任务,我我的认为由于串行执行的关系,HandlerThread 并不适用。
  • AsyncTask 主要用来耗时任务完成后与 UI 线程的交互,不过默认是串行的,可能这也是官方说明 AsyncTask 尽量执行耗时几秒的操做,不过能够直接经过 executeOnExecutor 变为并行处理任务。

给定大量的耗时任务,耗时操做并不连续,耗时时长长短不一,用哪一个

对于这个面试时抛出的问题,我简单说下个人理解。由于我实际开发经验少的可怜,因此说的可能有错误或者很片面,包括上面的几个问题的回答。如今网上的资料真的不敢随便相信,有的还自相矛盾,我突然明白面试时面试官问我日常都看谁的文章,知道哪些在 Android 方面比较专业的人士的用意了。

大量的耗时操做,若是任务之间没有什么关联的话,在我看来若是是串行处理的话都不怎么好,由于会阻塞后面的任务,而任务之间并不须要有个先后执行的顺序。因此在 AsyncTask 中并行处理比较好。而若是任务之间有关联,则需串行执行,此时就要看这些耗时任务的执行逻辑是否一致,若是不一致的话那就要自定义多个 AsyncTask,也非常麻烦。在我看来 AsyncTask 更多的是强调与 UI 线程的交互吧。

其实对于 Android 中的耗时任务处理,HandlerThread、IntentService、AsyncTask、ThreadPoolExecutor 这几个具体应用场景,有哪些差异,还真的说不出来个因此然来,还需努力。(总之就是菜(滑稽))

相关文章
相关标签/搜索