【问题标题】:Java Multithreading Execution BlockedJava 多线程执行受阻
【发布时间】:2015-07-29 18:57:29
【问题描述】:

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

同步队列:

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

【问题讨论】:

  • volatile 用于set 字段以确保可见性。否则允许 CPU 将其存储在缓存/寄存器中,而不是作为优化对其进行更新。
  • 工作了 thx :),另一个问题:为什么使用 synchronized 关键字,程序运行正确?同步是否强制从主内存读取变量,而不是缓存/寄存器?
  • 因为synchronized 具有相同的可见性结果,这在硬件中是存储内存屏障。我建议阅读Java Concurrency in Practice 进行温和的介绍。您将了解happens-beforeroach motel model,而不会迷失在硬件细节(屏障、MESI 协议等)中。
  • @karim:您需要的是一个内存屏障,以确保从set 字段的读取是新鲜的(而不是从缓存中读取的)。 volatile 关键字会为每个读写操作插入一个内存屏障。 synchronized 关键字导致获取监视器锁然后释放,获取监视器也会插入内存屏障。更迂腐的是,在enqueuedequeue 中对set 的写入与isSet 中的读取之间需要happens-before 关系;您可以阅读 Java 语言规范以找到可以建立 happens-before 关系的所有方法。
  • 此示例将通过添加 volatilesynchronized 来工作,正如其他人所提到的,但通常,您希望使用同步块包围整个操作。否则,例如,如果您有 2 个消费者,则两者都可以同时检查 isSet(),看看那里有一个值,然后都尝试将相同的值出列。这发生在 isSet() 和 dequeue() 之间发生线程切换的情况下,并且同步这两种方法对您没有帮助。

标签: java multithreading deadlock race-condition


【解决方案1】:

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 的同一线程。所以操作作为一个整体不是原子的,这意味着你必须同步整个操作。

【讨论】:

  • 但是,为什么当我使用同步关键字时,程序正常工作,同步关键字强制变量从主内存而不是缓存读取/写入?
  • @karim 我已经解释了为什么synchronized 有效。
  • @karim 在 NUMA 架构中,每个内核都有一个存储缓冲区缓存。因此,当您进行写入时,它们会先存储缓冲区缓存,然后再将它们刷新到主内存。这在非线程世界中非常好。但是,当您在代码周围使用锁时,您就是在告诉硬件不要对在关键部分执行任何写操作的代码段执行此操作。
【解决方案2】:

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

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

因此,即使生产者将set 设置为true,消费者仍将继续将其视为错误。如何发布新值?

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

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


正如@matt forsythe 指出的那样,您的代码也存在TOCTTOU 问题,因为您的线程可能会在isSet()enqueue()/dequeue() 之间被另一个线程中断。

【讨论】:

    【解决方案3】:

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

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

    那么第二步应该是 "set" 是共享资源,因此值切换得足够好,以便代码可以以所需的方式运行。

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

    【讨论】:

    • 我这样做是因为卡里姆指定它是为了学习目的:)。
    猜你喜欢
    • 2021-05-05
    • 1970-01-01
    • 1970-01-01
    • 2019-07-14
    • 2017-09-28
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多