为什么在同步块之外的notifyAll()调用时引发IllegalMonitorStateException?

时间:2014-05-13 11:13:18

标签: java multithreading concurrency synchronization

目前,我正在阅读在线Java Guarded Blocks教程的Concurrency章节。作为练习,我创建了一个类,以便在实践中正确使用wait()和notifyAll()方法。然而,我自己的代码中有一些我无法理解的内容,如果你能帮助我,我会很感激。

环境:

OS: Fedora Core 17 X86_64
JDK: 1.8.0_05 (64 Bit)

测试用例规范:

  
      
  • 定义一个创建并启动4个线程的类,
  •   
  • 每个线程的run()方法实际上是一个无限循环,当用户执行CTRL + C时会停止,
  •   
  • 这些主题中的每一个都必须在{A,B,C,D},
  • 中打印一个字母   
  • 无论四个创建的线程中的哪一个是当前运行的线程,都必须遵守字母的字母顺序   与上一封印刷的信件相比。
  •   
  • 首先打印字母' A'
  •   

预期的输出在终端上是这样的:

A
B
C
D
A
B
C
D
A
B
C
D
...

测试用例实施:

/*
My solution is based on a shared lock among threads.
This object has one attribute: a letter, indicating 
the letter that must be printed on the user terminal.
*/
class SharedLock
{
    private char letter;

    public SharedLock(char letter)
    {
        this.letter = letter;
    }

    /*
        Every thread which is owner of the shared lock's
        monitor call this method to retrieve the letter 
        that must be printed according to the alphabetic order.
    */
    public synchronized char getLetter()
    {
        return this.letter;
    }

    /*
        Every thread which is the owner of the shared lock's 
        monitor and besides has just printed its letter, before 
        releasing the ownership of the shared lock's monitor,
        calls this method in order to set the next 
        letter (according to the alphabetic order) to 
        be printed by the next owner of the shared 
        lock's monitor
    */
    public synchronized void setLetter(char letter)
    {
        this.letter = letter;
    }
}


/*
As said earlier each thread has a letter attribute.
So if I create 4 threads, there will be one thread 
for each letter, one which prints only 'A', another 
which prints only 'B', and so on.

Besides each thread's constructor takes as second 
parameter: the shared lock object (described above).

If the letter attribute of a thread which is the owner 
of the shared lock's monitor, is the same as 
the shared lock's letter attribute, then the thread can
print its letter because it respects the alphabetic order
otherwise it has to wait.
*/
class LetterPrinter implements Runnable
{
    private char letter;
    private SharedLock lock;

    public LetterPrinter(char letter, SharedLock lock)
    {
        this.letter = letter;
        this.lock = lock;
    }

    public void run()
    {
        while(true)
        {
            // Here the current thread tries to become the owner of
            // the shared lock's monitor
            synchronized(this.lock)
            {
                /*
                    Test whether the letter attribute of this 
                    thread must be printed. This will happen
                    only if the letter of the shared lock and
                    the thread's letter attribute are the same.
                */
                while(this.lock.getLetter() != this.letter)
                {
                    try
                    {
                        // The letters are different so in order to respect 
                        // the alphabetic order this thread has to wait
                        this.lock.wait();
                    }
                    catch(InterruptedException e)
                    {
                        e.printStackTrace();
                    }
                }
            }

            // printing the letter
            System.out.format("%s: %s%n", 
                Thread.currentThread().getName(), this.letter);

            // preparing for the next letter print according to the 
            // alphabetic order
            switch (this.letter)
            {
                case 'A': this.lock.setLetter('B'); break;
                case 'B': this.lock.setLetter('C'); break;
                case 'C': this.lock.setLetter('D'); break;
                case 'D': this.lock.setLetter('A'); break;
            }

            // And finally releasing the ownership of 
            // the shared lock's monitor
            synchronized(this.lock)
            {
                this.lock.notifyAll();
            }
        }
    }
}

public class MyTestClass
{
    public static void main(String[] args) 
    {
        // creating the shared lock object which is initialized
        // by the letter 'A'. This was the problem specification 
        // we wish to start by 'A'
        SharedLock lock = new SharedLock('A');

        // Creates the four threads with their distinct letter and 
        // their shared lock
        Thread thread1 = new Thread(new LetterPrinter('A', lock));
        Thread thread2 = new Thread(new LetterPrinter('B', lock));
        Thread thread3 = new Thread(new LetterPrinter('C', lock));
        Thread thread4 = new Thread(new LetterPrinter('D', lock));

        // And starting all of the four created threads above.
        thread1.start();
        thread2.start();
        thread3.start();
        thread4.start();
    }
}

这个程序实际上产生了所需的输出,在我看来正确地完成了工作(如果我错了,请纠正我)。然而,如果你看一下上面的run()方法,你会发现最后notify()调用也被置于同步块中。

只是为了看看会发生什么,我删除了synchronized块,我只是单独写了notify()来释放锁的监视器的所有权而且我得到了

Exception in thread "Thread-0" java.lang.IllegalMonitorStateException
        at java.lang.Object.notifyAll(Native Method)
        at LetterPrinter.run(MyTestClass.java:105)
        at java.lang.Thread.run(Thread.java:745)

根据IllegalMonitorStateException的文件:

  

公共类IllegalMonitorStateException扩展RuntimeException

     

抛出以指示线程已尝试在对象上等待   监视或通知在对象监视器上等待的其他线程   没有拥有指定的监视器。

这正是我的问题。为什么?

为什么通知呼叫,当当前所有者释放共享锁的所有权时,还必须放在同步块中?

根据notify()notifyAll()的文档:

  

线程成为对象监视器的所有者之一   方法:

     
      
  • 执行该对象的同步实例方法。
  •   
  • 通过执行在对象上同步的同步语句的主体。
  •   
  • 对于Class类型的对象,通过执行该类的同步静态方法。
  •   
     

一次只能有一个线程拥有对象的监视器。

第二个就是锁定的同步语句就是我所做的。因此,每个不是好的线程(根据字母顺序)等待。因此,当锁上的notify()被执行时,这只能由一个作为其监视器所有者的线程运行,并且没有其他线程可以尝试运行它,因为所有其他线程都在等待。

所以我不明白为什么在同步块之外的run()方法结束时调用notify()调用会引发IllegalMonitorStateException异常?

我是并发初学者。显然,似乎有一些关于语句执行和操作系统调度程序的事情,我误解了。

有人可以做一些澄清吗?

2 个答案:

答案 0 :(得分:6)

答案在你引用的一个javadoc中:

  

抛出以指示线程已尝试在对象的监视器上等待或通知在对象的监视器上等待的其他线程而不拥有指定的监视器。

您必须在等待它之前在监视器上进行同步或通知等待它的线程,并且等待/通知必须在内部同步块中完成。退出同步块后,您将不再拥有该监视器。

至于在等待/通知之前需要拥有监视器的原因,这是为了防止竞争条件,因为监视器通常用于线程之间的通信。确保一次只有一个线程可以访问监视器,确保所有其他线程都能看到“更改”。

另外,轻微的狡辩:在您的测试用例中,您在打印前释放锁定,并在打印后重新获得锁定。

这似乎在你的情况下有效,也许是因为它似乎只有一个线程一次被唤醒,但如果另一个线程自身唤醒(称为spurious wakeup),你很可能会得到字母不按顺序。我不认为这是常见的事情。

另一种可能出错的方法是,如果只是一个线程启动,越过锁定,在打印前停止,另一个线程进入,打印等等。

您要做的是在整个方法中保持锁定,因此您可以确保一次只打印一个线程。

答案 1 :(得分:3)

不是一个完整的答案,只是添加到user3580294已经说过:

同步不仅可以阻止两个线程同时进入同一个关键部分。它还保证了多处理器计算机上每个CPU内存缓存的同步。 java语言规范提供了这种保证;如果线程A更新一个字段(即实例变量或类变量)然后释放一个锁,然后线程B获得相同的锁,那么线程B将保证看到线程A写入该字段的新值。如果没有同步,则无法保证一个线程是否或何时会在其他线程更新的字段中看到新值。

假设线程A要通知一个对象,那么因为线程A改变了线程B正在等待的东西。但是如果线程A没有解锁并且线程B没有锁定相同的锁,那么当线程B唤醒时,它不一定会看到线​​程A发生了什么变化。

语言和库旨在让你犯错误。