为什么使用parallelStream访问和修改Collection得到不同的结果?

时间:2017-06-06 04:39:39

标签: java multithreading parallel-processing java-8

我对以下代码感到困惑

  public static void main(String[] args) throws InterruptedException
  {
    Integer[] intArray = {1, 2, 3, 4, 5, 6, 7, 8};
    List<Integer> listOfIntegers =
            new ArrayList<>(Arrays.asList(intArray));
    List<Integer> parallelStorage = new ArrayList<>();//Collections.synchronizedList(new ArrayList<>());
    listOfIntegers
            .parallelStream()
            // Don't do this! It uses a stateful lambda expression.
            .map(e -> {
                parallelStorage.add(e);
                return e;
            })
            .forEachOrdered(e -> System.out.print(e + " "));
    System.out.println();
    parallelStorage
            .stream()
            .forEachOrdered(e -> System.out.print(e + " "));
    System.out.println();
    System.out.println("Sleep 5 sec");
    TimeUnit.SECONDS.sleep(5);
    parallelStorage
            .stream()
            .forEachOrdered(e -> System.out.print(e + " "));
}

EveryTime执行它我得到了不同的结果,这让我很困惑, 这里有一些结果:

Result 1:
1 2 3 4 5 6 7 8 
null 3 8 7 1 4 5 6 
Sleep 5 sec
null 3 8 7 1 4 5 6

Result 2:
1 2 3 4 5 6 7 8 
6 2 4 1 5 7 8 
Sleep 5 sec
6 2 4 1 5 7 8

以下是两个问题:

  • Q1:为什么parallelStorage的大小不确定?

    我理解使用fork / join框架的parallelStream,所以我猜这个问题是由一些没有完成工作的线程引起的,然后我暂停主线程5秒,但似乎没有帮助,大小parallelStorage仍保持不变;

  • Q2:为什么parallelStorage中存在null元素?

2 个答案:

答案 0 :(得分:5)

ArrayList不是线程安全的。这意味着如果你有两个线程同时更新列表,那么两个线程可能会以可能导致数据丢失的方式相互干扰(或者,对于某些数据结构,可能会完全破坏结构)。

我不知道您添加到ArrayList时所采取的步骤的确切顺序,但让我们说它是这样的。 ArrayList应包含一个支持数组,以及一个指示当前大小

的实例变量
  • 将数组大小读入本地变量N
  • 将新元素放入arr[N]
  • 将{1添加到N
  • N存储回数组大小

现在假设你有两个线程这样做。由于没有同步,如果线程同时调用add,则线程可以按此顺序执行步骤:

Read the array size into N
                                    Read the array size into N
Put the new element in arr[N]
                                    Put the new element in arr[N]
Add 1 to N 
                                    Add 1 to N
Store N into the array size
                                    Store N into the array size

如果在线程调用add之前数组大小为3,请注意两个线程将3读入它们自己的局部变量N;然后他们将新元素放在同一个地方,然后将4存储到数组大小。因此,尽管添加了两个元素&#34;,但新数组大小将为4而不是5,并且其中一个新数据元素将丢失。

这就是你需要同步列表的原因。

(在多个线程之间完成步骤的方式是不可预测的。因此,在某些情况下,不同的执行顺序可能导致在存储元素之前两个线程增加大小,这是可信的。导致数组中的元素保持未使用状态,因此为null。请不要将我在此处发布的步骤序列作为Java运行时所采取的实际步骤;它只是一个示例,我没有查看ArrayList代码。)

答案 1 :(得分:4)

已经自己写了 - 这是一个有状态的 lambda,应该避免这些。 ArrayList确实不是线程安全的,收集到这样的List会以未经证实的方式破坏事物。特别是当列表需要在内部加倍它的大小并复制元素时。通常无法判断会发生什么(或者如果它发生在非线程安全的收集中)。

但即使添加Collections.synchronizedList仍然是错误的,因为它不会保留顺序(如果你关心的话)。您唯一的保证是确实会收集所有元素,但以无序方式

    Integer[] intArray = { 1, 2, 3, 4, 5, 6, 7, 8 };
    List<Integer> listOfIntegers = new ArrayList<>(Arrays.asList(intArray));
    List<Integer> parallelStorage = Collections.synchronizedList(new ArrayList<>(1));
    listOfIntegers
            .parallelStream()
            // Don't do this! It uses a stateful lambda expression.
            .map(e -> {
                parallelStorage.add(e);
                return e;
            })
            .forEachOrdered(e -> System.out.print(e + " "));
    System.out.println(parallelStorage);

你唯一确定的是parallelStorage确实拥有listOfIntegers中的所有元素(而不是你看到null的普通ArrayList);但否则订单仍将被破坏。

你可以很容易地看到这样的结果:

1 2 3 4 5 6 7 8 [3, 8, 5, 2, 7, 1, 4, 6]

forEachOrdered保留遭遇顺序(如果没有被其他中间操作破坏,例如unordered),但此顺序仅保留forEachOrdered,但这并不意味着在遇到订单中,元素仍然已处理