ICode9

精准搜索请尝试: 精确搜索
首页 > 编程语言> 文章详细

Java线程池系列之execute和submit区别

2022-01-08 11:59:13  阅读:162  来源: 互联网

标签:execute task Java submit Runnable 线程 null


这是几年前的一道面试题了,网上的答案一般都是

1. execute只能提交Runnable类型的任务,没有返回值,而submit既能提交Runnable类型任务也能提交Callable类型任务,返回Future类型。
2. execute方法提交的任务异常是直接抛出的,而submit方法是是捕获了异常的,当调用FutureTask的get方法时,才会抛出异常。

我们可能看过n多次但又忘了n多加1次,那么我们到底应该如何才能彻底理解和记住这两者的区别呢,这就要从ThreadPoolExecutor的根上说起了。根是什么呢,就是下图的继承体系:

可以看到ThreadPoolExecutor的根上是两个接口,分别是Executor和ExecutorService,查看接口里的方法如下:

从图中就可以很清楚的看到execute和submit的第一个区别了,

execute是Executor接口的方法,而submit是ExecutorService的方法,并且ExecutorService接口继承了Executor接口。

接下来,我们就从这四个类依次入手进行分析。

Executor

Executor接口源码贴在下面了,源码注释很多,对示例部分做了删减。

/**
 * An object that executes submitted {@link Runnable} tasks. This
 * interface provides a way of decoupling task submission from the
 * mechanics of how each task will be run, including details of thread
 * use, scheduling, etc.  An {@code Executor} is normally used
 * instead of explicitly creating threads. 
 *
 * @since 1.5
 * @author Doug Lea
 */
public interface Executor {

    /**
     * Executes the given command at some time in the future.  The command
     * may execute in a new thread, in a pooled thread, or in the calling
     * thread, at the discretion of the {@code Executor} implementation.
     *
     * @param command the runnable task
     * @throws RejectedExecutionException if this task cannot be
     * accepted for execution
     * @throws NullPointerException if command is null
     */
    void execute(Runnable command);
}

从类注释可以看出,作用是解耦任务提交和任务执行,比如执行一个Runnable任务,你可能会使用

new Thread(new(RunnableTask())).start()

而使用Executor之后,你就可以这样写了

Executor executor = anExecutor;
executor.execute(new RunnableTask());

看起来好像代码量增加了,其实不然。对第一种写法,任务的提交和执行就被固定死了,只能新起一个线程去跑,可以说毫无扩展性;而第二种写法,我们可以通过切换anExecutor到anotherExecutor,便可以很容易的改变任务的执行方式,如直接run、起新线程跑、存优先级队列跑,这就是把任务提交和执行解耦的好处。

最后,从方法定义上可以看到,入参是Runnable类型,没有返回值。 

ExecutorService

ExecutorService在Executor基础上,提供了更多的功能,如管理执行器的生命周期和异步任务的执行等。代码如下,还是删减了与本地主题关系不大的注释和方法

/**
 * An {@link Executor} that provides methods to manage termination and
 * methods that can produce a {@link Future} for tracking progress of
 * one or more asynchronous tasks.
 
 * <p>Method {@code submit} extends base method {@link
 * Executor#execute(Runnable)} by creating and returning a {@link Future}
 * that can be used to cancel execution and/or wait for completion.
 *
 * @since 1.5
 * @author Doug Lea
*/
public interface ExecutorService extends Executor {

    <T> Future<T> submit(Callable<T> task);

    <T> Future<T> submit(Runnable task, T result);
    
    Future<?> submit(Runnable task);
}

ExecutorService增加了三个submit方法,都可以返回一个Future对象。通过Future对象,就可以做更多的事情:获取任务执行结果、判断任务是否执行完毕、取消任务等等。

这样我们就发现execute和submit的第二个区别了,

execute只接受Runnable参数,没有返回值;而submit可以接受Runnable参数和Callable参数,并且返回了Future对象,可以进行任务取消、获取任务结果、判断任务是否执行完毕/取消等操作。

AbstractExecutorService

AbstractExecutorService看类名就知道是一个抽象类,它实现了ExecutorService接口,并提供了一些通用的方法实现,其中就实现了三个submit方法。

public abstract class AbstractExecutorService implements ExecutorService {

    protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
        return new FutureTask<T>(runnable, value);
    }
    
    protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
        return new FutureTask<T>(callable);
    }

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

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

    public <T> Future<T> submit(Callable<T> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task);
        execute(ftask);
        return ftask;
    }
}

在submit方法中,首先会判断入参是否为null,如果是就抛出空指针异常,显然线程池不允许提交null任务。

