Just started with threads in java and I can't reason with the output of my program
public class ThreadExample extends Thread{
private int info;
static int x = 0;
public ThreadExample (int info) {
this.info = info;
}
public void run () {
if ( info == 1 ) {
x = 3;
System.out.println(Thread.currentThread().getName() + " " + x);
} else{
x = 1;
System.out.println(Thread.currentThread().getName() + " " + x);
}
}
public static void main (String args []) {
ThreadExample aT1 = new ThreadExample(1);
ThreadExample aT2 = new ThreadExample(2);
aT1.start();
aT2.start();
System.err.println(x);
}
}
Output:
Thread-0 3
Thread-1 1
3
Why does it print 3 even though the 2nd thread changed the value of the static variable to 1?
Will there be 3 threads running concurrently?
If you change a variable in one thread it not immediately (or necessary ever) visible to a 2nd thread unless you use some kind of synchronization primitive like a Mutex. You can also use the atomic classes like AtomicInteger to ensure changes made in one thread become visible to the other.
There's a lot more information available in the documentation.
Two possible scenarios
Thread 2 would have updated x before Thread 1. You cannot determine how the execution interleaved between the two threads based on the order of the print statements you are seeing.
The threads indeed executed in the order you expect. But since x is not volatile you might not see the updated value.
See - What is the volatile keyword useful for
You cannot predict the result of threading.
It may be different if you run your code on another device or just multiple times.
You cannot (or should not) rely on timing or the scheduler.
I think that concurrency/non-volatility itself may not be the only problem but flushing is also something you may want to take into consideration:
x=3 (ThreadExample(1))
sysout 3 (ThreadExample(1))
syserr x (main thread)
x=1 (ThreadExample(2))
sysout 3 (ThreadExample (2))
flush stdout (caused by jvm exit)
flush stderr (caused by jvm exit)
Note the flush at the end. stdout and stderr may not be synchronized.
Those streams are buffered and written to the console at any time.
While two things written to stdout or stderr are guaranteed to be written in the correct order, this is not assured if you pring one thing to stdout and another thing to stderr.
It is also guaranteed that everything printed to stdout and stderr is written when the jvm terminates normally(no kill -9 or similar).
If the jvm writes stdout before stderr, you can get your result.
If you want the output to be printed correctly, you may want to do two things:
Call flush manually after printing
create a synchronized block(or similar) around the operation, the println and the flush. (Note that you may lose a bit of performance/parallelism with that)
If you want to test if flushing makes a difference in your case, add System.err.flush(); (so that stderr is flushed before stdout)at the end of your profram and see if there is a difference.
Also, there obe more thing that I didn't find in other answers, explicitly: JIT optimization.
The JIT compiler may make optimizations to your program. For example, it could optimize:
x=3;
System.out.println(x);
to:
x=3;
System.out.println(3);
so that it prints 3, even if it is not 3 at the time the println is called.
Variables are not recommended way to exchange information between threads. Use BlockingQueues for messages, Semaphores and CountDownLatches for signals. In short, transfer of a value must not only make silent assignment, but also create some kind of event, to notify other threads. I like word "token" for such objects.
Will there be 3 threads running concurrently?
Yes. The first thread is the main thread, the one which started it all, the one which invoked your public static void main (String args []) method. All code runs on a thread. Your main method then starts 2 threads. Since you started with 1, you now have 3.
As for why the final output from the main thread is 3 is hard to answer because you have a race condition. You have 3 threads reading a variable while 2 of them update, and these all happen concurrently.
x = 3;
System.out.println(Thread.currentThread().getName() + " " + x);
With 3 threads running, it's easy to assume the output of System.out.println above will be 3, but the reality is, after setting it to 3, another thread could have updated it and then when you print it, it's no longer 3.
Also consider the volatile keyword. Without it, the JVM may cache within a thread copies of shared values, which can lead to staleness when reading and writing across threads. What is the volatile keyword useful for
The outcome of threads is unpredictable.
To ensure consistent/predictable behavior use volatile/Atomic values to make the change visible to other threads
Related
I have the following code :
class thread_creation extends Thread{
int t;
thread_creation(int x){
t=x;
}
public void run() {
increment();
}
public void increment() {
for(int i =0 ; i<10 ; i++) {
t++;
System.out.println(t);
}
}
}
public class test {
public static void main(String[] args) {
int i =0;
thread_creation t1 = new thread_creation(i);
thread_creation t2 = new thread_creation(i);
t1.start();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
t2.start();
}
}
When I run it , I get :
1
2
3
4
5
6
7
8
9
10
1
2
3
4
5
6
7
8
9
10
Why I am getting this output ? According to my understanding , the variable i is a shared variable between the two threads created. So according to the code , the first thread will execute and increments i 10 times , and hence , i will be equal to 10 . The second thread will start after the first one because of the sleep statement and since i is shared , then the second thread will start will i=10 and will start incrementing it 10 times to have i = 20 , but this is not the case in the output , so why that ?
You seem to think that int t; in thread_creation is a shared variable. I'm afraid you are mistaken. Each t instance is a different variable. So the two threads are updating distinct counters.
The output you are seeing reflects that.
This is the nub of your question:
How do I pass a shared variable then ?
Actually, you can't1. Strictly a shared variable is actually a variable belonging to a shared object. You cannot pass a variable per se. Java does not allow passing of variables. This is what "Java does not support call-by-reference" really means. You can't pass or return a variable or the address of a variable in any method call. (Or in any other way.)
In Java you pass and return values: either primitives, or references to objects. The values may read from a variable by the call's parameter expression or assigned to a variable after the call's return. But you are not passing the variable. A variable and its value / contents are different things.
So the only way to implement a shared counter is to implement it as a shared counter object.
Note that "variable" and "object" mean different things, both in Java and in other programming languages. You should NOT use the two terms interchangeable. For example, when I declare this in Java:
String s = "Hello";
the s variable is not a String object. It is a variable that contains a reference to the String object. Other variables may contain references to the same String object as well. The distinction is even more stark when the objects are mutable. (String is not mutable ... in Java.)
Here are the two (IMO) best ways to implement a shared counter object.
You could create a custom Java Counter class with a count variable, a get method, and methods for incrementing, decrementing the counter. The class needs to implement various methods as thread-safe and atomic; e.g. by using synchronized methods or blocks2.
You could just use an AtomicInteger instance. That takes care of atomicity and thread-safety ... to the extent that it is possible with this kind of API.
The latter approach is simpler and likely more efficient ... unless you need to do something special each time the counter changes.
(It is conceivable that you could implement a shared counter other ways, but that is too much detail for this answer.)
1 - I realize that I just said the same thing more than 3 times. But as the Bellman says in "The Hunting of the Snark": "What I tell you three times is true."
2 - If the counter is not implemented using synchronized or an equivalent mutual exclusion mechanism with the appropriate happens before semantics, you are liable to see Heisenbugs; e.g. race conditions and memory visibility problems.
Two crucial things you're missing. Both individually explain this behaviour - you can 'fix' either one and you'll still see this, you'd have to fix both to see 1-20:
Java is pass-by-value
When you pass i, you pass a copy of it. In fact, in java, all parameters to methods are always copies. Hence, when the thread does t++, it has absolutely no effect whatsoever on your i. You can trivially test this, and you don't need to mess with threads to see it:
public static void main(String[] args) {
int i = 0;
add5(i);
System.out.println(i); // prints 0!!
}
static void add5(int i) {
i = i + 5;
}
Note that all non-primitives are references. That means: A copy of the reference is passed. It's like passing the address of a house and not the house itself. If I have an address book, and I hand you a scanned copy of a page that contains the address to my summer home, you can still drive over there and toss a brick through the window, and I'll 'see' that when I go follow my copy of the address. So, when you pass e.g. a list and the method you passed the list to runs list.add("foo"), you DO see that. You may think: AHA! That means java does not pass a copy, it passed the real list! Not so. Java passed a copy of a street address (A reference). The method I handed that copy to decided to drive over there and act - that you can see.
In other words, =, ++, that sort of thing? That is done to the copy. . is java for 'drive to the address and enter the house'. Anything you 'do' with . is visible to the caller, = and ++ and such are not.
Fixing the code to avoid the pass-by-value problem
Change your code to:
class thread_creation extends Thread {
static int t; // now its global!
public void run() {
increment();
}
public void increment() {
for(int i =0 ; i<10 ; i++) {
t++;
// System.out.println(t);
}
}
}
public class test {
public static void main(String[] args) throws Exception {
thread_creation t1 = new thread_creation();
thread_creation t2 = new thread_creation();
t1.start();
Thread.sleep(500);
t2.start();
Thread.sleep(500);
System.out.println(thread_creation.t);
}
}
Note that I remarked out the print line. I did that intentionally - see below. If you run the above code, you'd think you see 20, but depending on your hardware, the OS, the song playing on your mp3 playing app, which websites you have open, and the phase of the moon, it may be less than 20. So what's going on there? Enter the...
The evil coin.
The relevant spec here is the JMM (The Java Memory Model). This spec explains precisely what a JVM must do, and therefore, what a JVM is free not to do, especially when it comes to how memory is actually managed.
The crucial aspect is the following:
Any effects (updates to fields, such as that t field) may or may not be observable, JVM's choice. There's no guarantee that anything you do is visible to anything else... unless there exists a Happens-Before/Happens-After relationship: Any 2 statements with such a relationship have the property that the JVM guarantees that you cannot observe the lack of the update done by the HB line from the HA line.
HB/HA can be established in various ways:
The 'natural' way: Anything that is 'before' something else _and runs in the same thread has an HB/HA relationship. In other words, if you do in one thread x++; System.out.println(x); then you can't observe that the x++ hasn't happened yet. It's stated like this so that if you're not observing, you get no guarantees, which gives the JVM the freedom to optimize. For example, Given x++;y++; and that's all you do, the JVM is free to re-order that and increment y before x. Or not. There are no guarantees, a JVM can do whatever it wants.
synchronized. The moment of 'exiting' a synchronized (x) {} block has HB to the HA of another thread 'entering' the top of any synchronized block on the same object, if it enters later.
volatile - but note that with volatile it's basically impossible which one came first. But one of them did, and any interaction with a volatile field is HB relative to another thread accessing the same field later.
thread starting. thread.start() is HB relative to the first line of the run() of that thread.
thread yielding. thread.yield() is HA relative to the last line of the thread.
There are a few more exotic ways to establish HB/HA but that's pretty much it.
Crucially, in your code there is no HB/HA between any of the statements that modify or print t!
In other words, the JVM is free to run it all in such a way that the effects of various t++ statements run by one thread aren't observed by another thread.
What the.. WHY????
Because of efficiency. Your memory banks on your CPU are, relative to how fast CPUs are, oceans away from the CPU core. Fetching or writing to core memory from a CPU takes an incredibly long time - your CPU is twiddling its thumbs for a very long time while it waits for the memory controller to get the job done. It could be running hundreds of instructions in that time.
So, CPU cores do not write to memory AT ALL. Instead they work with caches: They have an on-core cache page, and the only interaction with your main memory banks (which are shared by CPU cores) is 'load in an entire cache page' and 'write an entire cache page'. That cache page is then effectively a 'local copy' that only that core can see and interact with (but can do so very very quickly, as that IS very close to the core, unlike the main memory banks), and then once the algorithm is done it can flush that page back to main memory.
The JVM needs to be free to use this. Had the JVM actually worked like you want (that anything any thread does is instantly observable by all others), then anything that any line does must first wait 500 cycles to load the relevant page, then wait another 500 cycles to write it back. All java apps would literally be 1000x slower than they could be.
This in passing also explains that actual synchronizing is really slow. Nothing java can do about that, it is a fundamental limitation of our modern multi-core CPUs.
So, evil coin?
Note that the JVM does not guarantee that the CPU must neccessarily work with this cache stuff, nor does it make any promises about when cache pages are flushed. It merely limits the guarantees so that JVMs can be efficiently written on CPUs that work like that.
That means that any read or write to any field any java code ever does can best be thought of as follows:
The JVM first flips a coin. On heads, it uses a local cached copy. On tails, it copies over the value from some other thread's cached copy instead.
The coin is evil: It is not reliably a 50/50 arrangement. It is entirely plausible that throughout developing a feature and testing it, the coin lands tails every time it is flipped. It remains flipping tails 100% of the time for the first week that you deployed it. And then just when that big potential customer comes in and you're demoing your app, the coin, being an evil, evil coin, starts flipping heads a few times and breaking your app.
The correct conclusion is that the coin will mess with you and that you cannot unit test against it. The only way to win the game is to ensure that the coin is never flipped.
You do this by never touching a field from multiple threads unless it is constant (final, or simply never changes), or if all access to it (both reads and writes) has clearly established HB/HA between all threads.
This is hard to do. That's why the vast majority of apps don't do it at all. Instead, they:
Talk between threads using a database, which has vastly more advanced synchronization primitives: Transactions.
Talk using a message bus such as RabbitMQ or similar.
Use stuff from the java.util.concurrent package such as a Latch, ForkJoin, ConcurrentMap, or AtomicInteger. These are easier to use (specifically: It is a lot harder to write code for these abstractions that is buggy but where the bug cannot be observed or tested for on the machine of the developer that wrote it, it'll only blow up much later in production. But not impossible, of course).
Let's fix it!
volatile doesn't 'fix' ++. x++; is 'read x, increment by 1, write result to x' and volatile doesn't make that atomic, so we cannot use this. We can either replace t++ with:
synchronized(thread_creation.class) {
t++;
}
Which works fine but is really slow (and you shouldn't lock on publicly visible stuff if you can help it, so make a custom object to lock on, but you get the gist hopefully), or, better, dig into that j.u.c package for something that seems useful. And so there is! AtomicInteger!
class thread_creation extends Thread {
static AtomicInteger t = new AtomicInteger();
public void run() {
increment();
}
public void increment() {
for(int i =0 ; i<10 ; i++) {
t.incrementAndGet();
}
}
}
public class test {
public static void main(String[] args) throws Exception {
thread_creation t1 = new thread_creation();
thread_creation t2 = new thread_creation();
t1.start();
Thread.sleep(500);
t2.start();
Thread.sleep(500);
System.out.println(thread_creation.t.get());
}
}
That code will print 20. Every time (unless those threads take longer than 500msec which technically could be, but is rather unlikely of course).
Why did you remark out the print statement?
That HB/HA stuff can sneak up on you: When you call code you did not write, such as System.out.println, who knows what kind of HB/HA relationships are in that code? Javadoc isn't that kind of specific, they won't tell you. Turns out that on most OSes and JVM implementations, interaction with standard out, such as System.out.println, causes synchronization; either the JVM does it, or the OS does. Thus, introducing print statements 'to test stuff' doesn't work - that makes it impossible to observe the race conditions your code does have. Similarly, involving debuggers is a great way to make that coin really go evil on you and flip juuust so that you can't tell your code is buggy.
That is why I remarked it out, because with it in, I bet on almost all hardware you end up seeing 20 eventhough the JVM doesn't guarantee it and that first version is broken. Even if on your particular machine, on this day, with this phase of the moon, it seems to reliably print 20 every single time you run it.
I have a String and ThreadPoolExecutor that changes the value of this String. Just check out my sample:
String str_example = "";
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(10, 30, (long)10, TimeUnit.SECONDS, runnables);
for (int i = 0; i < 80; i++){
poolExecutor.submit(new Runnable() {
#Override
public void run() {
try {
Thread.sleep((long) (Math.random() * 1000));
String temp = str_example + "1";
str_example = temp;
System.out.println(str_example);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}
so after executing this, i get something like that:
1
11
111
1111
11111
.......
So question is: i just expect the result like this if my String object has volatile modifier. But i have the same result with this modifier and without.
There are several reasons why you see "correct" execution.
First, CPU designers do as much as they can so that our programs run correctly even in presence of data races. Cache coherence deals with cache lines and tries to minimize possible conflicts. For example, only one CPU can write to a cache line at some point of time. After write was done other CPUs should request that cache line to be able to write to it. Not to say x86 architecture(most probable which you use) is very strict comparing to others.
Second, your program is slow and threads sleep for some random period of time. So they do almost all the work at different points of time.
How to achieve inconsistent behavior? Try something with for loop without any sleep. In that case field value most probably will be cached in CPU registers and some updates will not be visible.
P.S. Updates of field str_example are not atomic so you program may produce the same string values even in presense of volatile keyword.
When you talk about concepts like thread caching, you're talking about the properties of a hypothetical machine that Java might be implemented on. The logic is something like "Java permits an implementation to cache things, so it requires you to tell it when such things would break your program". That does not mean that any actual machine does anything of the sort. In reality, most machines you are likely to use have completely different kinds of optimizations that don't involve the kind of caches that you're thinking of.
Java requires you to use volatile precisely so that you don't have to worry about what kinds of absurdly complex optimizations the actual machine you're working on might or might not have. And that's a really good thing.
Your code is unlikely to exhibit concurrency bugs because it executes with very low concurrency. You have 10 threads, each of which sleep on average 500 ms before doing a string concatenation. As a rough guess, String concatenation takes about 1ns per character, and because your string is only 80 characters long, this would mean that each thread spends about 80 out of 500000000 ns executing. The chance of two or more threads running at the same time is therefore vanishingly small.
If we change your program so that several threads are running concurrently all the time, we see quite different results:
static String s = "";
public static void main(String[] args) throws Exception {
ExecutorService executor = Executors.newFixedThreadPool(5);
for (int i = 0; i < 10_000; i ++) {
executor.submit(() -> {
s += "1";
});
}
executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES);
System.out.println(s.length());
}
In the absence of data races, this should print 10000. On my computer, this prints about 4200, meaning over half the updates have been lost in the data race.
What if we declare s volatile? Interestingly, we still get about 4200 as a result, so data races were not prevented. That makes sense, because volatile ensures that writes are visible to other threads, but does not prevent intermediary updates, i.e. what happens is something like:
Thread 1 reads s and starts making a new String
Thread 2 reads s and starts making a new String
Thread 1 stores its result in s
Thread 2 stores its result in s, overwriting the previous result
To prevent this, you can use a plain old synchronized block:
executor.submit(() -> {
synchronized (Test.class) {
s += "1";
}
});
And indeed, this returns 10000, as expected.
It is working because you are using Thread.sleep((long) (Math.random() * 100));So every thread has different sleep time and executing may be one by one as all other thread in sleep mode or completed execution.But though your code is working is not thread safe.Even if you use Volatile also will not make your code thread safe.Volatile only make sure visibility i.e when one thread make some changes other threads are able to see it.
In your case your operation is multi step process reading the variable,updating then writing to memory.So you required locking mechanism to make it thread safe.
In an effort to practice my rusty Java, I wanted to try a simple multi-threaded shared data example and I came across something that surprised me.
Basically we have a shared AtomicInteger counter between three threads that each take turns incrementing and printing the counter.
main
AtomicInteger counter = new AtomicInteger(0);
CounterThread ct1 = new CounterThread(counter, "A");
CounterThread ct2 = new CounterThread(counter, "B");
CounterThread ct3 = new CounterThread(counter, "C");
ct1.start();
ct2.start();
ct3.start();
CounterThread
public class CounterThread extends Thread
{
private AtomicInteger _count;
private String _id;
public CounterThread(AtomicInteger count, String id)
{
_count = count;
_id = id;
}
public void run()
{
while(_count.get() < 1000)
{
System.out.println(_id + ": " + _count.incrementAndGet());
Thread.yield();
}
}
}
I expected that when each thread executed Thread.yield(), that it would give over execution to another thread to increment _count like this:
A: 1
B: 2
C: 3
A: 4
...
Instead, I got output where A would increment _count 100 times, then pass it off to B. Sometimes all three threads would take turns consistently, but sometimes one thread would dominate for several increments.
Why doesn't Thread.yield() always yield processing over to another thread?
I expected that when each thread executed Thread.yield(), that it would give over execution to another thread to increment _count like this:
In threaded applications that are spinning, predicting the output is extremely hard. You would have to do a lot of work with locks and stuff to get perfect A:1 B:2 C:3 ... type output.
The problem is that everything is a race condition and unpredictable due to hardware, race-conditions, time-slicing randomness, and other factors. For example, when the first thread starts, it may run for a couple of millis before the next thread starts. There would be no one to yield() to. Also, even if it yields, maybe you are on a 4 processor box so there is no reason to pause any other threads at all.
Instead, I got output where A would increment _count 100 times, then pass it off to B. Sometimes all three threads would take turns consistently, but sometimes one thread would dominate for several increments.
Right, in general with this spinning loops, you see bursts of output from a single thread as it gets time slices. This is also confused by the fact that System.out.println(...) is synchronized which affects the timing as well. If it was not doing a synchronized operation, you would see even more bursty output.
Why doesn't Thread.yield() always yield processing over to another thread?
I very rarely use Thread.yield(). It is a hint to the scheduler at best and probably is ignored on some architectures. The idea that it "pauses" the thread is very misleading. It may cause the thread to be put back to the end of the run queue but there is no guarantee that there are any threads waiting so it may keep running as if the yield were removed.
See my answer here for more info : unwanted output in multithreading
Let's read some javadoc, shall we?
A hint to the scheduler that the current thread is willing to yield
its current use of a processor. The scheduler is free to ignore this
hint.
[...]
It is rarely appropriate to use this method. It may be useful
for debugging or testing purposes, where it may help to reproduce bugs
due to race conditions. It may also be useful when designing
concurrency control constructs such as the ones in the
java.util.concurrent.locks package.
You cannot guarantee that another thread will obtain the processor after a yield(). It's up to the scheduler and it seems he/she doesn't want to in your case. You might consider sleep()ing instead, for testing.
I have written this code in Java but I can't understand why the output isn't what I expect.
Can anyone explain why Apples and Oranges are cluttered and they are not listed one by one?
package first_experiment;
class Orange extends Thread{
public void run (){
for ( int i=1 ; i<21 ; i++)
System.out.println( i + " - Orange");
}
}
class Apple extends Thread{
public void run(){
for (int i =1 ; i <11 ; i++)
System.out.println( i + " - Apple");
}
}
public class one{
public static void main (String args[]){
Thread O = new Orange();
Thread A = new Apple();
O.start();
A.start();
}
}
The two threads run at the same time. It's up to the JVM to decide which order they get printed out in, and you cannot rely on the order being predictable.
You are creating 2 threads that run concurrently. The order of execution is unknown (since both have the same priority). Note that if PrintStream#println() is not synchronized then the output would be something like:
12 - - A ppOranlgee
// and so on
If you want the results to appear in a particular order, use one thread in a loop.
If you use multiple threads you want them to start and run in any order as much as possible.
Your threads are so short lived, they are running to completion before the other one can start.
BTW: You PC can perform 100,000,000 operations in the time it takes you to blink, and starting a new thread takes time.
Threads run as seperate and indipendant streams of execution. You might loop through several Apples before the first Orange gets some CPU time. Also, if you are on a multiprocessor system, it is possible for both to be running concurently.
If you want them to coordinate their output, you would need to use wait() and notify().
Threads in java are processes running independently when started, one line of started thread code is executed a time with no specific order of execution between threads.
The above code will display in console count - Orange and count - Apple randomly.
The threads may start in any order but the results will be cluttered because System.out.println synchronizes the Thread access to it and as such then are executed one after another for printing.
To see truly random output, save the data in a List and then later print the contents of the List.
I was trying examples from JCIP and Below program should not work but even if I execute it say 20 times it always work which means ready and number are becoming visible even if it should in this case
public class NoVisibility {
private static boolean ready;
private static int number;
private static class ReaderThread implements Runnable {
public void run() {
while (!ready)
Thread.yield();
System.out.println(number);
}
}
public static void main(String[] args) {
System.out.println(Runtime.getRuntime().availableProcessors());
//Number of Processor is 4 so 4+1 threads
new Thread(new ReaderThread()).start();
new Thread(new ReaderThread()).start();
new Thread(new ReaderThread()).start();
new Thread(new ReaderThread()).start();
new Thread(new ReaderThread()).start();
number = 42;
ready = true;
}
}
On my machine it always prints
4 -- Number of Processors
42
42
42
42
42
According to Listing 3.1 of JCIP It should sometimes print 0 or should never terminate it also suggest that there is no gaurantee that ready and number written by main thread will be visible to reader thread
Update
I added 1000ms sleep in main thread after strating all threads still same output.I know program is broken And I expect it behave that way
This program is broken since ready and number should be declared as volatile.
Due to the fact that ready and number are primitive variables, operations on them are atomic but it is not guaranteed that they will be visible by other threads.
It seems that the scheduler runs the threads after main and that is why they see the number and ready being initialized. But that is just one scheduling.
If you add e.g. a sleep in main so as to affect the scheduler you will see different results.
So the book is correct, there is no guarantee whether the Runnables running in separate threads will ever see the variable's being updated since the variables are not declared as volatile.
Update:
The problem here is that the due to the lack of volatile the compiler is free to read the field ready just once, and reuse the cached value in each execution of the loop.
The program is inherently flawed. And with threading issues the problem usually appears when you deploy your application to the field....
From JSL:
For example, in the following (broken) code fragment, assume that
this.done is a non-volatile boolean field:
while (!this.done)
Thread.sleep(1000);
The compiler is free to read the field this.done just once, and reuse
the cached value in each execution of the loop. This would mean that
the loop would never terminate, even if an other thread changed the
value of this.done.
What is important to keep in mind is that a broken concurrent program might always work with the right combination of JVM's options, machine architecture etc. That does not make it a good program, as it will probably fail in a different context: the fact that no concurrency issue shows up does not mean there aren't any.
In other words, you can't prove that a concurrent program is correct with testing.
Back to your specific example, I see the same behaviour as what you describe. But, if I remove the Thread.yield() instruction in the while loop, 3 out of 8 threads stop and print 42, but the rest don't and the program never ends.