Java:两个WAITING +一个BLOCKED线程,notify()导致一个活锁,notifyAll()没有,为什么?

时间:2014-11-17 16:17:49

标签: java multithreading concurrency notify livelock

我试图使用Java同步"原语"来实现类似于Java的有界BlockingQueue接口的类似内容。 (同步,等待(),notify())当我偶然发现一些我不理解的行为时。

我创建了一个能够存储1个元素的队列,创建两个等待从队列中获取值的线程,启动它们,然后尝试将两个值放入主线程中同步块的队列中。它大部分时间都可以工作,但有时候等待值的两个线程似乎开始相互唤醒并且不让主线程进入同步块。

这是我的(简化)代码:

import java.util.LinkedList;
import java.util.Queue;

public class LivelockDemo {
    private static final int MANY_RUNS = 10000;

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < MANY_RUNS; i++) { // to increase the probability
            final MyBoundedBlockingQueue ctr = new MyBoundedBlockingQueue(1);

            Thread t1 = createObserver(ctr, i + ":1");
            Thread t2 = createObserver(ctr, i + ":2");

            t1.start();
            t2.start();

            System.out.println(i + ":0 ready to enter synchronized block");
            synchronized (ctr) {
                System.out.println(i + ":0 entered synchronized block");
                ctr.addWhenHasSpace("hello");
                ctr.addWhenHasSpace("world");
            }

            t1.join();
            t2.join();

            System.out.println();
        }
    }

    public static class MyBoundedBlockingQueue {
        private Queue<Object> lst = new LinkedList<Object>();;

        private int limit;

        private MyBoundedBlockingQueue(int limit) {
            this.limit = limit;
        }

        public synchronized void addWhenHasSpace(Object obj) throws InterruptedException {
            boolean printed = false;
            while (lst.size() >= limit) {
                printed = __heartbeat(':', printed);
                notify();
                wait();
            }
            lst.offer(obj);
            notify();
        }

        // waits until something has been set and then returns it
        public synchronized Object getWhenNotEmpty() throws InterruptedException {
            boolean printed = false;
            while (lst.isEmpty()) {
                printed = __heartbeat('.', printed); // show progress
                notify();
                wait();
            }
            Object result = lst.poll();
            notify();
            return result;
        }

        // just to show progress of waiting threads in a reasonable manner
        private static boolean __heartbeat(char c, boolean printed) {
            long now = System.currentTimeMillis();
            if (now % 1000 == 0) {
                System.out.print(c);
                printed = true;
            } else if (printed) {
                System.out.println();
                printed = false;
            }
            return printed;
        }
    }

    private static Thread createObserver(final MyBoundedBlockingQueue ctr,
            final String name) {
        return new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println(name + ": saw " + ctr.getWhenNotEmpty());
                } catch (InterruptedException e) {
                    e.printStackTrace(System.err);
                }
            }
        }, name);
    }
}

这是我在“&#34;阻止&#34;:

时所看到的
(skipped a lot)

85:0 ready to enter synchronized block
85:0 entered synchronized block
85:2: saw hello
85:1: saw world

86:0 ready to enter synchronized block
86:0 entered synchronized block
86:2: saw hello
86:1: saw world

87:0 ready to enter synchronized block
............................................

..........................................................................

..................................................................................
(goes "forever")

但是,如果我将addWhenHasSpace和getWhenNotEmpty方法的while(...)循环中的notify()调用更改为notifyAll(),那么&#34;总是&#34;通行证。

我的问题是:为什么在这种情况下,notify()和notifyAll()方法之间的行为会有所不同,为什么notify()的行为方式也是如此?

我希望这两种方法在这种情况下的行为方式相同(两个线程WAITING,一个BLOCKED),因为:

  1. 在我看来,在这种情况下,notifyAll()只会唤醒另一个线程,与notify()相同;
  2. 看起来唤醒一个线程的方法的选择会影响被唤醒的线程(并且我猜想会变得RUNNABLE)和主线程(已经被阻止)以后的竞争对于锁定 - 不是我期望从javadoc以及在主题上搜索互联网。
  3. 或者我可能完全做错了什么?

2 个答案:

答案 0 :(得分:2)

使用内部锁定似乎存在某种公平性/闯入 - 可能是由于某些优化。我猜,本机代码会检查当前线程是否已通知监视器它将要等待并允许它获胜。

synchronized替换为ReentrantLock,它应该按预期工作。这里的不同之处在于ReentrantLock如何处理它已通知的锁的服务员。


<强>更新

有趣的发现在这里。您所看到的是main线程进入

之间的竞赛
        synchronized (ctr) {
            System.out.println(i + ":0 entered synchronized block");
            ctr.addWhenHasSpace("hello");
            ctr.addWhenHasSpace("world");
        }

而另外两个线程进入各自的synchronized区域。如果主线程在两个中的至少一个之前没有进入其同步区域,您将体验到您正在描述的这个实时锁定输出。

似乎正在发生的事情是,如果两个消费者线程首先同时点击同步块,他们将为notifywait互相打乒乓。可能是JVM在线程被阻塞时为线程提供等待监视​​器优先级的线程。

答案 1 :(得分:2)

如果不深入研究您的代码,我可以看到您使用单个条件变量来实现具有一个生产者和多个消费者的队列。这是一个麻烦的秘诀:如果只有一个条件变量,那么当消费者呼叫notify()时,无法知道它是否会唤醒生产者或唤醒另一个消费者。

该陷阱有两种方法:最简单的方法是始终使用notifyAll().

另一种方法是停止使用synchronizedwait()notify(),而是使用java.util.concurrent.locks中的工具。

单个ReentrantLock对象可以为您提供两个(或更多)条件变量。专门为生产者使用一个通知消费者,并使用另一个专门为消费者通知生产者。

注意:切换到使用ReentrantLocks时名称会发生​​变化:o.wait()变为c.await()o.notify()变为c.signal()