Starvation and Fairness

If a thread is not granted CPU time because other threads grab it all, it is called “starvation”. The thread is “starved to death” because other threads are allowed the CPU time instead of it. The solution to starvation is called “fairness” - that all threads are fairly granted a chance to execute.
如果某个线程因为其他线程抢占了所有时间而没有被授予CPU时间,则称为“饥饿”。 该线程“饿死了”,因为其他线程有CPU时间,而它没有。 解决饥饿的方法称为“公平”-所有线程都被公平地授予执行的机会。

Causes of Starvation in Java

The following three common causes can lead to starvation of threads in Java:

  • Threads with high priority swallow all CPU time from threads with lower priority.
    高优先级的线程会占用低优先级的线程的所有CPU时间。
  • Threads are blocked indefinately waiting to enter a synchronized block, because other threads are constantly allowed access before it.
    线程因等待进入同步块而无限期地阻塞,因为一直允许其他线程对其进行访问。
  • Threads waiting on an object (called wait() on it) remain waiting indefinitely because other threads are constantly awakened instead of it.
    等待对象的线程(在该对象上称为wait())将无限期地等待,因为其他线程会不断唤醒它而不是它。

Threads with high priority swallow all CPU time from threads with lower priority

You can set the thread priority of each thread individually. The higher the priority the more CPU time the thread is granted. You can set the priority of threads between 1 and 10. Exactly how this is interpreted depends on the operating system your application is running on. For most applications you are better off leaving the priority unchanged.
您可以对每个线程设置优先级。 优先级越高,线程占有CPU时间就越多。优先级的等级为1-10 。具体线程的优先级取决于应用程序所运行的操作系统。 对于大多数应用程序,最好不要更改优先级。

Threads are blocked indefinitely waiting to enter a synchronized block

Java’s synchronized code blocks can be another cause of starvation. Java’s synchronized code block makes no guarantee about the sequence in which threads (waiting to enter the synchronized block) are allowed to enter. This means that there is a theoretical risk that a thread remains blocked forever trying to enter the block, because other threads are constantly granted access before it. This problem is called “starvation”, that a thread is “starved to death” by because other threads are allowed the CPU time instead of it.
Java的同步代码块可能是造成饥饿的另一个原因。 Java的同步代码块不能保证允许等待进入同步块的线程进入的顺序。 这意味着从理论上讲,一个线程在尝试进入同步块时将永远处于阻塞状态,因为其他线程在此之前一直被授予访问权限。 这个问题称为“饥饿”,一个线程被“饿死”是因为其他线程被允许占用CPU时间而不是它。

Threads waiting on an object (called wait() on it) remain waiting indefinitely

The notify() method makes no guarantee about what thread is awakened if multiple thread have called wait() on the object notify() is called on. It could be any of the threads waiting. Therefore there is a risk that a thread (waiting on a certain object) is never awakened because other waiting threads are always awakened instead of it.
如果多个线程在对象notify()上调用了wait(),则notify()方法不能保证唤醒哪个线程。 可能是任何正在等待的线程。 因此,存在这样的风险,即永远不会唤醒等待在某个对象上的某个线程,因为总是会唤醒其他等待线程而不是这个线程。

Implementing Fairness in Java

While it is not possible to implement 100% fairness in Java we can still implement our synchronization constructs to increase fairness between threads.
尽管不可能在Java中实现100%的公平性,但我们仍然可以实现同步结构来增加线程之间的公平性。
First lets study a simple synchronized code block:

public class Synchronizer{

  public synchronized void doSynchronized(){
    //do a lot of work which takes a long time
  }

}

If more than one thread call the doSynchronized() method, some of them will be blocked until the first thread granted access has left the method. If more than one thread are blocked waiting for access there is no guarantee about which thread is granted access next.
如果有多个线程调用doSynchronized()方法,则部分线程将被阻塞,直到被授权的线程离开该方法。 如果有多个线程被阻塞等待访问,则无法保证下一个线程被授予访问权限。

Using Locks Instead of Synchronized Blocks

To increase the fairness of waiting threads first we will change the code block to be guarded by a lock rather than a synchronized block:
为了增加等待线程的公平性,用lock而不是synchronized块

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();
  }

}

