Two threads adding objects to ArrayList: can't output the size: Why does removing println lead to an infinite loop

CyberWolf :

I have this program:

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

Main.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());
}
}

it gives this output:

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

so it never prints

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

however if I uncomment

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

inside while loop

I get this output:

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

you can see the

1

in the above output, which is the size of the array.

and the program never seems to stop.

I also tried this loop:

    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
    }

and

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

and it has the same behaviour

Why is this behaviour? Why adding the print statement inside the while loop gives correct output (array's size and the finishing of the program), while removing it seems to get the program stuck in the while loop even though the array gets filled?

EDIT:

I know it can be solved with using a synchronized list. I want to understand why this behaves the way it does, not fix the code.

hmakholm left over Monica :

Quick TL;DR solution: Insert some synchronized statements to protect access to the shared list already!


Java doesn't guarantee that things will work correctly when more than one thread accesses the same variable without being explicitly synchronized.

In the case where you have

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

the JIT is allowed to

  1. inline the body of apples.size() which most likely just reads a private variable in the list object, and

  2. then emit code that reads the variable only once from the heap and keep testing the copy of the size that it stored in a register once and for all. Nothing the other threads do will change this locally cached copy of the size information. So because the length of the list started at 0, that's what the test will see forever -- you have an infinite loop.

The JIT is allowed to do that because Java's concurrency model says that a thread is not guaranteed to be able to see changes to a heap field that another thread makes, until both threads have passed through some appropriate synchronization actions.

When you insert the call to System.out.println() what happens is that the body of the loop is now so complex that the JIT cannot prove that System.out.println() will not somehow change things inside the list object. (We can see easily that it won't but the JIT is a bear of very little brain and cannot). Therefore it doesn't dare share the value of the size field it read in the previous iteration of the loop; instead it creates machine code that will read the size field from the heap object once for each time around the loop. Therefore -- but definitely more by accident than by design! -- your main thread will eventually pick up the changes from the other threads.


This doesn't mean everything is now fine, because you still have two different threads trying to modify the ArrayList and nothing to prevent them from stepping on each other's toes. All manner of strange things may go wrong then. It may just happen to work as you intended. Or, if you're really lucky, you may get a nice explicit ConcurrentModificationException thrown in one of the threads. Or one or more of the apples may silently be lost, or you may encounter NullPointerException or worse if you later try to take the apples out of the list again.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=311581&siteId=1