ExecutorService(特别是ThreadPoolExecutor)线程安全吗?


77

是否ExecutorService保证线程安全?

我将把来自不同线程的作业提交到同一个ThreadPoolExecutor,在交互/提交任务之前,我是否必须同步对执行者的访问?

Answers:


31

的确,有问题的JDK类似乎并未明确保证线程安全的任务提交。但是,实际上,库中的所有ExecutorService实现实际上都是以这种方式线程安全的。我认为依靠这一点是合理的。由于实现这些功能的所有代码都放置在公共领域中,因此任何人都完全没有动力以另一种方式完全重写它。


“置于公共领域”真的吗?我以为它使用了GPL。
Raedwald 2014年

1
JDK可以,但Doug Lea则不可以。
凯文·布罗里恩

7
关于线程安全任务的提交,有足够的保证:请参阅javadoc的底部以了解interface ExecutorService,它ThreadPoolExecutor也必须遵守。(我最近更新的答案中有更多详细信息。)
卢克·乌舍伍德

如果您有一个具有一个线程的执行程序,并且在该线程中,您想向该执行程序提交工作,等待它完成,那么将出现一个死锁问题,即提交的工作将永远无法运行。使用同步块,当您进入等待模式时,将删除锁。考虑某人正在等待您的任务完成的情况,在该任务中,您可能会根据某些条件来安排要完成的更多工作。然后,您将不得不等待它们完成,以便在实际完成工作时向原始呼叫者发出信号。
毫米

对于任何大小的执行器都是如此。
毫米

55

(相反,其他的答案)线程安全性合同记载:看在interfaceJavadoc中(而不是方法的javadoc)。例如,在ExecutorService javadoc的底部,您可以找到:

内存一致性影响:在将Runnable或Callable任务提交给ExecutorService之前,线程中的操作发生在该任务执行 任何操作之前,而该操作又 发生在通过Future.get()检索结果之前

这足以回答以下问题:

“在交互/提交任务之前,我是否必须同步对执行器的访问?”

不,你不会。ExecutorService无需外部同步就可以构造作业并将其提交给任何(正确实施的)作业。这是主要的设计目标之一。

ExecutorService是一个并发实用程序,也就是说,它是为了在不进行同步的情况下最大程度地运行以提高性能。(同步会导致线程争用,这可能会降低多线程效率,尤其是在扩展到大量线程时。)

有没有关于在任务将执行什么在未来的时间或完全担保(有的甚至会立即执行上提交他们相同的线程),然而工作线程是保证已经看到,提交线程执行的所有效果达投稿点。因此,您的任务(正在运行的线程)还可以安全地读取为使用而创建的任何数据,而无需同步,线程安全的类或任何其他形式的“安全发布”。提交任务的行为本身足以将输入数据“安全发布”到任务。您只需要确保在任务运行时不会以任何方式修改输入数据。

同样,当您通过取回任务的结果时Future.get(),将确保检索线程可以看到执行者的工作线程所产生的所有效果(在返回的结果以及工作线程可能进行的任何副作用更改中) 。

该合同还意味着任务本身可以提交更多任务。

“ ExecutorService是否保证线程安全?”

现在,问题的这一部分更为笼统。例如,找不到关于该方法的任何线程安全性合同声明 shutdownAndAwaitTermination-尽管我注意到Javadoc中的代码示例未使用同步。(尽管也许有一个隐藏的假设,认为关闭是由创建执行器的线程发起的,而不是由工人线程发起的?)

顺便说一句,我推荐本书“ Java Concurrency In Practice”作为并发编程领域的基础。


3
这些并不是完整的线程安全保证;他们仅在特定情况下建立可见性顺序。例如,没有明确记录的保证可以安全地从多个线程(在执行程序上运行的任务的上下文之外)调用execute()。
迈尔斯

