Java中信号量的多线程

时间:2020-12-30 01:09:20

标签: java multithreading deadlock semaphore synchronized

所以基本上这就是我要解决的问题:

<块引用>

大卫、肖恩和弗兰克不断地播种。大卫挖洞。然后肖恩 在每个孔中放置一粒种子。弗兰克然后填补了这个漏洞。有几个 同步约束:

  1. 除非至少有一个空洞,否则肖恩不能种下种子,但肖恩可以 不在乎大卫领先肖恩多远。

  2. 弗兰克不能填补一个洞,除非至少有一个洞,肖恩 种下了一颗种子,但这个洞还没有被填满。弗兰克不在乎如何 肖恩远远领先于弗兰克。

  3. 弗兰克确实在意大卫不会提前打出超过 MAX 个洞 坦率。因此,如果有 MAX 个未填充的孔,大卫必须等待。

  4. 大卫和弗兰克只需要一把铲子来挖掘和填满 孔,分别。

为代表 David、Sean 和 Frank 的 3 个进程编写伪代码 使用信号量作为同步机制。确保初始化 信号量。

这是我用 Java 实现的解决方案,据我所知,与 standard solution 相比,它不是很优雅,也有点浪费:

import java.util.concurrent.Semaphore;

public class Holes {
    private static final int MAX = 3;
    private static final Semaphore mutexForShovel = new Semaphore(1, true);
    private static final Semaphore mutexForHoleCount = new Semaphore(1, true);
    private static int emptyHoleCount = 0, seededHoleCount = 0, finishedHoleCount = 0;

    public static void main(String[] args) {
        new Thread(() -> { // David
            try {
                while (true) {
                    if (emptyHoleCount < MAX) {
                        mutexForShovel.acquire(); // wait for shovel
                        System.out.println("David is digging a hole...");
                        Thread.sleep(200); // try annotating this
                        mutexForShovel.release(); // release shovel
                        mutexForHoleCount.acquire(); // enter critical section
                        emptyHoleCount++;
                        mutexForHoleCount.release(); // exit critical section
                        System.out.println("Empty = " + emptyHoleCount +
                                " Seeded = " + seededHoleCount +
                                " Finished = " + finishedHoleCount);
                    }
                }
            } catch (InterruptedException e) {
                System.out.println(e.toString());
            }
        }).start();

        new Thread(() -> { // Sean
            while (true) {
                try {
                    if (emptyHoleCount > 0) {
                        System.out.println("Sean is seeding a hole...");
                        Thread.sleep(200); // try annotating this
                        mutexForHoleCount.acquire(); // enter critical section
                        emptyHoleCount--;
                        seededHoleCount++;
                        mutexForHoleCount.release(); // exit critical section
                        System.out.println("Empty = " + emptyHoleCount +
                                " Seeded = " + seededHoleCount +
                                " Finished = " + finishedHoleCount);
                    }
                } catch (InterruptedException e) {
                    System.out.println(e.toString());
                }
            }
        }).start();

        new Thread(() -> { // Frank
            while (true) {
                try {
                    if (seededHoleCount > 0) {
                        mutexForShovel.acquire(); // ask for shovel
                        System.out.println("Frank is filling a hole...");
                        Thread.sleep(200); // try annotating this
                        mutexForShovel.release(); // release shovel
                        mutexForHoleCount.acquire(); // enter critical section
                        seededHoleCount--;
                        finishedHoleCount++;
                        mutexForHoleCount.release(); // exit critical section
                        System.out.println("Empty = " + emptyHoleCount +
                                " Seeded = " + seededHoleCount +
                                " Finished = " + finishedHoleCount);
                    }
                } catch (InterruptedException e) {
                    System.out.println(e.toString());
                }
            }
        }).start();
    }
}

我认为我对 acquire()release() 操作的排序方式避免了任何“保持等待”,从而避免了死锁。但是,这是我在终端中运行它得到的输出:

David is digging a hole...
Empty = 1 Seeded = 0 Finished = 0
David is digging a hole...
Empty = 2 Seeded = 0 Finished = 0
David is digging a hole...
Empty = 3 Seeded = 0 Finished = 0

我很困惑,我在调试器中一行一行地运行它,这就是我得到的:

David is digging a hole...
Sean is seeding a hole...
Frank is filling a hole...
Empty = 1 Seeded = 0 Finished = 0
Empty = 0 Seeded = 1 Finished = 0
David is digging a hole...
Empty = 1 Seeded = 0 Finished = 1
Sean is seeding a hole...
David is digging a hole...
Empty = 0 Seeded = 0 Finished = 1
Empty = 0 Seeded = 1 Finished = 1
Frank is filling a hole...
Empty = 1 Seeded = 1 Finished = 1
Empty = 1 Seeded = 0 Finished = 2
Sean is seeding a hole...
David is digging a hole...
...

现在这是合理的输出!不是吗?继续,我删除了带注释的 Thread.sleep() 行,我得到了这个:

...
Sean is seeding a hole...
Frank is filling a hole...
Empty = 2 Seeded = 0 Finished = 29748
Empty = 2 Seeded = 1 Finished = 29747
Sean is seeding a hole...
Frank is filling a hole...
Empty = 1 Seeded = 1 Finished = 29748
Empty = 1 Seeded = 0 Finished = 29749
Sean is seeding a hole...
Frank is filling a hole...
Empty = 0 Seeded = 1 Finished = 29749
Empty = 0 Seeded = 0 Finished = 29750

Process finished with exit code 130 (interrupted by signal 2: SIGINT)

还是不错的输出!

所以我想我现在有两个问题:

Q1:终端中的原始输出是否表明发生了死锁?如果是这样,我的代码的哪一部分是错误的,我该如何更改?

问题 2:为什么我以不同的方式运行基本相同的代码却得到不同的输出?

非常感谢!!!

3 个答案:

答案 0 :(得分:2)

private static volatile int emptyHoleCount    = 0,
                            seededHoleCount   = 0,
                            finishedHoleCount = 0;

现在再试一次。线程不会注意到这些变量的变化,因为它们没有被标记为 volatile

<块引用>

volatile 具有内存可见性的语义。基本上,价值 所有读者都可以看到 volatile 字段的内容(其他线程 特别)在写操作完成之后。 没有挥发性, 读者可以看到一些未更新的值。

volatile 修饰符保证 任何读取字段的线程都将看到最近写入的值

您的 while 循环可能会受到编译器优化的影响,因此不会检查变量更新。将变量标记为 volatile 可以避免这种情况(就像说,嘿,不要优化这个,它可能会改变,所以不断检查它的值)。

当您使它们休眠或调试它们时,您正在生成一个线程状态更改,这可能涉及您的线程在返回运行状态时再次检查变量的值。如果没有那个“状态改变”,你的线程会读取过时的数据。


除此之外,作为一个建议,您的线程包含一个非常危险的循环:

while(true)

如示例中所示,您可以通过调用 SIGINT 来阻止它们。我建议实施某种监视器或设置一些易失性布尔标志,以便优雅地停止它们。


涉及非易失性变量的死锁示例

public class UntilYouUpdateIt 
{
   public static boolean flag = true;

   public static void main(String[] args) throws InterruptedException 
   {
      Thread t1 = new Thread(()->
      {
        while(flag){}
        System.out.println("end");
      });
      t1.start();

      Thread.sleep(100);

      Thread t2 = new Thread(()->
      {
       flag = false;
       System.out.println("changed");
      });
      t2.start();
   }
}

上面的代码会输出:

changed

但该计划永远不会结束。由于编译器优化,线程 t1 永远不会退出其循环,假设 flag 始终为真。将布尔值设置为 volatile 可以避免这种情况,就像您示例的 int 变量一样。

答案 1 :(得分:1)

如果不满足启动条件,您的线程将进入快速流失状态。也就是说,它们变成了“while(true) if (false)”,然后循环。由于没有工作要做,它们旋转得非常快,并且可能会对其他想要使用 CPU 的东西产生负面影响。我建议如果没有工作要做(不满足启动条件),在再次检查之前让线程休眠。这样线程(无需工作)可以与其他线程很好地协作。

您应该查看计算机性能图表。如果您遇到死锁,那么您会期望 CPU 图形全部归零 - 没有 CPU 可以向前移动,因为每个都在等待另一个。我认为您实际上是在达到消耗 - CPU 可能固定在 100% 并保持在那里。无限循环线程占用了应用程序的 CPU,而从循环中解脱出来的线程永远得不到足够的空气来启动。

答案 2 :(得分:-1)

是的,在无法工作时使用 volatile 和一些放松时间, 而且,播种机肖恩不应该emptyHoleCount--; 填充弗兰克应该。

约束 #3 是挖掘者 Dean 和填充者 Frank 之间的合同,挖掘者 Dean 兑现了 if(emptyHoleCount < MAX) 但播种者 Sean 正在干扰,而不是填充者 Frank。

相关问题