使用newSingleThreadExecutor消除ExecutorService中的竞争条件

时间:2018-12-04 06:44:40

标签: java executorservice synchronized

我编写了以下程序来了解赛车:

import java.util.concurrent.*;

class RaceCount
{
    static int count = 0;

    public static void main(String [] args)
    {        
        ExecutorService executor = Executors.newSingleThreadExecutor();

        for (int i = 0; i < 1000; i++)
        {
            executor.submit(new Callable<String>() {
                public String call() throws Exception {
                    count++; return "Incremented";
                }
            });
        }
        executor.shutdown();
        System.out.println(count);
    }
}

很明显,计数少于1000。因此,我将call()方法签名更改为:

public synchronized String call() throws Exception {

但是,结果仍然少于1000。如果我使用 newFixedThreadExecutor(1000)而不是 newSingleThreadExecutor ,那么即使 call()方法不带有 synchronized 关键字作为前缀。
因此,我的查询是:
1.在 newSingleThreadExecutor 情况下如何同步线程?
2.为什么在使用 newFixedThreadExecutor 时不需要同步?

2 个答案:

答案 0 :(得分:3)

您的问题不是由于比赛情况引起的。发生这种情况仅仅是因为executor.shutdown()在返回之前没有等待完全关闭。

这来自java.util.concurrent.ExecutorService.shutdown()的javadocs:

  

...
此方法不等待先前提交的任务完成执行。使用awaitTermination可以做到这一点。

换句话说,System.out.println(count)在某些任务运行之前运行(尽管它必须在所有任务提交之后运行)。

我对您的代码做了一些小的改动,以使这一事实显而易见:

public static void main(String[] args) {
    ExecutorService executor = Executors.newSingleThreadExecutor();

    for (int i = 0; i < 1000; i++) {
        int e = i;
        executor.submit(new Callable<String>() {
            public String call() throws Exception {
                System.out.println("Executing " + e);
                count++;
                return "Incremented";
            }
        });
    }
    executor.shutdown();
    System.out.println("Count: " + count);
}

输出如下:

...
Executing 835
Executing 836
Executing 837
Count: 837     <----- Printed before all tasks are run
Executing 838
Executing 839
Executing 840
Executing 841
...

这清楚地表明,在您读取count变量之后,任务继续运行。

如果需要确保在读取更新后的值之前已执行任务,则可能需要使用awaitTermination,如下所示:

executor.shutdown();
executor.awaitTermination(3, TimeUnit.SECONDS); //Pick an appropriate timeout value
System.out.println("Count: " + count);

答案 1 :(得分:1)

有关关机的部分只是解决方案的一半。 “公共同步的字符串call()”将对调用进行同步,以便只有一个线程可以同时执行一个实例的调用,但是通过“ executor.submit(new Callable()”),您可以拥有1000个调用实例。实际上没有同步。 您可以在循环外将其更改为“ Callable call = new Callable()...”。还有“ executor.submit(call);”内部,以便您拥有一个同步的呼叫实例。或从“ int i”更改为“ AtomicInteger i”,从++ i更改为i.incrementAndGet();