然后第二步,不管入参是Runnable还是Callable,统统封装成RunnableFuture类型的任务,也就是submit方法会对任务做一次适配。RunnableFuture看名字就知道它同时继承了Runnable接口和Future接口,这样就可以作为返回值了。

第三步,调用execute方法。原来是submit会调用execute方法,而AbstractExecutorService没有实现execute方法,显然是留给子类去实现了,这里就用到了模板方法模式。

最后,返回了ftask,上面说了,ftask既是Runnable又是Future,因此我们就可以通过ftask来获取最终任务结果了。

这样我们就发现execute和submit的一个联系了,

submit会对Runnable或Callable入参封装成RunnableFuture对象,调用execute方法并返回。

还有一点值得提一下,这里的newTaskFor方法返回了FutureTask对象,也就是FutureTask是实现了RunnableFuture接口的,而newTaskFor方法是protected的,也就是说我们可以在子类中重写该方法,以达到我们特殊的目的。 

ThreadPoolExecutor

终于到了本文主角——线程池了。

public class ThreadPoolExecutor extends AbstractExecutorService {
    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);
    }
}

ThreadPoolExecutor最终对execute方法做了实现,方法上的注释也解释的很清楚,线程池是如何处理提交过来的任务的。这里不得不赞一下JDK源码的注释,往往比代码都长。

这里只提一下一个容易忽略或看不明白的地方,就是任务放阻塞队列时的一个子分支

else if (workerCountOf(recheck) == 0)
    addWorker(null, false);

这里为什么放完阻塞队列之后还要再次判断下worker的数量呢,并且如果是0的话就会调用addWorkder方法。

其实,举一个实际例子就清楚了,我们在创建线程池的时候,可能想着不保持核心线程,有任务到来时才启动线程,即

ThreadPoolExecutor threadPool = new ThreadPoolExecutor(0, 2, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<>(size));

这样的话,当第一个任务到来时,因为已经达到核心线程数了(其实是0),因此任务会自动放入阻塞队列中,那岂不是永远不会执行了?因此会判断当前有没有工作线程,如果没有就添加一个,并且不需要给添加的这个工作线程指定初始任务,因为它会自动取刚刚的阻塞队列中取任务。

而这里的worker又是什么呢,这里其实我们就可以理解为对一个线程的封装,关键代码如下

private final class Worker extends AbstractQueuedSynchronizer implements Runnable {

    /** 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;

    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);
    }
}

runWorker是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();
            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.run()来执行任务,如果出现异常则会向外抛出,即通过execute来执行任务,执行线程会抛出异常。

FutureTask

对于submit提交的任务又是如何执行的呢,我们就需要查看newTaskFor返回的FutureTask了,关键部分代码如下

public class FutureTask<V> implements RunnableFuture<V> {
    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 = null;
            int s = state;
            if (s >= INTERRUPTING)
                handlePossibleCancellationInterrupt(s);
        }
    }
    
    public V get() throws InterruptedException, ExecutionException {
        int s = state;
        if (s <= COMPLETING)
            s = awaitDone(false, 0L);
        return report(s);
    }
    
    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);
    }
}

可以看出FutureTask和runWorker的区别了,runWorker是catch到异常接着throw出去,完全不接锅;而FutureTask则是将catch到的异常存下来,在get的时候再去判断任务执行状态,如果任务状态时异常,则再抛出ExecutionException异常。

因此execute和submit的第三个区别就是

通过execute方法提交的任务如果出现异常则直接抛出原异常,是在线程池中的线程中;而submit方法是捕获了异常的,只有当调用Future的get方法时,才会抛出ExecutionException异常,且是在调用get方法的线程。

实际效果如图

总结

经过以上分析,我们得出了execute和submit的区别了

1. execute是Executor接口的方法,而submit是ExecutorService的方法,并且ExecutorService接口继承了Executor接口。
2. execute只接受Runnable参数,没有返回值;而submit可以接受Runnable参数和Callable参数,并且返回了Future对象,可以进行任务取消、获取任务结果、判断任务是否执行完毕/取消等操作。其中,submit会对Runnable或Callable入参封装成RunnableFuture对象,调用execute方法并返回。
3. 通过execute方法提交的任务如果出现异常则直接抛出原异常,是在线程池中的线程中;而submit方法是捕获了异常的,只有当调用Future的get方法时,才会抛出ExecutionException异常,且是在调用get方法的线程。

最后,附上线程池相关的类图,以便清晰地了解这些类之间的关联关系

 

标签:execute,task,Java,submit,Runnable,线程,null
来源: https://blog.csdn.net/happyyear1/article/details/122378013

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有