Java线程活锁问题

3
我有一个关于Java线程活锁的有趣问题。它是这样的。
有四个全局锁 - L1、L2、L3、L4
有四个线程 - T1、T2、T3、T4
T1需要锁L1、L2和L3,T2需要锁L2,T3需要锁L3和L4,T4需要锁L1和L2。
因此,问题的模式是 - 任何一个线程都可以以任意顺序运行并获取锁。如果任何一个线程检测到它需要的锁不可用,它会释放它之前获取的所有其他锁,并在重新尝试之前等待一段固定时间。这个循环重复,导致了一个活锁状态。
因此,为了解决这个问题,我有两个解决方案:
1)让每个线程在重新尝试之前等待一段随机的时间。
2)改变锁的获取顺序,例如按照L1、L2、L3、L4的顺序获取锁,这样就消除了死锁。
OR,

2) 让每个线程按特定顺序获取所有锁(即使线程不需要所有锁)

我并不认为这是我唯一的两个选择。请给予建议。


确实。(1)存在可避免的延迟和(2)具有极高的死锁风险,正如Zim-Zam所强调的那样,除非未能获取其锁的线程释放已经获取的锁并稍后重试。 - Martin James
4个回答

1
当线程需要并释放其锁集时,将所有线程输入单个互斥状态机中。线程应暴露方法以返回它们需要继续的锁集,并发出/等待私有信号量信号。SM应包含每个锁的bool和一个“等待”队列/数组/向量/列表/任何容器以存储等待线程。
如果线程进入SM互斥体以获取锁并可以立即获取其锁集,则可以重置其布尔集、退出互斥体并继续运行。
如果线程进入SM互斥体并且无法立即获取其锁集,则应将自己添加到“等待”中,退出互斥体并等待其私有信号量。
如果线程进入SM互斥体以释放其锁,则设置锁布尔值以“返回”其锁并迭代“等待”,以尝试找到现在可以使用可用锁集运行的线程。如果找到一个线程,则适当地重置布尔值,从“等待”中删除找到的线程并发出“已找到”线程信号。然后退出互斥体。
你可以随意调整用于将可用的锁布尔值与等待线程匹配的算法。也许你应该释放需要最大匹配集的线程,或者你想要“旋转”“等待”容器元素以减少饥饿。由你决定。
这样的解决方案不需要轮询(具有性能损耗的CPU使用和延迟),也不需要连续获取/释放多个锁。
使用面向对象的设计更容易开发这样的方案。信号/等待信号量并返回所需锁集的方法/成员函数通常可以塞到线程类继承链的某个地方。

0

除非有很好的理由(性能方面)不这样做,否则我会将所有锁定统一到一个锁对象中。 在我看来,这与您提出的解决方案2类似,只是更简单。

顺便说一下,这个解决方案不仅更简单,而且更少出错, 性能可能比您提出的解决方案1更好。


0

就我个人而言,我从未听说过选项1,但我绝不是多线程方面的专家。经过思考,它似乎可以正常工作。

然而,处理线程和资源锁定的标准方式与选项2有些相关。为了防止死锁,资源需要始终按相同顺序获取。例如,如果您总是以相同的顺序锁定资源,就不会有任何问题。


0

采取2a)让每个线程按照特定顺序获得其所需的所有锁(而不是所有锁),如果一个线程遇到无法获取的锁,则释放所有锁。

只要线程按照相同的顺序获取锁,就不会发生死锁。但是,仍然可能会出现饥饿(一个线程可能会遇到一种情况,在没有取得进展的情况下不断释放所有锁)。为确保取得进展,可以为线程分配优先级(0 = 最低优先级,MAX_INT = 最高优先级)- 当线程必须释放其锁时,提高其优先级,并在其获取所有锁时将其降至0。将等待中的线程放入队列中,如果较低优先级的线程需要与较高优先级的线程使用相同的资源,则不启动较低优先级的线程-这样可以保证较高优先级的线程最终将获取其所有锁。除非实际遇到线程饥饿问题,否则不要实施此线程队列,因为这可能比让所有线程同时运行效率低。

你也可以通过实现Omer Schleifer的将所有锁压缩成一个的解决方案来简化事情;但是,除非除了你提到的四个线程之外还有其他线程争夺这些资源(在这种情况下,你仍然需要从外部线程锁定资源),否则你可以更高效地实现这一点,方法是移除所有锁,并将你的线程放入一个循环队列中(所以你的线程只按照相同的顺序不断运行)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接