翻译:GentlemanTsao,2020-05-28
以下三个常见原因可能导致Java中的线程的饥饿:
你可以分别设置每个线程的线程优先级。 优先级越高,授予线程的CPU时间就越多。 你可以设置线程的优先级在1到10之间。对优先级的具体解释取决于应用程序所运行的操作系统。 对于大多数应用程序,最好不要更改优先级。
Java的同步代码块可能是造成饥饿的另一个原因。 Java的同步代码块不能保证允许等待进入同步块的线程进入的顺序。 这意味着在理论上存在一种风险,那就是线程试图进入该块而始终处于阻塞状态,因为其他线程在此之前一直被授予访问权限。 这个问题称为“饥饿”,一个线程被“饿死”是因为其他线程占用了CPU时间而不是它。
如果多个线程在某对象上调用了wait(),则notify()方法不能保证哪个线程被唤醒。 可能是任何正在等待的线程。 因此,存在这样的风险,即等待在某个对象上的线程永远不会被唤醒,因为被唤醒的总是其他等待线程而不是它。
尽管不可能在Java中实现100%的公平性,但我们仍然可以实现同步结构来增加线程之间的公平性。
让我们先研究一个简单的同步代码块:
public class Synchronizer{
public synchronized void doSynchronized(){
//do a lot of work which takes a long time
}
}
如果有多个线程调用doSynchronized()方法,则其中的一些线程将被阻塞,直到第一个被授予访问权限的线程离开该方法为止。 如果有多个线程被阻塞等待访问,则不能保证接下来被授予访问权限的是哪个线程。
为了增加等待线程的公平性,我们首先将代码块更改为由锁保护,而不是由同步块保护:
public class Synchronizer{
Lock lock = new Lock();
public void doSynchronized() throws InterruptedException{
this.lock.lock();
//critical section, do a lot of work which takes a long time
this.lock.unlock();
}
}
请注意,doSynchronized()方法不再声明为synchronized。 相反,临界区由lock.lock()和lock.unlock()调用保护。
Lock类的简单实现如下所示:
public class Lock{
private boolean isLocked = false;
private Thread lockingThread = null;
public synchronized void lock() throws InterruptedException{
while(isLocked){
wait();
}
isLocked = true;
lockingThread = Thread.currentThread();
}
public synchronized void unlock(){
if(this.lockingThread != Thread.currentThread()){
throw new IllegalMonitorStateException(
"Calling thread has not locked this lock");
}
isLocked = false;
lockingThread = null;
notify();
}
}
查看上面的Synchronizer类并研究此Lock实现,你会注意到,如果多个线程同时调用lock(),则线程现在试图访问lock()方法时被阻塞。 其次,如果锁被锁定,则线程在lock()方法的while(isLocked)循环内的wait()调用中被阻塞。 请记住,调用wait()的线程会释放Lock实例上的同步锁,因此等待进入lock()的线程现在可以这样做。 结果是多个线程最终可能在lock()内部调用了wait()。
如果回头看doSynchronized()方法,您会注意到lock()和unlock()状态之间的注释,这两个调用之间的代码需要花费很长的时间才能执行。 让我们进一步假设,与进入lock()方法和调用wait()相比,此代码需要花费较长的时间执行,因为该锁已被锁定。 这意味着在等待锁定锁并进入临界区时,大部分时间都花在了lock()方法内部的wait()调用中,而不是阻塞在尝试进入lock()方法时。
如前所述,如果有多个线程在等待进入同步块,则同步块不能保证授予哪个线程访问权限。 wait()也不保证在调用notify()时唤醒了哪个线程。 因此,当前的Lock类版本与doSynchronized()的synchronized 版本在公平性方面没有什么不同。 但是我们可以修改。
当前版本的Lock类调用自己的wait()方法。 然而,如果每个线程在一个单独的对象上调用wait(),从而只有一个线程在各自对象上调用了wait(),则Lock类可以决定在哪个对象上调用notify(),从而实际上准确的选择要唤醒的线程 。
下面展示了将原来的Lock类变成了一个公平锁,即FairLock。 你会发现,与前面展示的Lock类相比,在同步和wait()/ notify()上的实现有所变化。
确切地说,达成这个设计是从上一个Lock类开始的。说来话长,这涉及几个增量设计步骤,每个步骤都解决了上一步的问题:嵌套管程锁死,滑丝条件和信号丢失。 为了使文本简短,该讨论被省略了,但是每个步骤在该主题的相应文章中讨论(请参见上面的链接)。 重要的是,现在每个调用lock()的线程都已排队,并且只有队列中的第一个线程才可以锁定FairLock实例(如果已解锁)。 所有其他线程将被暂存,直到它们到达队列的头部。
public class FairLock {
private boolean isLocked = false;
private Thread lockingThread = null;
private List<QueueObject> waitingThreads =
new ArrayList<QueueObject>();
public void lock() throws InterruptedException{
QueueObject queueObject = new QueueObject();
boolean isLockedForThisThread = true;
synchronized(this){
waitingThreads.add(queueObject);
}
while(isLockedForThisThread){
synchronized(this){
isLockedForThisThread =
isLocked || waitingThreads.get(0) != queueObject;
if(!isLockedForThisThread){
isLocked = true;
waitingThreads.remove(queueObject);
lockingThread = Thread.currentThread();
return;
}
}
try{
queueObject.doWait();
}catch(InterruptedException e){
synchronized(this) { waitingThreads.remove(queueObject); }
throw e;
}
}
}
public synchronized void unlock(){
if(this.lockingThread != Thread.currentThread()){
throw new IllegalMonitorStateException(
"Calling thread has not locked this lock");
}
isLocked = false;
lockingThread = null;
if(waitingThreads.size() > 0){
waitingThreads.get(0).doNotify();
}
}
}
public class QueueObject {
private boolean isNotified = false;
public synchronized void doWait() throws InterruptedException {
while(!isNotified){
this.wait();
}
this.isNotified = false;
}
public synchronized void doNotify() {
this.isNotified = true;
this.notify();
}
public boolean equals(Object o) {
return this == o;
}
}
首先,你可能会注意到lock()方法不再声明为synchronized, 而是仅将需要同步的块嵌套在synchronized块内。
FairLock创建一个新的QueueObject实例,并为每个调用lock()的线程排队。 调用unlock()的线程将获取队列头部的QueueObject并对其调用doNotify(),以唤醒在该对象上等待的线程。 这样,一次仅唤醒一个等待线程,而不唤醒所有等待线程。 这个部分决定着FairLock的公平性。
请注意,同一同步块内仍要测试并设置锁的状态,以免发生滑丝条件。
还要注意,QueueObject实际上是一个信号量。 doWait()和doNotify()方法将信号存储在QueueObject中。 这样做是为了避免导致信号丢失——由于线程在调用queueObject.doWait()之前被另一个线程所抢占,而另一个线程又调用unlock(),从而调用queueObject.doNotify()。 调用queueObject.doWait()放置在synchronized(this)块之外,以避免嵌套管程锁死,因此,当在lock()方法的synchronized(this)块内没有线程正在执行时,另一个线程实际上可以调用unlock()。
最后,请注意在try-catch块内调用了queueObject.doWait()。 如果抛出InterruptedException,线程将离开lock()方法,我们需要将其出队。
如果比较Lock和FairLock类,你会发现FairLock类的lock()和unlock()内部还有更多操作。 此额外的代码使FairLock成为比Lock稍慢的同步机制。 它对应用程序的影响取决于FairLock保护的临界区中的代码执行的时间。 执行时间越长,同步器增加的开销就越小。 当然,这也取决于此代码被调用的频率。
原文:
Exactly how I arrived at this design beginning from the previous Lock class is
a longer story involving several incremental design steps, each fixing the problem
of the previous step: Nested Monitor Lockout,
Slipped Conditions, and
Missed Signals.
说明:
Slipped Conditions,表示没有真正锁住,本文译为滑丝条件,比喻锁打滑了。
译文:
确切地说,达成这个设计是从上一个Lock类开始的。说来话长,这涉及几个增量设计步骤,每个步骤都解决了上一步的问题:嵌套管程锁死,滑丝条件和信号丢失。
下一篇:
2020版Java并发和多线程教程(十九):嵌套管程锁死
并发系列专栏:
Java并发和多线程教程2020版