Using “notify()” & “wait()” instead of “suspend()”

2019-02-15 15:29发布

问题:

I'm trying to learn how to pause and resume a thread in java. I'm using an Applet that implements Runnablehas 2 buttons "Start" and "Stop".

public void init(){
  th = new Thread(this);
  th.start();

  btn_increment = new Button("Start");
  btn_increment.addActionListener(new ActionListener(){
    public void actionPerformed(ActionEvent ev){
      th.notify();
    }
  });
  add(btn_increment);

  btn_decrement = new Button("Stop");
  btn_decrement.addActionListener(new ActionListener(){
    public void actionPerformed(ActionEvent ev){
      try{
        th.wait();
      } catch(InterruptedException e) {
        e.printStackTrace();
      }
    }
  });

  add(btn_decrement);                               
}

The run method of the thread:

public void run(){
  while(true){
    repaint();
    try{
      Thread.sleep(20);
    } catch(InterruptedException e) {
      e.printStackTrace();
    }
  }
}

Now whenever I try to to pause or resume the thread an exception is thrown:

Exception in thread "AWT-EventQueue-1" java.lang.IllegalMonitorStateException

Notes:

The previous code runs perfectly if I use the deprecated methods suspend() and resume(), but the documentation points out at using notify() and wait() instead with synchronization. I tried adding the word synchronized to the actionPerformed method, but it still throws the exception.

Can someone please explain WHY this isn't working and how to solve the synchronization problem? Few explanation points would really be of much help ;)

回答1:

You have misunderstood how wait() works. Calling wait on a Thread object does not pause that thread; it instead tells the currently running thread to wait for something else to happen. To explain why, I'll need to back up a bit and explain what synchronized actually does.

When you enter a synchronized block you obtain the monitor associated with an object. For example,

synchronized(foo) {

obtains the monitor associated with the object foo.

Once you have the monitor, no other threads can obtain it until you exit the synchronized block. This is where wait and notify come in.

wait is a method on the Object class that tells the currently running thread to temporarily release the monitor it holds. This allows other threads to synchronize on foo.

foo.wait();

This thread will not resume until someone else calls notify or notifyAll on foo (or the thread is interrupted). Once that happens, this thread will attempt to re-acquire the monitor for foo and then continue. Note that if any other threads are waiting to obtain the monitor then they might get in first - there is no guarantee of the order the JVM will hand out locks. Note that wait() will wait forever if no-one calls notify or notifyAll. It's usually best to use the other form of wait that takes a timeout. That version will wake up when someone calls notify/notifyAll or when the timeout has expired.

So, you need one thread to do the waiting and a different thread to do the notifying. Both wait and notify must hold the monitor on the object they are trying to wait on or notify; this is why you are seeing the IllegalMonitorStateException.

An example might help you understand:

class RepaintScheduler implements Runnable {
    private boolean paused = false;
    private final Object LOCK = new Object();

    public void run() {
        while (true) {
            synchronized(LOCK) {
                if (paused) {
                    try {
                        LOCK.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    repaint();
                }
            }
            try {
                Thread.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public void pause() {
        synchronized(LOCK) {
            paused = true;
            LOCK.notifyAll();
        }
    }

    public void resume() {
        synchronized(LOCK) {
            paused = false;
            LOCK.notifyAll();
        }
    }
}

Your Applet code can then do this:

public void init() {
    RepaintScheduler scheduler = new RepaintScheduler();
    // Add listeners that call scheduler.pause and scheduler.resume
    btn_increment.addActionListener(new ActionListener() {public void actionPerformed(ActionEvent e) {
        scheduler.resume();
    }});
    btn_decrement.addActionListener(new ActionListener() {public void actionPerformed(ActionEvent e) {
        scheduler.pause();
    }});
    // Now start everything up
    Thread t = new Thread(scheduler);
    t.start();
}

Note that the Applet class does not care about how the scheduler pauses/resumes and does not have any synchronized blocks.

So a possible sequence of events here is:

  • Thread A starts running the repaint scheduler.
  • Thread A goes to sleep for 20ms.
  • Thread B (the event dispatch thread) receives a button click; calls 'pause'.
  • Thread B obtains the monitor on LOCK.
  • Thread B updates the 'paused' variable and calls LOCK.notifyAll.
  • No threads are waiting on LOCK so nothing interesting happens.
  • Thread B releases the monitor on LOCK.
  • Thread A wakes up, goes through its loop again.
  • Thread A obtains the monitor on LOCK.
  • Thread A sees that it should be paused, so it calls LOCK.wait.
  • At this point Thread A suspends, waiting for someone to call notifyAll. Thread A releases the monitor on LOCK.
  • Some time later, the user clicks 'resume'.
  • Thread B calls scheduler.resume.
  • Thread B obtains the monitor on LOCK.
  • Thread B updates the 'paused' variable and calls LOCK.notifyAll.
  • Thread A sees the 'notifyAll' and wakes up. It tries to obtain the monitor on LOCK but it is held by Thread B so Thread A blocks.
  • Thread B releases the monitor on LOCK.
  • Thread A obtains the monitor and carries on.

Does that all make sense?

Having a separate LOCK variable is not required; I've done that to highlight the fact that you are not calling wait/notify on a Thread instance. Similarly, the logic inside the RepaintScheduler is not ideal but is just there to illustrate how wait/notify could be used.



回答2:

You can't just call notify and wait. You have to wait for something. And before calling notify, you have to make it so that there's nothing to wait for anymore.

If your blocks aren't already synchronized, then something is wrong in your design.

How can you call wait unless you have something to wait for? And how can you know that there is something to wait for if you haven't checked? And how can you check without synchronizing with the code that controls whether that thing has happened yet or not?

How can you call notify unless something just happened that you need to notify the thread about? And how could something have happened that another thread cares about if you don't hold the lock that would tell that thread about it?

You should use wait like this:

while (something_to_wait_for()) wait();

And that something_to_wait_for should check something that is protected by synchronization. And you can't make something_to_wait_for synchronized because then you have a race condition -- what if that something happens after something_to_wait_for returns but before you enter wait? Then you are waiting for something that already happened! So you need synchronization fundamentally. If you are just adding it at the end, your design is broken.

The solution in your case is probably to add something to wait for. Perhaps a simple boolean variable is all you need. Then your code can be while (should_wait) wait();, should_wait = true;, and should_wait = false(); notifyAll(). You'll need synchronized to protect the boolean and the wait/notify logic.



回答3:

I think you have to synchronize on the thread in order to call wait and notify. Try to use

synchronized (th) {
    th.notify();
}

and the same with wait().