Notice how the doSynchronized() method is no longer declared synchronized. Instead the critical section is guarded by the lock.lock() and lock.unlock() calls.
用lock.lock() and lock.unlock() 代替synchronized块来访问临界区。

A simple implementation of the Lock class could look like this:

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();
  }
}

If you look at the Synchronizer class above and look into this Lock implementation you will notice that threads are now blocked trying to access the lock() method, if more than one thread calls lock() simultanously. Second, if the lock is locked, the threads are blocked in the wait() call inside the while(isLocked) loop in the lock() method. Remember that a thread calling wait() releases the synchronization lock on the Lock instance, so threads waiting to enter lock() can now do so. The result is that multiple threads can end up having called wait() inside lock().
如果您查看上面的Synchronizer类并研究Lock实现,您将注意到,多个线程因同时调用lock()而阻塞(因为synchronized void lock():同步实例方法会锁住这个实例)。 其次,若有线程在lock()中对lock实例上锁,退出后其他线程获得lock锁执行lock(),也会因为while(isLocked=true)而阻塞。 调用wait()的线程会释放Lock实例上的同步锁,因此等待进入lock()的其他线程就有机会进。 结果是,有锁的多个线程可能会在lock()中调用wait(),而释放锁(因为没有等到执行条件isLocked=false)。
【也就是线程即使拥有lock实例锁,但isLocked=true,也会wait释放掉锁,让其他线程来执行】

If you look back at the doSynchronized() method you will notice that the comment between lock() and unlock() states, that the code in between these two calls take a “long” time to execute. Let us further assume that this code takes long time to execute (compared to entering the lock() method and calling wait() )because the lock is locked. This means that the majority of the time (waited to be able to lock the lock and enter the critical section) is spent waiting in the wait() call inside the lock() method, not being blocked trying to enter the lock() method.
如果回头看doSynchronized()方法,您会注意到lock()和unlock()状态之间的注释,这两个调用之间的代码要花很长时间才能执行。 让我们进一步假设,与进入lock()方法和调用wait()相比,临界区代码需要较长时间执行。 这意味着等待获得lock实例锁并进入关键部分的大部分时间都花在了lock()方法内部的wait()调用中,而不是在尝试进入lock()方法时被阻塞。

As stated earlier synchronized blocks makes no guarantees about what thread is being granted access if more than one thread is waiting to enter. Nor does wait() make any guarantees about what thread is awakened when notify() is called. So, the current version of the Lock class makes no different guarantees with respect to fairness than synchronized version of doSynchronized(). But we can change that.
如前所述,如果有多个线程正在等待进入,则同步块不能保证授予哪个线程访问权限。 notify()时也没保证唤醒了wait()哪个线程。 因此,当前的Lock类版本与doSynchronized()的同步版本在公平性方面没有不同的保证。 但是我们可以改变这一点。

The current version of the Lock class calls its own wait() method. If instead each thread calls wait() on a separate object, so that only one thread has called wait() on each object, the Lock class can decide which of these objects to call notify() on, thereby effectively selecting exactly what thread to awaken.
当前版本的Lock类将调用其自己的wait()方法。 相反,如果每个线程在一个单独的对象上调用wait(),从而只有一个线程在每个对象上调用了wait(),则Lock类可以决定在这些对象中的哪个上调用notify(),从而有效地准确选择要使用的线程唤醒

A Fair Lock

Below is shown the previous Lock class turned into a fair lock called FairLock. You will notice that the implementation has changed a bit with respect to synchronization and wait() / notify() compared to the Lock class shown earlier.
下面显示了以前的Lock类已变成称为FairLock的公平锁。 您会注意到,与前面显示的Lock类相比,该实现在同步和wait()/ notify()方面有所变化。

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. That discussion is left out of this text to keep the text short, but each of the steps are discussed in the appropriate texts on the topic ( see the links above). What is important is, that every thread calling lock() is now queued, and only the first thread in the queue is allowed to lock the FairLock instance, if it is unlocked. All other threads are parked waiting until they reach the top of the queue.
确切地说,我是从上一个Lock类开始的,是一个较长的故事,涉及几个增量设计步骤,每个步骤都解决了上一步的问题:Nested Monitor LockoutSlipped ConditionsMissed Signals。 为了使文本简短,该讨论被省略了,但是在该主题的相应文本中讨论了每个步骤(请参见上面的链接)。 重要的是,每个调用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;
  }
}

