所以基本上这就是我想要解决的问题:
大卫、肖恩和弗兰克不断播种。大卫挖洞。那么肖恩 在每个洞里放一颗种子。弗兰克随后填补了这个漏洞。有几个 同步约束:
除非存在至少一个空洞,否则肖恩无法种植种子,但肖恩可以 不在乎大卫比肖恩领先多远。
弗兰克无法填补一个洞,除非至少存在一个肖恩有的洞 种下了种子,但坑还没有填满。弗兰克不在乎如何 肖恩远远领先于弗兰克。
弗兰克确实关心大卫没有提前获得超过 MAX 个洞 坦率。因此,如果有 MAX 个未填满的洞,David 就必须等待。
大卫和弗兰克只需要一把铲子来挖掘和填土 分别是孔。
为代表 David、Sean 和 Frank 的 3 个进程编写伪代码 使用信号量作为同步机制。确保初始化 信号量。
这是我用 Java 实现的解决方案,我知道,与标准解决方案相比,它不是很优雅,而且有点浪费:
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:终端中的原始输出是否表明发生了死锁?如果是这样,我的代码的哪一部分是错误的,我该如何更改它?
Q2:为什么我以不同的方式运行基本相同的代码时得到不同的输出?
非常感谢!
private static volatile int emptyHoleCount = 0,
seededHoleCount = 0,
finishedHoleCount = 0;
现在再试一次。线程不会注意到这些变量的变化,因为它们没有标记为
volatile
。
具有内存可见性的语义。基本上,价值 易失性字段的属性对所有读者都可见(其他线程 特别是)在写入操作完成后。 无挥发性, 读者可以看到一些未更新的值。volatile
易失性修饰符保证that任何读取字段的线程都将看到最近写入的值。
您的 while 循环可能会受到编译器优化的影响,因此不会检查变量更新。将变量标记为
volatile
可以避免这种情况(就像说,嘿,不要优化它,它可能会改变,所以不断检查它的值)。
当您睡眠它们或调试它们时,您正在生成线程状态更改,这可能涉及您的线程在返回到running状态时再次检查变量的值。如果没有“状态更改”,您的线程将读取陈旧的数据。
除此之外,作为建议,您的线程包含一个非常危险的循环:
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
始终为 true。将布尔值设置为 volatile
可以避免这种情况,就像示例的 int 变量一样。
如果不满足启动条件,您的线程就会快速流失。 也就是说,它们变成“while(true) if (false)”,然后循环。 由于没有工作可做,它们旋转得非常快,并且可能会对其他想要使用 CPU 的事物产生负面影响。 我建议如果没有工作要做(不满足启动条件),请在再次检查之前让线程休眠。 这样,线程(无需做任何工作)可以与其他线程很好地配合。
您应该观看计算机性能图表。 如果遇到死锁,那么您会期望 CPU 图表全部归零 - 没有 CPU 可以继续前进,因为每个 CPU 都在等待另一个。 我认为你实际上正在达到消耗 - CPU 可能固定在 100% 并保持在那里。 无限循环线程会占用应用程序的 CPU,而要脱离循环的线程永远不会获得足够的空气来启动。
是的,在无法工作时利用不稳定和一些放松的时间, 而且,播种者 Sean 不应该
emptyHoleCount--;
填充者 Frank 应该。
约束 #3 是挖掘者 Dean 和填充者 Frank 之间的合同,挖掘者 Dean 正在履行该合同
if(emptyHoleCount < MAX)
,但播种者 Sean 正在干扰,而不是填充者 Frank。