1
@Miles经过几年的经验:-) ...我不同意。事前发生关系是Java 5中引入的(突破性的)Java内存模型的基本概念,而Java内存模型又形成了定义并发(相对于同步)线程安全协定的基本构建块。(我再次支持我的原始答案,尽管希望现在可以通过一些编辑更清楚地
卢克·乌舍伍德

9

您的问题是开放式的:ExecutorService接口的所有作用是确保某个地方的某个线程将处理提交的RunnableCallable实例。

如果提交Runnable/Callable引用一个共享的数据结构,是从其他无障碍Runnable/Callable小号情况下(可能是由不同的线程simulataneously处理),那么它是你的责任,以确保在这个数据结构线程安全。

要回答问题的第二部分,是的,您可以在提交任何任务之前访问ThreadPoolExecutor。例如

BlockingQueue<Runnable> workQ = new LinkedBlockingQueue<Runnable>();
ExecutorService execService = new ThreadPoolExecutor(4, 4, 0L, TimeUnit.SECONDS, workQ);
...
execService.submit(new Callable(...));

编辑

基于Brian的评论,以防万一我误解了您的问题:ExecutorService将多个生产者线程中的任务提交给遗嘱通常是线程安全的(尽管据我所知,接口API中并未明确提及)。任何不提供线程安全性的实现在多线程环境中都是无用的(因为多个生产者/多个消费者是一个相当普遍的范例),而这正是ExecutorService(以及其余的java.util.concurrent)设计目的。


8
他不是在问提交是线程安全的吗?即,他可以从不同的主题进行提交
Brian Agnew

1
是的,我问是否可以安全地从多个线程向同一ThreadPoolExecutor实例提交任务。更新了问题,因为一个重要的“同步”一词消失了:|
leeeroy

1
“任何不提供线程安全性的实现在多线程环境中都是无用的”:假设的ExecutorService提供非线程安全的实现并不是完全不可行的,因为单一生产者是一种很常见的模式。(但对于打算用于一般用途的ThreadPoolExecutor,该评论肯定有效)
Miles

6

对于 ThreadPoolExecutor答案是肯定的ExecutorService没有强制要求或以其他方式保证所有的实现是线程安全的,它不能因为它是一个接口。这些类型的合同不在Java接口范围之内。但是,ThreadPoolExecutor两者都是并且被明确记录为线程安全的。此外,ThreadPoolExecutor管理它的作业队列是使用java.util.concurrent.BlockingQueue哪个接口来请求所有实现都是线程安全的。可以安全地假定的任何java.util.concurrent.*实现BlockingQueue都是线程安全的。任何非标准的实现都可能不会,尽管如果有人要提供一个BlockingQueue不是线程安全的实现队列,那将是完全愚蠢的。

所以标题问题的答案很明显 是肯定的。下一个问题的答案可能是,因为两者之间存在一些差异。


4
接口可以并且确实要求线程安全的实现。线程安全是一个已记录的合同,就像任何其他类型的行为(例如List.hashCode())一样。javadocs说“ BlockingQueue实现是线程安全的”(因此,非线程安全的BlockingQueue不仅愚蠢而且有漏洞),但是对于ThreadPoolExecutor或它实现的任何接口都没有这样的文档。
迈尔斯

1
您能否参考清楚说明ThreadPoolExecutor线程安全的文档?
mapeters '17

2

对于ThreadPoolExecutor,它提交是线程安全的。您可以在jdk8中查看源代码。添加新任务时,它使用mainLock来确保线程安全。

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

1

卢克·乌瑟伍德Luke Usherwood)的回答相反,该文档并未暗含ExecutorService保证实现是线程安全的。至于ThreadPoolExecutor具体问题,请参见其他答案。

是的,已指定了事前发生的关系,但这并不意味着有关方法本身的线程安全性,正如Miles所评论的那样。在卢克·厄舍伍德()的回答中,指出前者足以证明后者,但没有提出实际论据。

“线程安全”可能意味着各种各样的事情,但这是一个简单的反例Executor(没有,ExecutorService但没有区别),它微不足道地满足了所需的先发生后关系,但是由于对count字段的不同步访问而不是线程安全的。

class CountingDirectExecutor implements Executor {

    private int count = 0;

    public int getExecutedTaskCount() {
        return count;
    }

    public void execute(Runnable command) {
        command.run();
    }
}

免责声明:我不是专家,我找到了这个问题,因为我自己在寻找答案。


您声明的内容都是正确的,但问题专门询问“我是否必须同步对执行程序的访问”,因此在此上下文中我将阅读“线程安全性”以仅谈论(内部的状态/数据的)线程安全性。 ()执行者,以及调用其方法的动作。
Luke Usherwood '18

如何使提交的任务本身具有“线程安全的副作用”是一个更大的话题!(如果不这样做,则到处都是容易得多。例如,如果可以将某些不可变的计算结果传递回去。当它们确实碰到了可变的共享状态时,请确保:您需要小心定义和理解线程边界并考虑线程安全,死锁,活动锁等)
Luke Usherwood
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.