1

问题是:有两个线程,一个是 List 的写入者,另一个是 List 的读取者。如果编写器中的循环有大量迭代,有时阅读器会卡住。在那种情况下,那个读者变成了阻塞(不是等待),这意味着它收到了通知,但作者没有释放监视器?

那么,为什么会这样?最好的办法是什么?(睡得好吗?)

import java.util.LinkedList;
import java.util.List;

public class Main {

    private List<Object> m_calls =  new LinkedList<Object>();

    public void startAll(){

        Thread reader = new Thread(new Runnable() {           
            @Override
            public void run() {
                while(true){
                    synchronized(m_calls){
                        while (m_calls.size() == 0) {
                            try {
                                System.out.println("wait");
                                m_calls.wait();
                            } catch (InterruptedException e) {                               
                                return;
                            }
                        }
                        m_calls.remove(0);
                        System.out.println("remove first");
                    }
                }
            }
        });

        Thread writer = new Thread(new Runnable() {           
            @Override
            public void run() {

                for(int i = 0; i < 15; i++){

                    // UN-comment to have more consistent behavior
                    /*try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }*/
                    synchronized(m_calls){
                        m_calls.add(new Object());
                        m_calls.notifyAll();
                        System.out.println("sent");
                    }
                }
            }
        });

        reader.start();
        writer.start();
    }

    public static void main(String[] args) {     
        new Main().startAll();       
    } 
}

运行上面的代码会给出不同的结果:

---------------------------------- 第一次尝试

等待
发送
发送
发送
发送
发送
发送
发送
发送
发送
发送
发送
发送
发送
移除
先 移除 先
移除 先 移除 先 移除 先 移除 先 移除 先 移除 先 移除 先 移除 先 移除 先 移除 先 移除先 移除 先 移除















---------------------------------- 第二次尝试

等待
发送
发送
发送
发送
发送
发送
移除 先
移除 先
移除 先
移除 先
移除 先
移除 先
等待
发送
发送
移除 先
移除 先
等待
发送
发送
发送
发送
发送
发送
移除

移除 先
移除 先
移除 先
移除 先
移除 先
移除 先
等待

------------------------------ 未注释的 sleep() - 符合我们的预期

等待
发送
删除第一个
等待
发送
删除第一个
等待
发送
删除第一个
等待
发送
删除第一个
等待
发送
删除第一个
等待
发送
删除第一个
等待
发送
删除第一个
等待
发送
删除第一个
等待
发送
删除第一个
等待
发送
删除第一个等待
发送
删除第一个
等待
发送
删除第一个
等待
发送
先删除
等待
发送
先删除
等待
发送
删除第一个
等待

编辑1:阅读器线程(其中之一)似乎不再等待,而是被阻塞了,看起来它的监视器收到了通知(在notifyAll()之后)但是编写器线程没有在其循环中释放锁,这是令人困惑的...

在此处输入图像描述

4

3 回答 3

3

您的特殊情况最好使用BlockingQueue. 阻塞队列将阻塞take线程(读取器),直到put队列中有东西(由写入器)。

这是使用阻塞队列修改后的代码:

public class Main {

    private BlockingQueue<Object> m_calls =  new LinkedBlockingQueue<Object>();

    public void startAll(){

        Thread reader = new Thread(new Runnable() {           
            @Override
            public void run() {
                while(!Thread.currentThread().isInterrupted()) {
                    try {
                        Object obj = m_calls.take();
                        System.out.println("obj taken");
                    } catch(InterruptedException ex) {
                        // Let end
                    }
                }
            }
        });

        Thread writer = new Thread(new Runnable() {           
            @Override
            public void run() {
                try {
                    for(int i = 0; i < 15; i++){
                        m_calls.put(new Object());
                        System.out.println("obj put");
                    }
                } catch (InterruptedException ex) {
                    // Let end
                }
            }
        });

        reader.start();
        writer.start();
    }

    public static void main(String[] args) {     
        new Main().startAll();       
    }
}

输出:

obj put
obj taken
obj put
obj taken
obj put
obj taken
obj put
obj taken
obj put
obj taken
obj put
obj taken
obj put
obj taken
obj put
obj taken
obj put
obj taken
obj put
obj taken
obj put
obj taken
obj put
obj taken
obj put
obj taken
obj put
obj taken
obj put
obj taken

这将比 a) 使用普通LinkedList和 b) 尝试使用您自己的等待/通知更安全。您的等待/通知也很容易受到竞争条件的影响。如果写线程notify在读者调用之前调用wait,那么读者可以无限期地等待最后一个条目。

我还可以补充一点,这个解决方案对于多个读取器写入器线程是安全的。多个线程可以同时放入和取出所有线程,它们LinkedBlockingQueue将为您处理并发。

唯一需要注意的是是否Object访问了某些共享资源,但这是与一组对象的并发访问有关的另一个问题。(按照“我可以obj1同时obj2从两个不同的线程访问吗?”)这完全是另一个问题,所以我不会在这里详细说明解决方案。

于 2012-09-18T15:06:04.200 回答
1

没有任何事情会立即发生,这毫无价值,当涉及到线程时,您无法确定独立事件何时发生。(需要同步的原因之一)

final long start = System.nanoTime();
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.printf("Took %,d ns to start this thread%n", System.nanoTime() - start);
    }
}).start();

印刷

Took 2,807,336 ns to start this thread

这听起来可能不是很长,但在 3.2 GHz 下,这几乎是 900 万个时钟周期。在那个时候,计算机可以做很多事情。在您的情况下,一个短暂的线程甚至可以在第二个线程开始之前运行完成。

在第二种情况下,您所看到的是锁定是不公平的(即公平意味着等待时间最长的人首先获得锁定) 原因是正确实现它要慢得多,例如慢 10 倍或更多。出于这个原因,锁往往会被赋予最后拥有它的线程,因为这在大多数情况下效率更高。您可以使用公平锁来获得公平锁,Lock lock = new ReentrantLock(true);但除非需要,否则通常不会使用它,因为大多数情况下它的速度较慢而收益甚微。

您可以尝试-XX:-UseBiasedLocking使锁定更加公平。


要使用 ExecutorService 做很多相同的事情,您可以像这样编写代码

ExecutorService service = Executors.newSingleThreadExecutor();
// writer
for (int i = 0; i < 15; i++) {
    service.submit(new Runnable() {
        @Override
        public void run() {
            // reader
            System.out.println("remove first");
        }
    });
    System.out.println("sent");
}
service.submit(new Runnable() {
    @Override
    public void run() {
        System.out.println("wait");
    }
});
service.shutdown();

印刷

sent
remove first
sent
sent
remove first
sent
remove first
sent
remove first
sent
remove first
sent
remove first
sent
remove first
sent
remove first
sent
remove first
sent
remove first
sent
remove first
sent
remove first
sent
remove first
sent
remove first
remove first
wait
于 2012-09-18T14:39:41.417 回答
0

在这种情况下进行同步的更好方法是在您的情况下使用 java.util.concurrent.*,也许是 CountDownLatch。

也许在寻找死锁的原因之前先试试这个。

编辑:彼得是对的。它似乎运行正常?

编辑2:好的,在附加信息之后完全不同的故事。我建议您使用超时来强制至少尝试一次阅读,即使在特定时间跨度之后还有更多要写入的内容。 wait甚至有一个超时版本... http://docs.oracle.com/javase/1.4.2/docs/api/java/lang/Object.html#wait(long )

但同样:我个人更喜欢使用并发 API。

于 2012-09-18T14:30:39.520 回答