Java 在线程之间共享变量时如何避免并发错误?
我写了一个典型的生产者-消费者程序:一个生产者,负责填充队列;一个消费者,负责排队。下面是我的实现 棘手的是还有另一个共享变量vFlag。进程(str)可能会将其值更改为真,这将被生产者线程检测到,并导致队列清空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
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的更多信息,请参阅文档。