为测试构建了以下代码。在这个测试中,读者被要求解释为什么代码在启动代码后不到一秒就会进入死锁。
谁能准确描述导致此代码死锁的原因?
public class Test {
static class FailerThread implements Runnable {
final Object[] objects;
final Random random;
final int number;
public FailerThread(final Object[] objects, final int number) {
this.objects = objects;
this.random = new Random();
this.number = number;
}
@Override
public void run() {
final boolean isWriter = number % 2 == 0;
int index = random.nextInt(objects.length);
try {
while (Thread.interrupted() == false) {
synchronized (objects) {
if (isWriter) {
while (objects[index] == null) {
System.out.println(number + ": Index " + index + " is null, waiting...");
objects.wait();
}
for (int copyIndex = 0; copyIndex < objects.length; ++copyIndex) {
if (objects[copyIndex] == null) {
objects[copyIndex] = this.objects[index];
}
}
objects.notifyAll();
} else {
objects[index] = null;
}
}
++index;
if (index >= objects.length) {
index = 0;
}
}
} catch (InterruptedException e) {
}
}
}
public static void main(String[] args) throws InterruptedException {
final Object[] objects = new Object[10];
for (int i = 0; i < objects.length; ++i) {
objects[i] = new Object();
}
final int NUM_THREADS = 32;
final ExecutorService executor = Executors.newFixedThreadPool(NUM_THREADS);
for (int i = 0; i < NUM_THREADS; ++i) {
executor.execute(new FailerThread(objects, i));
}
}
}
编辑:这个测试的官方答案(类似于都铎写的,只是更详细)
上述构造死锁,因为在某些时候所有的“作家”都在等待一个空值,但由于这些作家是唯一可以释放它们的人,他们将无限期地挂起。然而,更重要的问题是:为什么?
乍一看,这些代码看起来像是那些编写者占主导地位。每个循环都会选择一个线程(写入器或 nuller)来处理数组,但是虽然 nuller 只写入单个 null,但写入器会消除数组中的所有 null。所以人们会期望死锁 - 虽然可能 - 是非常不可能的(但令人惊讶的是代码在一秒钟内死锁)。然而,仔细观察,这个假设被证明是错误的,因为我们正在处理线程。
给定足够的执行时间,在多线程应用程序中重要的是:代码的哪一部分实际上能够阻塞?让我们看一下 writer/nullers 可能出现的最坏情况:
在最坏的情况下,nuller 可以在没有任何影响的情况下执行。也就是说:它将 null 写入数组中已经为 null 的位置。
作家可以 - 在最坏的情况下 - 无限期地阻止。
此外,在同步块的开始,选择一个(或多或少)随机候选者进入。一开始,对于写入者和无效者来说,这都是 50%,但是对于每个被阻止的写入者,机会偏向无效者的方向。即使成功的写入消除了所有空值,但 nullers 的机会始终是 50% 或更多,因为写入程序的机会(由于阻塞)不断减少。因此,从线程的角度来看,nullers 实际上是主要部分,因为整个系统旨在支持它们作为同步块的候选者。
此外——这是重要的部分——线程的执行顺序是未定义的。一个天真的印象是允许哪个线程执行alternate,但事实并非如此。同步块没有偏好,哪个线程获得访问权限是未定义的(可以说:完全随机,尽管不涉及随机)。因此,在所有 16 个线程都在同步等待的情况下,20 个执行线程内完美交替的机会,正好等于连续调用 20 个写入器或 20 个 nuller 的机会。但是由于 nuller 占主导地位(20 个 writer 什么都不做),连续调用 20 个 nuller 几乎可以保证将整个数组设置为 null,这会导致任何后续 writer 无限期地阻塞。
如果您在代码中添加更多日志输出以查看实际选择了哪个线程,您很快就会看到连续调用 10 个或更多 nuller,通常在前 200 个循环内。之后系统挂起。
为什么问这个问题
我目前正在开发一个用于评估专家 Java 程序员的测试集,并且编写了所有代码,最终需要对其进行测试。好消息:它成功了。;)
现在,在您抱怨 StackOverflow 使用不当之前:请将此视为问答。对于多线程架构的实际实现,可以从这个示例中学到很多东西。由于这是一个专家级别的问题 - 正如预期的那样 - 没有多少人能够回答它甚至理解它。然而,专家级问题的好处是,您也可以从专家级答案中学到很多东西。这就是为什么我包含了完整详细的答案。
候选人的评分方式
预见到有些人会认为这个问题对于评估测试来说太难了,并且为了给你测试者的观点,这就是候选人的评分方式:
是的,这道题太难了,没有人期望在考试中找到正确的答案,重要的是他们如何解决问题。程序员每天都会遇到他/她以前从未解决过的任务并且不知道如何立即解决,因此具有良好的解决问题的能力在这个行业中很重要。没有人可以无所不知,但每个人都可以学习。
一般有4种可能的结果:
候选人不知道答案并这么说。这是一个很好的初学者水平,因为考生有能力在紧张的考试情况下承认这一点。一个好学生是一个善于倾听的人,因此可以被教导。
候选人现在确实知道答案,但要么责备“糟糕”的问题(又名投反对票),要么给出了错误的答案,然后他/她对此进行了激烈的辩护。这基本上是最坏情况的候选人:他处于初级/中级水平,但认为自己是专家,因此拒绝学习,将被困在这个水平。在一个团队中,这个候选人要么会阻碍团队的进步(如果他们认为他是“专家”),要么很快就会成为讨厌的人。
候选人提出一个(或多或少正确的)答案,并使用有条不紊的方法来找到它。这是一个很好的中级/专家级候选人。他/她已经开发出一种有条不紊的方法来处理具有挑战性的任务,并且根据答案可以预期会进一步发展。
候选人使用有条不紊的方法并提出正确的答案。这是最好的结果,但遇到的可能只有百万分之一。