Java多线程执行被阻止

时间:2015-07-29 18:57:30

标签: java multithreading deadlock race-condition

出于学习目的,我试图实现一个线程安全的队列数据结构+消费者/生产者链,出于学习目的,我还没有使用通知/等待机制:

SyncQueue:

package syncpc;

/**
 * Created by Administrator on 01/07/2009.
 */
public class SyncQueue {

   private int val = 0;
   private boolean set = false;


   boolean isSet() {
      return set;
   }

   synchronized  public void enqueue(int val) {
      this.val = val;
      set = true;
   }

   synchronized public int dequeue()  {
      set = false;
      return val;
   }
}

消费者:

package syncpc;

/**
 * Created by Administrator on 01/07/2009.
 */
public class Consumer implements Runnable {
    SyncQueue queue;

    public Consumer(SyncQueue queue, String name) {
        this.queue = queue;

        new Thread(this, name).start();
    }


    public void run() {

        while(true) {
            if(queue.isSet()) {
                System.out.println(queue.dequeue());
            }

        }
    }
}

制片人:

package syncpc;

import java.util.Random;

/**
 * Created by Administrator on 01/07/2009.
 */
public class Producer implements Runnable {
    SyncQueue queue;

    public Producer(SyncQueue queue, String name) {

        this.queue = queue;
        new Thread(this, name).start();
    }

    public void run() {
        Random r = new Random();

        while(true) {
            if(!queue.isSet()) {
                    queue.enqueue(r.nextInt() % 100);
            }
        }
    }
}

主要:

import syncpcwn.*;

/**
 * Created by Administrator on 27/07/2015.
 */
public class Program {

    public static void main(String[] args) {
        SyncQueue queue  = new SyncQueue();

        new Producer(queue, "PROCUDER");
        new Consumer(queue, "CONSUMER");
    }


}

这里的问题是,如果 isSet 方法未同步,我得到了这样的输出:

97,
55

并且程序只是继续运行而不输出任何值。如果 isSet 方法已同步,则程序正常工作。

我不明白为什么,没有死锁, isSet 方法只是查询 set 实例变量而不设置它,所以没有竞争条件。

3 个答案:

答案 0 :(得分:4)

set需要volatile

private boolean volatile set = false;

这可确保所有读者在写入完成时看到更新值。否则他们最终会看到缓存的值。这在this关于并发的文章中有更详细的讨论,并且还提供了使用volatile的不同模式的示例。

现在您的代码与synchronized一起使用的原因最好用一个例子来解释。 synchronized方法可以写成如下(即,它们等同于以下表示):

public class SyncQueue {

   private int val = 0;
   private boolean set = false;


   boolean isSet() {
      synchronized(this) {
          return set;
      }
   }

   public void enqueue(int val) {
      synchronized(this) {
          this.val = val;
          set = true;
      }
   }

   public int dequeue()  {
      synchronized(this) {
          set = false;
          return val;
      }
   }
}

这里,实例是本身用作锁。这意味着只有线程可以保存该锁。这意味着任何线程总是获取更新的值,因为只有一个线程可以写入值,并且想要读取set的线程赢了能够执行isSet 直到其他线程释放锁定this,此时set的值将会更新。

如果你想要正确理解Java中的并发性,你应该真正阅读Java: Concurrency In Practice(我认为在某个地方也有一个免费的PDF版本)。我还在阅读这本书,因为还有许多我不理解或错误的事情。

matt forsythe所述,当您有多个消费者时,您会遇到问题。这是因为他们都可以检查isSet()并发现有一个值出队,这意味着他们都会尝试将相同的值出列。它归结为这样一个事实,即真正想要的是“检查并且如果设置已经出列”操作是有效的原子,但它不是你编码它的方式。这是因为最初调用isSet的同一个线程可能不一定是调用dequeue的相同线程。因此整个操作不是原子操作,这意味着您必须同步整个操作。

答案 1 :(得分:3)

你遇到的问题是可见性(或者说缺乏可见性)。

没有任何相反的指令,JVM将假定分配给一个线程中的变量的值不需要对其他线程可见。它有时可以在以后(当它方便的时候)可见,或者可能永远不可见。管理什么应该可见的规则以及何时由Java内存模型定义,并且它们总结为here(起初他们可能有点干和可怕,但理解它们绝对是至关重要的。)

因此,即使生产者将set设置为true,消费者也会继续将其视为错误。你怎么能发布新的价值?

  1. 将字段标记为volatile。这适用于像boolean这样的原始值,引用时你必须要小心一些。
  2. synchronized不仅提供互斥,还保证任何进入使用同一对象的synchronized块的人都可以看到其中设置的任何值。 (这就是为什么一切都有效,如果你声明isSet()方法synchronized。)
  3. 使用线程安全的库类,如Atomic*
  4. java.util.concurrent

    在您的情况下,volatile可能是最佳解决方案,因为您只更新boolean,因此默认情况下保证更新的原子性。

    正如@matt forsythe所指出的那样,您的代码也存在TOCTTOU问题,因为您的帖子可能被isSet()enqueue()/dequeue()之间的其他人中断。

答案 2 :(得分:0)

我认为当我们陷入线程问题时,第一步是确保两个线程都运行良好。 (我知道他们会因为没有锁来创建死锁)

为此你也可以在enqueue函数中添加一个printf语句。这将确保排队和出队线程运行良好。

然后第二步应该是“set”是共享资源,因此值足够切换以便代码可以以所需的方式运行。

我认为如果你可以推理并充分记录日志,你就可以意识到问题所在。