I was teaching a bunch of college students about Java threads today, and gave an example in which two threads share an int counter, without giving any consideration to synchronization, and jointly count down from 100 to 0. I showed how most of the time the count goes smoothly, but once in a while you get "78...77...77...76...74...73..." or the like. So then I introduced "synchronized" to fix the problem. Except that some of my students had never seen the problem, so they couldn't tell if it was fixed. Specifically, the students running JDK 1.5 never got duplicate or skipped numbers. Why not? What has changed in the thread libraries that makes threads behave more predictably? And how can I still illustrate the need for synchronization?
(By the way, that code structure was a poor choice pedagogically: synchronizing the run() method doesn't do anything, because the two run() methods have different "this"es. I'll redesign this before the next time I present it. But that's not the question at hand....)
I believe Java 1.5 is slightly stricter about synchronization than previous versions, for example regarding volatile variables. It's possible that early implementations are even more strict, so that they don't exhibit the synchronization issues you are trying to illustrate.
There are three ways that I see in which this can exhibit synchronization problems.
Both threads enter the while loop when the counter is at 1. This results in 0 being printed.
One thread gets interrupted right after printing the counter but before decrementing it. This results in duplicated values being printed along with a value being skipped: 10 9 9 7 6 ...
One thread gets interrupted in the middle of decrementing the counter, and the other thread decrements the counter during the interruption. This causes the countdown to reset to a previous value, most likely just one step since blocking I/O tends to cause thread interruptions: 10 9 8 7 9 ...
Now that I've typed all that out, I can't think of anything in JDK 1.5 that would make this less likely other than general improvements in thread-switching performance. Perhaps threads are switched more frequently now, and as such there's much less likelihood of the second condition occurring (the most likely of the three assuming blocking I/O swaps the thread out).
Oh, maybe non-blocking I/O is used for the console and so threads tend to be swapped out in sections of the code less likely to cause issues.
Yes, step one is to remove the sleeping. You are counting on the variable being accessed at EXACTLY the same time. But you put in random sleeping which works against that. In fact random sleeping is a common technique to reduce the chance of clashing over a resource. IP uses it for broadcast messages and other reasons.
Also, try to let the computer detect the bad count. Write some code that you can use to verify the count is correct. Much easier than counting on the printout especially since counting to 100 happens so fast on computers nowadays, there is probably not resonably enough time to get a clash.