简体   繁体   中英

Thread never stops in Java

I am reading Effective Java and in Chapter 10: Concurrency; Item 66: Synchronize access to shared mutable data , there is some code like this:

public class StopThread {
private static boolean stopRequested;
public static void main(String[] args) throws InterruptedException {
    // TODO Auto-generated method stub
    System.out.println(stopRequested);
    Thread backgroundThread = new Thread(new Runnable(){

        @Override
        public void run() {
            // TODO Auto-generated method stub
            int i = 0;
            while (!stopRequested){
                i++;
            }

            System.out.println("done");
        }

    });
    backgroundThread.start();
    TimeUnit.SECONDS.sleep(1);
    stopRequested = true;
}

}

First, I think the thread should run one second and then stop, since the stopRequested is set to true afterwards. However, the program never stops. It will never print done . The author said

while (!stopRequested)
    i++;

will be transformed into this:

if (!stopRequested)
     while(true)
         i++;

Could someone explain me this?

And another thing I find is that if I change the program to this:

public class StopThread {
private static boolean stopRequested;
public static void main(String[] args) throws InterruptedException {
    // TODO Auto-generated method stub
    System.out.println(stopRequested);
    Thread backgroundThread = new Thread(new Runnable(){

        @Override
        public void run() {
            // TODO Auto-generated method stub
            int i = 0;
            while (!stopRequested){
                i++;
                System.out.println(i);
            }

            System.out.println("done");
        }

    });
    backgroundThread.start();
    TimeUnit.SECONDS.sleep(1);
    stopRequested = true;
}

}

The program runs 1 second and stops as expected. What's the difference here?

I doubt the author actually said that (exactly).

But the point is that

while (!stopRequested)
    i++;

could behave like

if (!stopRequested)
     while(true)
         i++;

since the Java spec allows the initial value of stopRequested to be cached in a register, or fetched from a (potentially stale) copy in the memory cache. One thread is not guaranteed to read the results of memory writes made by another thread unless there is a formal "happens before" relationship between the write and the subsequent read. In this case, there is no such relationship. That means that it is not specified whether the child thread will see the result of the parent thread's assignment to stopRequested .

As the author of that book would have explained, the solutions include:

  • declaring stopRequested as volatile ,
  • making sure that the code that reads and writes stopRequested does so within a synchronized blocks or methods that synchronizes on the same object,
  • uses Lock objects rather than synchronized , or
  • some other concurrency mechanism that satisfies the "happens before" requirement.

Then you ask why your test seemed to work.

That's is explained by the fact that while the child is not guaranteed to see the effect of the parent's assignment, it is also not guaranteed to NOT see it ... either.

Or to put it another way. The Java spec does not say which of the two possibilities will happen.

Now for a specific program, compiled by a specific compiler, run by a specific version of the JVM on specific hardware, you could find that the program behaved one way (or the other way) consistently. Maybe 99.9% of the time. Maybe even 100% of the time. But the same program compiled and run in a different context could behave differently. The JLS says so.


Another explanation for why the two almost identical versions of the program behave differently is that the System.out PrintWriter object is doing some internal synchronization when println is called. That could be giving you a serendipitous "happens before".

I think you are making Joshua Bloch (the author of that great book) say something that he did not say :-). To be precise, the book says the following (only emphasize mine):

In the absence of synchronization , it's quite acceptable for the virtual machine to transform this code:

while (!done)
  i++;

into this code:

if (!done)
  while (true)
    i++;

To understand what he means (it's rather tough to explain it in a way better than he has himself done in pages 261-264, but I will try. Sorry Josh!) you should first try to run this program verbatim and see what is happening. With multithreading, anything is possible, but here's what I did:

  1. Coded up StopThread as is.
  2. Ran it on my Linux computer with JRE 1.8.0_72.
  3. It simply hung! So, the behavior has not changed from what he described.
  4. Then I took the 'thread dump' to see what is happening. You can simply send a kill -3 signal to the running JVM pid to see what the threads are doing. Here's what I observed (relevant portion of the thread dump):
 "DestroyJavaVM" #10 prio=5 os_prio=0 tid=0x00007fd678009800 nid=0x1b35 waiting on condition [0x0000000000000000] java.lang.Thread.State: RUNNABLE "Thread-0" #9 prio=5 os_prio=0 tid=0x00007fd6780f6800 nid=0x1b43 runnable [0x00007fd64b5be000] java.lang.Thread.State: RUNNABLE at StopThread$1.run(StopThread.java:14) at java.lang.Thread.run(Thread.java:745) "Service Thread" #8 daemon prio=9 os_prio=0 tid=0x00007fd6780c9000 nid=0x1b41 runnable [0x0000000000000000] java.lang.Thread.State: RUNNABLE 

As you can see, the background thread that we started is alive, doing something . I looked at my computer's diagnosis tool named top and here is what it shows:

顶部cmd输出 .

Can you see that one of my CPU's (it's a quad core computer) is fully busy (100%!) doing something , moreover, it is the java process that is doing something . Isn't it puzzling? Well it is somewhat puzzling. When a CPU is busily doing something you don't understand, one very likely cause is that it is tirelessly checking contents of some memory location. In this case, if you try to connect the dots, it's the stopRequested variable whose value (as we can expect) it is constantly reading. So, effectively, the CPU is just reading the value of the boolean, finding it false, all the time and it goes back to checking if it has changed! Again, it finds it has not (it is still hanging on my machine as I write this :-)).

You'll say ... Didn't the main thread (which, by the way, has long gone, since it does not appear in the thread dump) stopRequested = true ?

Yes, it did!

Naturally, you would suspect then, why doesn't the Thread-0 see it?

And therein lies the clue. In the presence of a data race, the value that a thread writes is not visible to another thread that reads it.

Now we look at the declaration of that data, that variable that shows this peculiar behavior:

private static boolean stopRequested;

is what it is! This particular declaration is rather underspecified as far as its treatment by various involved parties (the compiler, the just in time compiler and its optimizations ...) is concerned. In the case of such underspecification, anything may happen. In particular, the value that the main thread (thought it) wrote may never be actually written into the main memory for Thread-0 to read, making it go into an infinite loop.

Thus, this is a visibility issue. Without enough synchronization, it is not guaranteed that the value written by a thread will be seen by another thread.

Does that explain? For more details, we all need to have a better understanding of modern hardware. An excellent resource for this is The Art of Multiprocessor Programming by Herlihy and Shavit. This book makes a software engineer understand the intricacies of the hardware and also explains why the multithreading is so hard.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM