【问题标题】:Why the IllegalMonitorStateException is raised upon notifyAll() call outside a synchronized block?为什么在同步块外调用 notifyAll() 时会引发 IllegalMonitorStateException?
【发布时间】:2014-07-01 00:48:09
【问题描述】:

目前我正在阅读在线Java Concurrency 教程的Guarded Blocks 章节。作为一个练习,我创建了一个类来在实践中查看 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() 调用也已放置在同步块中。

为了看看会发生什么,我消除了同步块,我只写了 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() 被执行时,它只能由作为其监视器所有者的线程运行,并且没有其他线程可以尝试运行它,因为所有其他线程都在等待。

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

我是并发方面的初学者。显然,我误解了语句的执行和操作系统调度程序。

有人可以澄清一下吗?

【问题讨论】:

  • “拥有监视器”意味着在适当的synchronizedblock 内。观点。其他线程做什么都没关系。
  • @Holger 非常感谢您的评论。

标签: java multithreading concurrency synchronization


【解决方案1】:

答案在您引用的其中一个 javadocs 中:

抛出表明一个线程试图在一个对象的监视器上等待或通知其他线程在一个对象的监视器上等待而不拥有指定的监视器。

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

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

另外,小问题:在您的测试用例中,您在打印之前释放锁,并在打印后重新获得它。

这似乎对您的情况有效,也许是因为似乎一次只唤醒了一个线程,但如果另一个线程自行唤醒(称为spurious wakeup),您很可能会从中取出信件命令。不过,我不认为这是一件常见的事情。

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

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

【讨论】:

  • 很好的解释!其实我没有注意离开同步块意味着释放监视器的所有权。因此,据我了解,我的错误是,在同步块(while 循环块)之后,监视器的所有权被释放,但在同步之后仅几行,尝试在同一个共享上调用 notify()锁定其所有权已被释放,因此 IllegalMonitorStateException。感谢您的帮助。
  • 是的!差不多就是这样。很高兴我能帮上忙!
  • 虽然通常需要在 notifyAll 之前拥有监视器以避免竞争条件,但至少在一种情况下该要求是不必要的问题:如果资源受到保护如果监视器有一个shutdown 标志,没有代码会清除它,那么设置该标志并随时向监视器发送notifyAll 应该是安全的,而不必等待监视器锁定可用。 notifyAll 可能在锁定可用之前没有任何效果,想要发出关闭的代码可能会通过转移到其他事情上而受益。
【解决方案2】:

不是一个完整的答案,只是补充一下 user3580294 已经说过的内容:

同步不仅仅是防止两个线程同时进入同一个临界区。它还保证了多处理器机器上每个 cpu 内存缓存的同步。 Java 语言规范做出了这样的保证;如果线程 A 更新了一个字段(即实例变量或类变量)然后释放了一个锁,然后线程 B 获得了相同的锁,那么线程 B 将保证看到线程 A 写入该字段的新值.如果没有同步,则无法保证一个线程是否或何时会在其他线程更新的字段中看到新值。

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

语言和库旨在防止您犯这种错误。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-05-11
    • 2013-08-15
    • 2016-02-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多