【译】【Java】【多线程】饥饿与公平

饥饿与公平

原文地址: http://tutorials.jenkov.com/j...html

若是一个线程没有被分配到 CPU 执行时间,该线程就处于“饥饿”状态。若是老是分配不到 CPU 执行时间(由于老是被分配到其余线程去了),那么该线程可能会被“饿死”。有一种策略用于避免出现该问题,称做“公平策略”,即保证全部的线程都能公平地获得被执行的机会。java

产生饥饿的缘由

在 Java 中,有三种最广泛的情形会致使饥饿的发生:安全

  1. 高优先级的线程老是吞占 CPU 执行时间,致使低优先级的线程没有机会;
  2. 某些线程老是能被容许进入 synchronized 块,以至某些线程老是得不到机会;
  3. 某些线程在等待指定的对象(即调用了该对象的 wait() 方法)时,彻底得不到唤醒的机会,由于被唤醒的老是别的线程。

高优先级的线程老是吞占 CPU 执行时间

每一个线程均可以单独设置优先级。优先级越高,该线程就能得到更多的 CPU 执行时间。优先级的值最低为 1 最高为 10。至于如何根据优先级来分配 CPU 执行时间,则依赖于操做系统的具体实现。在大多数应用中,咱们最好不要去擅自修改它。this

线程无限等待进入 synchronized 块的机会

Java 当中的 synchronized 代码块也是致使饥饿的一个因素。它不保证线程进入的顺序,因此理论上某个线程可能永远没法进入 synchronized 块,这种状况下能够说这个线程就被“饿死”了。操作系统

线程无限等待被锁对象唤醒的机会

当多个线程同时调用的某个对象的 wait() 方法并等待时,notify() 方法不保证必定能唤醒哪一个指定的线程。因此若是它老是不去唤醒某个线程的话,这个线程就处于永久性地等待当中了。线程

如何在 Java 中实现公平策略

固然咱们没办法实现 100% 的绝对公平,但仍是能够经过一些结构上的设计来增长线程之间的公平性。设计

首先咱们来看一个简单的 synchronized 代码块:code

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

当多个线程调用 doSynchronized() 方法时,只有一个线程可以进入该方法并执行,并且该线程退出该方法后,正在等待的线程中没法保证哪个才是接下来能够进入的。htm

用锁对象来代替 synchronized 块

为了加强公平性,第一步咱们先把 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();
  }
}

请注意 doSynchronized() 方法自己如今再也不是同步的了,须要同步执行的代码如今由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() 方法的线程会在 wait() 语句处阻塞。这里要注意:当线程成功调用 wait() 方法时,会自动释放 Lock 对象的锁,因而其余的线程可以得以进入 lock() 方法,最终会有多个线程都阻塞在 wait() 语句处。

咱们回头看 doSynchronized() 方法中 lock() 和 unlock() 之间的部分,假设这部分代码须要很长时间来执行,甚至比线程在 wait() 语句处等待所花的时间都长的多。那么线程得到锁所需的时间主要也是耗在 wait() 语句处,而不是进入 lock() 方法的时候。

在目前这个版本的代码中,不论线程是在 synchronized 块阻塞,仍是在 wait() 处阻塞,都不能保证哪一个线程能必定被唤醒,因此目前的代码还没有提供公平策略。

(译注:之因此改为这样,目的是令线程在进入 lock() 方法时的阻塞时间尽量短,也就是全部的线程都在 wait() 处阻塞,以便实施接下来的改动。)

目前版本的 Lock 对象是在调用自身的 wait() 方法。咱们改掉这点,让每一个线程调用不一样对象的 wait() 方法的话,那么就能够自行挑选调用哪一个对象的 notify() 方法,以此实现自行挑选唤醒哪一个线程。

公平锁

下面的代码展现了将 Lock 类转化为 FairLock 类的结果。请注意同步方式和 wait()/notify() 的调用方式有了哪些的变化。

整个改动的实现是阶段性的,这个过程当中须要依次解决内部锁对象死锁同步条件丢失以及解锁信号丢失等问题。因为篇幅长度所限这里就不详述了(请参考上面的连接)。这里最重要的改动点,就是对 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。由于只有这个方法里面的部分代码才须要同步。

FairLock 会为每一个线程建立一个新的 QueueObject 对象并将其加入队列。调用 unlock() 方法的线程会从队列中取第一个元素对象并调用它的 doNotify() 方法,这样唤醒的就只有一个线程,而不是一堆线程。这个就是 FairLock 的公平机制所在。

注意接下来就是在同步块中从新检查条件并更新锁状态,这是为了不同步条件丢失。

此外 QueueObject 其实是一个信号量,doWait()doNotify() 方法的目的是存取锁的状态信号,以免解锁信号丢失,即在一个线程调用 queueObject.doWait() 以前,另外一个线程已经在 unlock() 方法中调用了该对象的 queueObject.doNotify() 方法。至于将 queueObject.doWait() 方法的调用放在同步块外面,是为了不内部对象死锁的状况发生,这样另外一个线程就能够持有 FairLock 对象的锁,并安全的调用 unlock() 方法了。

最后就是对 queueObject.doWait() 这条语句进行异常捕获。若是这条语句执行时发生了 InterruptedException 异常,那么就须要在离开这个方法前将 queueObject 对象从队列中去掉。

关于执行效率的说明

咱们把 LockFairLock 对比一下就会看到后者的 lock() unlock() 增长了不少代码,它们会致使其执行效率比前者略有降低。这个影响的程度如何,取决于 lock()unlock() 之间的同步代码的执行时间,该时间越长,则影响就越小。固然同时也取决于锁自己的使用频繁程度。

相关文章
相关标签/搜索