1

您好,我在理解此处显示的代码时遇到问题,此代码显示了如何将 wait() 和 notify() 正确实现到线程中的示例。

这是代码:

class Q {

  int n;    
  boolean valueSet = false;

  synchronized int get() {
    if(!valueSet)
    try {
        wait();
    } catch(InterruptedException e) {
        System.out.println("InterruptedException caught");
    } 
    System.out.println("Got: " + n);
    valueSet = false;
    notify();
    return n;    
  }

  synchronized void put(int n) {
    if(valueSet)
    try {
        wait();
    } catch(InterruptedException e) {
        System.out.println("InterruptedException caught");
    }
    this.n = n;
    valueSet = true;
    System.out.println("Put: " + n);
    notify();
   }
 }

class Producer implements Runnable {
  Q q;
  Producer(Q q) {
    this.q = q;
    new Thread(this, "Producer").start();
  }
  public void run() {
    int i = 0;
    while(true) {
      q.put(i++);
    }
  }    
}

class Consumer implements Runnable {
  Q q;
  Consumer(Q q) {
    this.q = q;
    new Thread(this, "Consumer").start();
  }
  public void run() {
    while(true) {
      q.get();
    }
  }
}

class PCFixed {
    public static void main(String args[]) {
      Q q = new Q();
      new Producer(q);
      new Consumer(q);
      System.out.println("Press Control-C to stop.");
    }
  }

我很难理解这里布尔值的用法,如果布尔变量保留,代码将正确打印。

但是,如果我拿走布尔值,那么它只会打印“按 Control-C 停止”。这是为什么?

为什么布尔值在这里如此重要,它的用途是什么?

谢谢。

4

3 回答 3

1

布尔值的名称是valueSet。如果为真,则表示已设置了一个值。为 false 时,表示尚未设置值。将布尔值视为一个标志,当它为真时,有数据被消耗(标志向上),当它为假时,没有数据被消耗(标志向下)。

只有当标志为假时,生产者线程才会设置一个值。如果为真,则等待消费者通知。

如果标志为真,消费者线程只会读取该值。如果为假,则等待生产者通知。

您是否有权访问并使用调试器?逐步浏览这两个线程并查看它们如何相互交互可能会对您有所帮助。如果您以前没有使用过调试器,那么多线程可能不是理想的学习场景。

于 2013-02-27T00:15:53.720 回答
1

该类Q为存储在n. 该容器用于将该值从 传递ProducerConsumer。由于容器一次只能保留一个值,因此两者都Producer必须Consumer以某种方式知道容器是否已满。布尔值valueSet就是这个指标。

如果设置为true,则容器已满,因此Producer必须等到它被清空才能再次填充。同样,如果valueSet为 false,则在有要检索的内容之前,Consumer可能不会尝试检索Q实例的内容。

通过删除布尔值(及其状态的测试),您将两个Producer线程都Consumer置于等待通知状态(这可能永远不会发生,因为只有它们能够在代码中生成它),因此出现的唯一消息是来自主线程的那个。

非常重要的一点:正如Freedom_Ben在他自己的回答中所暗示的那样,这段代码之所以有效,是因为getput方法都是生成的synchronised,这意味着它们将阻止所有其他线程synchronized在执行期间通过调用来访问对象,从而使这些调用相对于彼此。valueSet这一点很重要,因为它几乎可以保证两者的读写操作都是n原子的。如果没有在这两种方法上设置该属性,则通知put可能会在Consumer已检查之后valueSet 但在它调用之前发生wait。根据通知机制 (*) 的实现,这可能导致Consumer缺少通知并进入等待状态,即使Q. 使用synchronized这些方法的属性,我们可以确保这些调用将按预期运行。


(*) waitandnotifiy代码可以通过两种方式实现:

  • 快速的方法是notify简单地检查一个线程是否正在运行,如果是wait,则将其唤醒,否则什么也不做。这是导致没有正确synchronized方法调用的竞争条件的场景。

  • 更正确的方法是使用在 处初始化的专用信号量,并将和分别0别名为信号量上的(aka ) 和(aka ) 操作。notifiywaitincrementreleasedecrementacquire

于 2013-02-27T00:18:28.920 回答
1

如果尚未处理最后一个值,则似乎正在使用布尔值来避免等待。例如,在 put 方法中,如果 valueSet 为真,我们将跳过 wait(),因为这意味着自上次更新 this.n 的值以来 get() 还没有运行。如果我们每次都等待()而不考虑布尔值,那么死锁的可能性非常大,两个线程都在等待并且没有线程会通知。

这就是我不喜欢将 synchronized 关键字应用于方法的原因。将哪个对象用作互斥锁可能会令人困惑。我更喜欢这种风格,因为它更清楚正在等待哪个资源。我还发现这种风格通过在不需要同步的互斥锁下工作来阻止懒惰。不过这都是个人喜好:

void get(int n){
    synchronized(this){
        // do the work
    }
}

void put(int n){
    synchronized(this){
        // do the work
    }
}
于 2013-02-27T00:19:47.757 回答