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

dar*_*osh 1 java concurrency multithreading synchronization

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

环境:

OS: Fedora Core 17 X86_64
JDK: 1.8.0_05 (64 Bit)
Run Code Online (Sandbox Code Playgroud)

测试用例规范:

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

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

A
B
C
D
A
B
C
D
A
B
C
D
...
Run Code Online (Sandbox Code Playgroud)

测试用例实现:

/*
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 Code Online (Sandbox Code Playgroud)

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

只是为了看看会发生什么,我删除了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)
Run Code Online (Sandbox Code Playgroud)

根据IllegalMonitorStateException的文档:

公共类IllegalMonitorStateException扩展RuntimeException

抛出此异常表示线程已尝试在对象的监视器上等待,或者在没有指定监视器的情况下通知在对象监视器上等待的其他线程.

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

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

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

线程以三种方式之一成为对象监视器的所有者:

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

一次只有一个线程可以拥有对象的监视器.

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

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

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

有人可以做一些澄清吗?

awk*_*ksp 6

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

抛出此异常表示线程已尝试在对象的监视器上等待,或者在没有指定监视器的情况下通知在对象监视器上等待的其他线程.

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

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

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

这似乎在你的情况下有效,也许是因为它似乎只有一个线程一次被唤醒,但如果另一个线程自身唤醒(称为虚假唤醒),你很可能会使信件乱序.我不认为这是常见的事情.

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

你想要做的是在整个方法中保持锁定,所以你保证一次只能打印一个线程.