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

Java 在线程之间共享变量时如何避免并发错误?,java,multithreading,debugging,concurrency,thread-safety,Java,Multithreading,Debugging,Concurrency,Thread Safety,我写了一个典型的生产者-消费者程序:一个生产者,负责填充队列;一个消费者,负责排队。下面是我的实现 棘手的是还有另一个共享变量vFlag。进程(str)可能会将其值更改为真,这将被生产者线程检测到,并导致队列清空 private class PQueue { private Queue<String> queue; PQueue() { queue = new LinkedList<String>(); } pub

我写了一个典型的生产者-消费者程序:一个生产者,负责填充队列;一个消费者,负责排队。下面是我的实现

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

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);
        }
    }
}

在其他情况下,程序正常运行。如果我注释掉队列.clear(),它工作得很好。

您需要确保
vFlag.value
字段是
volatile
,因为布尔值正在被多个线程更改。另外,我假设生产者一旦看到
被设置为
true
,就会清空队列,然后将其设置为
false
。这听起来像是比赛条件的结果。您可以考虑使用<代码> AtomicBoolean < /代码>,这样您就可以使用<代码> CabaseDeSET(…)<代码>来避免重写。

我不确定这是一个问题,但是你应该考虑做一些类似的事情来消除一些不确定的比赛条件。您不需要
AtomicBoolean
来实现此目的:

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

您也可以考虑使用<代码>阻塞队列>代码>,而不是同步自己的<代码>队列< /代码>。code>BlockingQueues为您处理线程安全,并且还有一个

take()
方法为您执行
wait()
逻辑。如果队列中有项目,代码似乎要阻止,因此您可能需要使用
新建LinkedBlockingQueue(1)
,它将队列大小限制为1,从而阻止
放置(…)

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

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

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

您需要确保
vFlag.value
字段是
volatile
,因为布尔值正被多个线程更改。另外,我假设生产者一旦看到
被设置为
true
,就会清空队列,然后将其设置为
false
。这听起来像是比赛条件的结果。您可以考虑使用<代码> AtomicBoolean < /代码>,这样您就可以使用<代码> CabaseDeSET(…)<代码>来避免重写。

我不确定这是一个问题,但是你应该考虑做一些类似的事情来消除一些不确定的比赛条件。您不需要
AtomicBoolean
来实现此目的:

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

您也可以考虑使用<代码>阻塞队列>代码>,而不是同步自己的<代码>队列< /代码>。code>BlockingQueues为您处理线程安全,并且还有一个

take()
方法为您执行
wait()
逻辑。如果队列中有项目,代码似乎要阻止,因此您可能需要使用
新建LinkedBlockingQueue(1)
,它将队列大小限制为1,从而阻止
放置(…)

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

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

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

对于通用线程安全队列,您可以使用 或者,如果您希望一次限制队列中的一个对象 最后,如果一次只计算一个对象,但可以对下一个对象进行预计算

Java内存模型允许有效地缓存值,并允许编译器对指令进行重新排序,前提是如果只有一个线程在运行,则重新排序不会改变结果。因此,不能保证生产者将vFlag视为true,因为它被允许使用缓存的值。因此,使用者线程将vflag视为true,而生产者将其视为false。将一个变量声明为volatile意味着JVM每次都必须查找该值,并在访问的任一侧产生一个内存界限,以防止重新排序。比如说

public class vFlagClass {
  public volatile boolean value;
}
消费者可以在将vFlag设置为true后继续访问队列,或者消费者应该清除队列本身,或者消费者需要等待生产者发出的清除队列的信号。即使假设该值是可变的,在vFlag.value设置为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);
        }
    }
}
此外,队列结构假定只有一个线程在写,一个线程在读,通过让生产者清除队列,生产者有效地从队列中读取并破坏了不变量

对于您的错误,具体来说,我设想正在发生的情况是,当队列为空时,消费者告诉生产者清除队列,然后再次尝试队列,队列仍然为空,因此等待。然后生产者唤醒,填充队列,这将唤醒消费者,生产者随后清除队列,消费者开始运行并轮询队列,但队列为空。因此返回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;
但这不是一个好的解决方案,因为它仍然存在前面提到的所有其他bug。 注意:这只允许两个线程从队列中读取,不能防止多个线程写入队列

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);
        }
    }
}
编辑:注意到另一个竞争条件,生产者可能将“end”放入队列,消费者读取并处理它,然后生产者更新exitFlag。请注意,更改exitFlag不会修复任何问题,因为它与读取过时数据无关。您不应该使用exitFlag,而是应该让使用者在读取“end”时也中断

有关ge的更多信息,请参阅文档。