First you might notice that the lock() method is no longer declared synchronized. Instead only the blocks necessary to synchronize are nested inside synchronized blocks.
首先,您可能会注意到lock()方法不再声明为同步。需要同步代码方法普通方法的同步块里。

FairLock creates a new instance of QueueObject and enqueue it for each thread calling lock(). The thread calling unlock() will take the top QueueObject in the queue and call doNotify() on it, to awaken the thread waiting on that object. This way only one waiting thread is awakened at a time, rather than all waiting threads. This part is what governs the fairness of the FairLock.
FairLock创建一个新的QueueObject实例(它是为调用lock()的线程排队)。调用unlock()的线程将获取QueueObject的头部对象并对其调用doNotify(),以唤醒在该对象上等待的线程。这样,一次仅唤醒一个等待线程,而不唤醒所有等待线程。这部分决定了FairLock的公平性。
【注意队列的特性:先进先出】

Notice how the state of the lock is still tested and set within the same synchronized block to avoid slipped conditions.
检查和设置锁状态在同一个同步块中,以免发生slipped conditions情况。

Also notice that the QueueObject is really a semaphore. The doWait() and doNotify() methods store the signal internally in the QueueObject. This is done to avoid missed signals caused by a thread being preempted just before calling queueObject.doWait(), by another thread which calls unlock() and thereby queueObject.doNotify(). The queueObject.doWait() call is placed outside the synchronized(this) block to avoid nested monitor lockout, so another thread can actually call unlock() when no thread is executing inside the synchronized(this) block in lock() method.
避免missed signals
QueueObject实际上是一个信号量。 有方法:doWait()和doNotify()方法。
若不这些做,可能发生missed signals。
情景如下:
若QueueObject没有doWait()、doNotify(),则
第一个线程调用lock(),将isLocked设置为true,这样其他线程无法再上锁,并且return了。
执行临界区
其他线程因无法获得锁,进入等待队列,阻塞在了synchronized(this)
执行unlock(),唤醒,但因为线程在lock时并没有执行到wait,所以就信号丢失,发生了missed signals。当那个线程执行到doWait()时,还需要wait

若QueueObject设置了doWait()、doNotify(),则
执行unlock(),唤醒会将isNotified 设置为true,当执行到对应线程doWait时,无需再wait
(注意queueObject是线程局部变量,并不共享)

避免Nested Monitor Lockout
lock():若把queueObject.doWait()放置在synchronized(this)内部,则会先锁住this,然后锁住queueObject,但wait就释放了queueObject ,等待被唤醒但没释放this
unlock():是同步方法,需要锁this,才能唤醒。但它被lock()占有。

把若把queueObject.doWait()放置在synchronized(this)外部,则this、queueObject不再是嵌套锁,synchronized(this)与queueObject.doWait();分离,从而可以执行完synchronized(this)就可以释放锁,queueObject.doWait()释放锁,其他线程可以获得锁执行unlock(),从而可以notify等待的线程

避免Slipped Conditions
isLockedForThisThread 的检查和设置放在同一同步块里,避免一个线程检查但未设置时,另一个线程读到未设置的数据。

Finally, notice how the queueObject.doWait() is called inside a try - catch block. In case an InterruptedException is thrown the thread leaves the lock() method, and we need to dequeue it.
最后,请注意在try-catch块中如何调用queueObject.doWait()。如果抛出InterruptedException,线程将离开lock()方法,我们需要将其出队。

A Note on Performance

If you compare the Lock and FairLock classes you will notice that there is somewhat more going on inside the lock() and unlock() in the FairLock class. This extra code will cause the FairLock to be a sligtly slower synchronization mechanism than Lock. How much impact this will have on your application depends on how long time the code in the critical section guarded by the FairLock takes to execute. The longer this takes to execute, the less significant the added overhead of the synchronizer is. It does of course also depend on how often this code is called.
如果比较Lock和FairLock类,您会发现FairLock类的lock()和unlock()内部还有更多操作。 此额外的代码将使FairLock的同步机制比Lock慢得多。 这将对您的应用程序产生多大影响,取决于FairLock保护的关键部分中的代码需要执行多长时间。 执行时间越长,同步器增加的开销就越小。 当然,这也取决于此代码被调用的频率。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值