Java 多线程执行被阻止

Java Multithreading Execution Blocked

出于学习目的,我尝试实现队列数据结构 + Consumer/producer 线程安全的链,出于学习目的,我也没有使用 notify/wait 机制:

同步队列:

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实例变量而没有设置它,所以没有竞争条件。

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() 并发现有一个值要出列,这意味着它们都将尝试使同一个值出列。归结为这样一个事实,即您 真正 想要的是 "check and dequeue if set" 操作是有效的原子操作,但它不是您编写代码的方式。这是因为最初调用 isSet 的同一线程可能不一定是随后调用 dequeue 的同一线程。所以操作 作为一个整体 不是原子的,这意味着您必须同步整个操作。

您遇到的问题是可见性(或者更确切地说,缺乏可见性)。

如果没有任何相反的指示,JVM 将假定分配给一个线程中的变量的值不需要对其他线程可见。它可能会在以后有时可见(当这样做方便时),或者可能永远不会。 Java 内存模型定义了关于什么应该可见以及何时可见的规则,它们总结为 here(一开始可能有点枯燥和可怕,但理解它们绝对是至关重要的。)

因此,即使生产者将 set 设置为 true,消费者仍会将其视为 false。您如何发布新值?

  1. 将字段标记为 volatile。这适用于像 boolean 这样的原始值,对于引用你必须更加小心。
  2. synchronized 不仅提供互斥,而且保证其中设置的任何值对于进入使用同一对象的 synchronized 块的任何人都是可见的。 (这就是为什么如果你声明 isSet() 方法 synchronized 一切正常的原因。)
  3. 使用线程安全库 class,例如 Atomic* class 的 java.util.concurrent

在你的情况下 volatile 可能是最好的解决方案,因为你只更新一个 boolean,所以默认情况下保证更新的原子性。


As @matt forsythe pointed out, there is also a TOCTTOU 您的代码也有问题,因为您的线程可能会在 isSet()enqueue()/dequeue() 之间被另一个中断。

我假设当我们陷入线程问题时,第一步是确保两个线程都正常 运行。 (我知道他们会的,因为没有锁会造成死锁)

为此,您也可以在 enqueue 函数中添加一个 printf 语句。这将确保入队和出队线程 运行 正常。

那么第二步应该是 "set" 是共享资源,因此值切换是否足够好,以便代码可以 运行时尚

我想如果你能推理并把日志记录得足够好,你就能意识到问题所在。