java - 两个线程将对象添加到 ArrayList : can't output the size: Why does removing println lead to an infinite loop

标签 java concurrency

我有这个程序:

Farmer.java:

import java.util.*;

public class Farmer implements Runnable  {
  List<Apple> busket;
  private static int count;
  private int id;
  private int appleCount;

  public Farmer(List<Apple> busket)  {
    this.busket = busket;
    id = count++;
  }

  public void makeApple()  {
    Apple apple = new Apple("Apple" + appleCount + " by Farmer"+id);
    System.out.println("making apple: " + apple);

    busket.add(apple);
    appleCount++;
  }

  public void run()  {
    while (appleCount<5)  {
      makeApple();
    }
  }
}

Apple.java:

public class Apple  {
  private String name;

  public String getName() {return name;}

  public String toString() {return name;}

  public Apple(String name)  {
    this.name = name;
  }
}

主.java:

import java.util.*;

class Main {
  public static void main(String[] args) throws Exception {
    System.out.println("Hello world!");

    List<Apple> apples = new ArrayList<>();
    Thread t1 = new Thread(new Farmer(apples));
    t1.start();

    Thread t2 = new Thread(new Farmer(apples));
    t2.start();

    while (apples.size()==0) {
  //    System.out.println(apples.size());
    }

    System.out.println(apples.size());
}
}

它给出了这个输出:

Hello world!
making apple: Apple0 by Farmer1
making apple: Apple1 by Farmer1
making apple: Apple2 by Farmer1
making apple: Apple3 by Farmer1
making apple: Apple4 by Farmer1
making apple: Apple0 by Farmer0
making apple: Apple1 by Farmer0
making apple: Apple2 by Farmer0
making apple: Apple3 by Farmer0
making apple: Apple4 by Farmer0

所以它永远不会打印

System.out.println(apples.size());

但是如果我取消注释

//    System.out.println(apples.size());

在 while 循环中

我得到这个输出:

...many more zeroes
0
0
0
making apple: Apple0 by Farmer1
0
0
...a bit more zeroes
1
making apple: Apple0 by Farmer0
making apple: Apple1 by Farmer0
making apple: Apple2 by Farmer0
making apple: Apple3 by Farmer0
making apple: Apple4 by Farmer0
making apple: Apple1 by Farmer1
making apple: Apple2 by Farmer1
making apple: Apple3 by Farmer1
making apple: Apple4 by Farmer1

你可以看到

1

在上面的输出中,就是数组的大小。

而且程序似乎永远不会停止。

我也试过这个循环:

    int i=0;
    while (apples.size()==0) {
      System.out.println("i: " + i); //works as previous: if uncommented, there's size output later, if commented out, no size output, program keeps running
    }

    int i=0;
    while (apples.size()==0) {
      //System.out.println("i: " + i);
      i++; //doesn't influence - no size output unless uncomment system.out.println
    }

它有相同的行为

为什么会出现这种行为?为什么在 while 循环中添加 print 语句会给出正确的输出(数组的大小和程序的结束),而删除它似乎会使程序卡在 while 循环中,即使数组已填满?

编辑:

我知道使用同步列表可以解决这个问题。我想了解为什么会这样,而不是修复代码。

最佳答案

快速 TL;DR 解决方案:插入一些 synchronized 语句来保护对共享列表的访问!


Java 不保证当多个线程访问同一个变量时没有显式同步时事情会正常工作。

如果你有

while (apples.size()==0) {
    //    System.out.println(apples.size());
}

允许 JIT

  1. 内联 apples.size() 的主体,它很可能只是读取列表对象中的私有(private)变量,并且

  2. 然后发出代码,只从堆中读取变量一次,并一劳永逸地测试它存储在寄存器中的大小副本。其他线程所做的任何事情都不会改变这个本地缓存的大小信息副本。因此,因为列表的长度开始为 0,所以这就是测试将永远看到的 -- 你有一个无限循环。

JIT 被允许这样做是因为 Java 的并发模型表明一个线程不能保证能够看到另一个线程对堆字段所做的更改,直到两个线程都通过了一些适当的同步操作。

当您插入对 System.out.println() 的调用时,循环体现在变得如此复杂以至于 JIT 无法证明 System.out.println () 不会以某种方式 更改列表对象中的内容。 (我们 可以很容易地看出它不会,但 JIT 是一只没有头脑的熊,因此不能)。因此它不敢共享它在循环的前一次迭代中读取的大小字段的值;相反,它会创建机器代码,每次循环都会从堆对象中读取一次大小字段。因此——但绝对是意外多于设计! -- 您的主线程最终从其他线程获取更改。


这并不意味着现在一切都很好,因为您仍然有两个不同的线程试图修改 ArrayList 并且没有任何东西可以防止它们踩到彼此的脚趾。那时可能会出现各种奇怪的事情。它可能只是恰好按照您的预期工作。或者,如果您真的很幸运,您可能会在其中一个线程中抛出一个很好的显式 ConcurrentModificationException。或者一个或多个苹果可能会悄无声息地丢失,或者您可能会遇到 NullPointerException 或更糟的情况,如果您稍后尝试再次将苹果 从列表中取出。

关于java - 两个线程将对象添加到 ArrayList : can't output the size: Why does removing println lead to an infinite loop,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57333778/

相关文章:

java - 如何在 Eclipse 中减少用 JAVA 编写的 AWS Lambda 代码的大小

java - 我的 Java 8 源代码的 Maven 编译失败

shell - 同时启动应用程序池会导致与最大并发 shell 数相关的错误

java - 在 session 范围内的 spring-bean 中同步实例变量以限制对遗留服务的并发请求

java - 尝试在 JTextArea 中使用 BufferedWriter 时发生并发冲突

java - 自动更新jtextfield

java - '@' python 装饰器过去常常做一些类似于 java 中的方法重写的事情?

java - Pie 上的 Camera2 API 越界异常

java - 通过 Jackson ObjectMapper 将字符串序列化为 JSON 作为名称-值

C++11 std::condition_variable:我们可以将锁直接传递给通知线程吗?