在线程之间共享变量时如何避免并发错误?

时间:2012-07-31 21:56:33

标签: java multithreading debugging concurrency thread-safety

我写了一个典型的生产者 - 消费者计划:一个生产者,负责填充队列;一个消费者,负责队列的出列。以下是我的实施。

棘手的部分是有另一个共享变量 vFlag 进程(str)可能会将其更改为 true ,这将由生产者线程检测到,并导致清空队列。

private class PQueue
{
    private Queue<String> queue;
    PQueue()
    {
        queue = new LinkedList<String>();
    }

    public synchronized void add(String str)
    {
        if(queue.size() > 1)
        {
        wait();
        }
        queue.add(token);
        notify();
    }

    public synchronized String poll()
    {
        if(queue.size() == 0)
        {
            wait();
        }
        String str = queue.poll();
        notify();
        return str;
    }

    public synchronized void clear()
    {
        queue.clear();
    }

}

PQueue queue = new PQueue();
private class Producer implements Runnable
{
    public void run()
    {
        while (true) {
            String str = read();
            queue.add(str);

            if(vFlag.value == false)
            {
                queue.clear();
                vFlag.value = true;
            }

            if (str.equals("end"))
                break;
        }           
        exitFlag = true;
    }
}

private class Consumer implements Runnable
{   
    public void run()
    {
        while(exitFlag == false)
        {
            String str = queue.poll();
            process(str, vFlag);
        }
    }
}

现在,还有一些错误。任何人都可以给我一些线索或建议,比如更好的并发编程理念或机制来避免并发错误?

对于上述实施,该程序有时会报告进程中的错误(str,vFlag)

Exception in thread "Thread-3" java.lang.NullPointerException

在其他情况下,程序正常运行。如果我注释掉 queue.clear(),那么效果会很好。

2 个答案:

答案 0 :(得分:2)

您需要确保vFlag.value字段为volatile,因为多线程正在更改布尔值。此外,我假设生产者一旦看到value已设置为true,就会排空队列,然后将其设置为false。这听起来像是竞争条件。您可以考虑使用AtomicBoolean,以便使用compareAndSet(...)来避免覆盖。

我不确定这是一个问题,但您应该考虑执行以下操作来删除一些设置/未设置的竞争条件。您不需要AtomicBoolean

while (!vFlag.value) {
    // set to true immediately and then clear to avoid race
    vFlag.value = true;
    queue.clear();
}

您也可以考虑使用BlockingQueue而不是同步自己的QueueBlockingQueue为您处理线程安全,并且还有take()方法为您执行wait()逻辑。如果队列中有项目,您的代码似乎想要阻止,因此您可能希望使用new LinkedBlockingQueue(1)将队列大小限制为1,这将阻止put(...)

最后,在这些情况下,始终建议使用while循环而不是if循环:

// always use while instead of if statements here
while (queue.size() == 0) {
    wait();
}

这解决了意外信号和多个消费者或生产者的问题。

答案 1 :(得分:2)

对于通用线程安全队列,您可以使用ConcurrentLinkedQueue 或者如果你想一次限制队列中的一个对象SynchronousQueue 最后,如果一次只有一个对象,但可以使用ArrayBlockingQueue预先计算下一个对象。

Java内存模型允许有效缓存值,以及编译器重新排序指令,假设只有一个线程在运行时重新排序不会改变结果。因此,生产者不能保证将vFlag视为真,因为它允许使用缓存的值。因此,消费者线程将vflag视为true,并且生产者将其视为false。将变量声明为volatile意味着JVM每次都必须查找该值,并在访问的任一侧之间生成一个内存栅栏,以防止重新排序。例如

public class vFlagClass {
  public volatile boolean value;
}

消费者可以在设置vFlag为true之后继续访问队列,消费者应该清除队列本身,或者需要等待来自生产者的清除队列的信号。即使假设值是易失性的,在激活生产者并清除队列之前,在vFlag.value设置为true之后,程序仍然可以使用队列中的所有输入。

此外,您的队列结构假设只有一个线程正在写入并且一个线程正在读取,通过让生产者清除队列,生产者有效地从队列中读取并破坏您的不变量。

特别是对于你的错误我想象发生的事情是消费者告诉生产者在队列为空时清除队列然后再次尝试队列,队列仍然是空的,所以它等待。然后生产者醒来,填充队列,唤醒消费者,生产者然后清除队列,消费者开始前进并轮询队列,但队列是空的。因此返回NULL。换句话说

Producer : queue.add(str); // queue size is now 1
Producer : goes to end of loop
Consumer : String str = queue.poll(); // queue size is now 0
Consumer : process(str, vFlag); // vFlag.value is set to false 
Consumer : String str = queue.poll(); // waits at
            if(queue.size() == 0)
            {
                wait();
            }
Producer : queue.add(str);
Producer : notify() // wakes up consumer
Producer : queue.clear();
Consumer : String str = queue.poll(); // note queue is empty, so returns NULL

在这种情况下,您需要将PQueue.poll更改为

String str = null;
while((str = queue.poll()) == null)
    wait();
return str;

但这不是一个好的解决方案,因为它仍然具有前面提到的所有其他错误。 注意:这只允许两个线程从队列中读取,它不能防止多个线程写入队列。

编辑:注意到另一个竞争条件,生产者可以将“结束”放入队列,消费者阅读并处理它,然后生产者更新exitFlag。请注意,更改exitFlag不会修复任何内容,因为它与读取陈旧数据无关。而不是使用exitFlag,你应该让消费者在读取“结束”时也会中断。

有关memory consistency in Java的更多信息,请参阅文档。