Java 为什么使用parallelStream访问和修改集合会得到不同的结果?

Java 为什么使用parallelStream访问和修改集合会得到不同的结果?,java,multithreading,parallel-processing,java-8,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<I

我对下面的代码感到困惑

  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 + " "));
}
这里有两个问题:

  • 问题1:为什么并行存储的大小不确定

    我了解使用fork/join框架的parallelStream,所以我猜问题是由某个线程没有完成任务造成的,然后我暂停了主线程5秒钟,但似乎没有帮助,parallelStorage的大小仍然保持不变

  • 问题2:为什么并行存储中存在空元素


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

我不知道添加到
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存储到数组大小中。因此,即使“添加”了两个元素,新的数组大小将是4而不是5,并且其中一个新数据元素将丢失

这就是为什么你需要一个同步列表

(在多个线程之间执行步骤的方式是不可预测的。因此,在某些情况下,不同的执行顺序可能会导致在存储元素之前两个线程的大小都会增加,从而导致数组中的元素保持未使用状态,因此为
null
。请不要按顺序执行我在这里发布的步骤是Java运行时实际执行的步骤;这只是一个示例,我没有查看
ArrayList
code。)您自己编写的-这是一个有状态的lambda,应该避免使用。
ArrayList
确实不是线程安全的,收集到这样一个
列表将以一种不可表达的方式破坏一切。尤其是当列表需要在内部将其大小加倍并复制元素时。通常无法判断会发生什么情况appen(或者如果它发生在这种非线程安全的集合中)

但是,即使添加
集合。synchronizedList
仍然是错误的,因为它不能保持顺序(以防您在意)。您唯一能保证的是,所有元素都会被收集,但会以无序的方式收集


forEachOrdered
保留遭遇顺序(如果它没有被其他中间操作破坏,例如
unordered
),但此顺序仅为
forEachOrdered
保留,这并不意味着元素仍按遭遇顺序处理。

From:
聚合操作和并行流使您能够使用非线程安全集合实现并行性,前提是在对集合进行操作时不修改该集合请注意,使用
集合.synchronizedList
将不会保留顺序。唯一的保证是将收集所有元素(例如,不会有空值);但是,
parallelStorage
java8的
ArrayList
中的顺序仍然被打破。将备份数组的创建延迟到添加第一个元素时。因此,当两个或多个线程尝试添加第一个元素时,可能会创建多个数组,这可能会导致在开始时出现
null
元素,甚至没有可见性或重新排序问题。当程序运行时间超过几纳秒时,其他问题可能会出现(或变得更糟…@Eugene Right。我试图回答有关为什么大小不一致以及为什么数组中存在空值的问题,而不是顺序的问题。
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
    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);
1 2 3 4 5 6 7 8 [3, 8, 5, 2, 7, 1, 4, 6]