Java 向ArrayList添加对象的两个线程:can';t输出大小:为什么删除println会导致无限循环

Java 向ArrayList添加对象的两个线程:can';t输出大小:为什么删除println会导致无限循环,java,concurrency,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 = b

我有这个计划:

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();
    }
  }
}
public class Apple  {
  private String name;

  public String getName() {return name;}

  public String toString() {return name;}

  public Apple(String name)  {
    this.name = name;
  }
}
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());
}
}
Main.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();
    }
  }
}
public class Apple  {
  private String name;

  public String getName() {return name;}

  public String toString() {return name;}

  public Apple(String name)  {
    this.name = name;
  }
}
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());
}
}
所以它从不打印

System.out.println(apples.size());
但是如果我取消注释

//    System.out.println(apples.size());
内部while循环

我得到这个输出:

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

它也有同样的行为

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

编辑

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();
    }
  }
}
public class Apple  {
  private String name;

  public String getName() {return name;}

  public String toString() {return name;}

  public Apple(String name)  {
    this.name = name;
  }
}
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());
}
}

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

将ArrayList创建包装到一个同步对象将起到作用,因为原始ArrayList实现不是线程安全的,或者您可以使用Vector

import java.util.*;

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

    List<Apple> apples = Collections.synchronizedList(new ArrayList<>());

    //Or
    //List<Apple> apples = new Vector<>();

    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());
}
}
import java.util.*;
班长{
公共静态void main(字符串[]args)引发异常{
System.out.println(“你好,世界!”);
List apples=Collections.synchronizedList(新的ArrayList());
//或
//列表=新向量();
线程t1=新线程(新农民(苹果));
t1.start();
线程t2=新线程(新农民(苹果));
t2.start();
而(apples.size()==0){
//System.out.println(apples.size());
}
System.out.println(apples.size());
}
}

将ArrayList创建包装到一个同步对象将实现这一点,因为原始ArrayList实现不是线程安全的,或者您可以使用Vector

import java.util.*;

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

    List<Apple> apples = Collections.synchronizedList(new ArrayList<>());

    //Or
    //List<Apple> apples = new Vector<>();

    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());
}
}
import java.util.*;
班长{
公共静态void main(字符串[]args)引发异常{
System.out.println(“你好,世界!”);
List apples=Collections.synchronizedList(新的ArrayList());
//或
//列表=新向量();
线程t1=新线程(新农民(苹果));
t1.start();
线程t2=新线程(新农民(苹果));
t2.start();
而(apples.size()==0){
//System.out.println(apples.size());
}
System.out.println(apples.size());
}
}

快速TL;灾难恢复解决方案:插入一些
synchronized
语句以保护对共享列表的访问


当多个线程在没有显式同步的情况下访问同一个变量时,Java不能保证事情能够正常工作

如果你有

while (apples.size()==0) {
    //    System.out.println(apples.size());
}
JIT被允许

  • 内联
    apples.size()
    的主体,它很可能只读取列表对象中的一个私有变量,然后

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

  • JIT可以做到这一点,因为Java的并发模型表示,在两个线程都通过一些适当的同步操作之前,不能保证一个线程能够看到另一个线程对堆字段所做的更改

    当您插入对
    System.out.println()
    的调用时,发生的情况是循环体现在非常复杂,JIT无法证明
    System.out.println()
    不会以某种方式更改列表对象内部的内容。(我们可以很容易地看到,它不会,但JIT是一个大脑非常小的熊,不能)。因此,它不敢共享在循环的上一次迭代中读取的size字段的值;相反,它创建机器代码,在循环中每次从heap对象读取一次size字段。因此——但肯定更多的是偶然而非设计您的主线程最终将从其他线程获取更改



    这并不意味着现在一切都很好,因为仍然有两个不同的线程试图修改
    ArrayList
    ,并且没有任何东西可以阻止它们互相攻击。所有奇怪的事情都可能出差错。它可能恰好按照您的预期工作。或者,如果您真的很幸运,您可能会在其中一个线程中抛出一个漂亮的显式
    ConcurrentModificationException
    。或者一个或多个苹果可能会自动丢失,或者如果以后再次尝试将苹果从列表中删除,您可能会遇到
    NullPointerException
    或更糟的情况。

    Quick TL;灾难恢复解决方案:插入一些
    synchronized
    语句以保护对共享列表的访问


    当多个线程在没有显式同步的情况下访问同一个变量时,Java不能保证事情能够正常工作

    如果你有

    while (apples.size()==0) {
        //    System.out.println(apples.size());
    }
    
    JIT被允许

  • 内联
    apples.size()
    的主体,它很可能只读取列表对象中的一个私有变量,然后

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

  • JIT可以做到这一点,因为Java的并发模型表示,在两个线程都通过一些适当的同步操作之前,不能保证一个线程能够看到另一个线程对堆字段所做的更改

    当您插入对
    System.out.println()
    的调用时,发生的情况是循环体现在非常复杂,JIT无法证明
    System.out