0

Something changed from Java8 to Java9 behind the scenes in Thread Scheduler. I'm trying to narrow down the change in below program.

Below program spawns 3 threads which run parallely and synchronously passing the monitor lock properly, printing

Aa0Bb1Cc2Dd3.......Zz25

Current Code is already working fine in all Java versions and i'm not looking for any optimizations.

I used Object.notifyAll() before passing the lock using Object.wait() (that may not be correct all the time but in this situation it didn't make a difference in java 1.8). That's why there's two versions of this code version 1 and version 2.

version 1 runs fine in all java versions(Java8 and prior, Java9 and later). But not version 2. When you comment version 1 and un-comment version 2 for examlpe like this

//obj.wait();//version 1
obj.notifyAll();obj.wait();//version 2

It runs exactly the same in Java8 where as in Java9 and later JDKs it doesn't. It fails to grab the lock or it grabs the lock but condition has already been flipped and it's no thread's turn.

(for example in let's say numb thread finished its job and now only thread which can grab the lock and proceed is ThreadCapital but somehow isCapital has been turned false - this is just a speculation can't prove this or not sure this is even happening)

I've little experience working with threads so i'm sure i didn't exploit the lock on monitor or even if i had it should reflect same in all JDKs. unless something changed in Java9 and later versions. Did anything change internally in thread scheduler or something?

    package Multithreading_misc;

    public class App {

        public static void main(String[] args) throws InterruptedException {

            SimpleObject obj = new SimpleObject();
            ThreadAlphaCapital alpha = new ThreadAlphaCapital(obj);
            ThreadAlphaSmall small   = new ThreadAlphaSmall(obj);
            ThreadNum num            = new ThreadNum(obj);

            Thread tAlpha = new Thread(alpha);
            Thread tSmall = new Thread(small);
            Thread tNum   = new Thread(num);

            tAlpha.start();
            tSmall.start();
            tNum.start();

        }
    }

    class ThreadAlphaCapital implements Runnable{
        char c = 'A';
        SimpleObject obj;

        public ThreadAlphaCapital(SimpleObject obj){
            this.obj = obj;
        }

        @Override
        public void run() {
            try {
                synchronized (obj) {
                    while(c < 'Z')      
                        {
                            if(!obj.isCapitalsTurn || obj.isNumsTurn)
                            {   
                                obj.wait();//version 1
                                //obj.notifyAll();obj.wait();//version 2
                            }
                            else 
                            {
                                Thread.sleep(500);
                                System.out.print(c++);
                                obj.isCapitalsTurn = !obj.isCapitalsTurn;
                                obj.notifyAll();//version 1
                                //obj.notifyAll();obj.wait();//version 2
                            }   
                        }   
                    obj.notifyAll();
                }

            }
             catch (InterruptedException e1) {
                    // TODO Auto-generated catch block
                    e1.printStackTrace();
                }
        }

    }
    class ThreadAlphaSmall implements Runnable{
        char c = 'a';
        SimpleObject obj;

        public ThreadAlphaSmall(SimpleObject obj){
            this.obj = obj;
        }

        @Override
        public void run() {
            try {
                synchronized (obj) {
                    while(c < 'z')      
                        {           
                            if(obj.isCapitalsTurn || obj.isNumsTurn)
                            {
                                obj.wait();//version 1
                                //obj.notifyAll();obj.wait();//version 2
                            }
                            else 
                            {
                                    Thread.sleep(500);
                                    System.out.print(c++);
                                    obj.isCapitalsTurn = !obj.isCapitalsTurn;
                                    obj.isNumsTurn = !obj.isNumsTurn;
                                    obj.notifyAll();//version 1
                                    //obj.notifyAll();obj.wait();//version 2    
                            }   
                        }   
                    obj.notifyAll();
                }
            }
            catch (InterruptedException e1) {
                // TODO Auto-generated catch block
                e1.printStackTrace();
            }
        }
    }

    class ThreadNum implements Runnable{

        int i = 0;
        SimpleObject obj;

        public ThreadNum(SimpleObject obj){
            this.obj = obj;
        }
        @Override
        public void run() {
            try {   
                synchronized (obj) {
                    while(i < 26)       
                        {
                            if(!obj.isNumsTurn)
                            {   
                                obj.wait();//version 1
                                //obj.notifyAll();obj.wait();//version 2
                            }
                            else 
                            {
                                Thread.sleep(500);
                                System.out.print(i++);
                                obj.isNumsTurn = !obj.isNumsTurn;
                                obj.notifyAll();//version 1
                                //obj.notifyAll();obj.wait();//version 2
                            }   
                        }
                    obj.notifyAll();    
                }
            }
            catch (InterruptedException e1) {
                // TODO Auto-generated catch block
                e1.printStackTrace();
            }
        }
    }

    class SimpleObject{
        public boolean isNumsTurn = false;
        public boolean isCapitalsTurn = true;   
    }

Few Notes:

  1. This is being run from UnNamed module when run from Java9 and later versions

  2. I'm not saying this is only happening with 3 threads, just giving an example. btw it (overnotifying) runs fine for two threads for all java versions

skY
  • 111
  • 7
  • This sounds similar to [THIS](https://stackoverflow.com/questions/11401313/why-does-my-notify-not-wake-a-waiting-thread) one – Flown Mar 08 '19 at 21:37
  • No. This is different. Notify works fine. Just reiterate problem is not that it's not working problem is it's not working for java9 and later versions (only version 2, version 1 works impeccably in all versions). – skY Mar 09 '19 at 03:41

1 Answers1

3

I believe in over notifying.

It’s not clear why you believe in that or what you hope to gain from sprinkling notifyAll() all over the code, but now is the time to become a skeptic.

that may not be correct all the time but in this situation it doesn't make a difference.

Well, it does make a difference, obviously.

Yes, it seems that some aspects of the JVM’s waiting queue implementation have been changed, but that doesn’t matter, as your code with the obsolete notifyAll() invocations was broken all the time and just ran by pure luck.

The situation is actually easy to understand:

  1. Thread A changes the state such that thread B is eligible to proceed and invokes notifyAll()
  2. Thread B and C wake up due to the notifyAll() and try to reacquire the lock. Which one will win, is unspecified
  3. Thread C gets the lock, finds itself ineligible and goes to wait() again, but in your second variant it will do a spurious notifyAll() first
  4. Thread A and B wake up due to the spurious notifyAll() (B might be awake already, but that doesn’t matter) and try to reacquire the lock. Which one will win, is unspecified
  5. Thread A gets the lock, finds itself ineligible and goes to wait() again, but in your second variant it will do a spurious notifyAll() first
  6. Thread B and C wake up due to the spurious notifyAll() (B might be awake already, but that doesn’t matter) and try to reacquire the lock. Which one will win, is unspecified
  7. See 3.

As you can see, with your second variant you have a potential loop that may run forever, as long as B never gets the lock. Your variant with obsolete notifyAll() invocations relies on the wrong assumption that the right thread will eventually receive the lock if you notify more than one thread.

There’s no problem in using notifyAll() at places where notify() would be appropriate, as all well behaving threads would recheck their conditions and go to wait() again if not fulfilled, so the right thread (or one eligible thread) will eventually make progress. But invoking notifyAll() before waiting is not well-behaved and may cause threads permanently rechecking their conditions, without the eligible thread ever getting its turn.

Holger
  • 285,553
  • 42
  • 434
  • 765
  • I'm not saying version 2 is correct i just want to know what changed in jdk1.8 to 9 that lead to this, which you mention 'some aspect of JVM's waiting queue implementations have been changed'. Do you have any link explaining the same? I'd appreciate if you share one. – skY Mar 11 '19 at 17:17
  • Besides I'm doing spurious wake calls in version 1 as well. I'm calling notifyAll() before wait in both versions. Moreover I don't think version 2 runs just by luck. I tried running version 2 many a times in jdk 1.8 not a single time it gave unexpected result. – skY Mar 11 '19 at 17:20
  • Replacing notifyAll with notify doesn't make any difference in terms of outcome ( but i get what you're saying) it still gives exactly same outcome as before in 2nd version in both jdk 1.8 and jdk9. Which thread will win in getting lock is not really in our hands we can only provide shared object to check on right conditions. Regardless I'm not specifying with jdk 1.8 either which works just as fine. – skY Mar 11 '19 at 18:10
  • “I tried running version 2 many a times…”. That’s what we call “run by luck”. An obviously incorrect code happened to do the intended thing with one particular JVM implementation in one particular version in one particular environment. A small implementation detail, i.e. whether the set of blocked threads is managed by a FIFO, LIFO or hash set kind of structure can break it. So can changes in spin-wait settings. And you don’t even know whether even the JVM you’ve tested with can switch the implementation depending on environmental circumstances (options, number of CPUs or threads, etc.) – Holger Mar 12 '19 at 09:43
  • Exactly, if some small implementation detail ( I assume you mean inside JVM ) change can break it, then I'm interested in that change in implementation detail. Do you have any ref. for that, or is it something that can be changed by jdk developers without notifying? – skY Mar 12 '19 at 11:52
  • Thanks for the response this is getting clearer and clearer to me. – skY Mar 12 '19 at 12:21
  • 1
    There’s no change without an associated record, however, the purpose of the change does not necessarily have to be “change the queue”, but could be a related feature which changed it as a side effect. Even worse, as said, it could be that the alternative behavior always was present but dependent on some option or other environmental aspect and the change only affected, e.g. the default value for an option or surrounding conditions, which lead to selecting the other algorithm indirectly. This makes it really hard to find the particular change, as we don’t know what to search for. – Holger Mar 12 '19 at 12:36
  • That's disappointing. Are you sure there's no way we can fine tune one of the environmental attribute and hold on to others for while in order to narrow down to what exactly caused the discrepancy? – skY Mar 13 '19 at 03:55
  • With sufficient effort, it might be possible to identify the change, but even my curiosity has limits. For all practical purposes, it’s enough to know that `synchronized` has no fairness guaranty and never had. – Holger Mar 13 '19 at 07:53