[译]SEI CERT Oracle Coding Standard for Java - Thread Apis(THI02-J)

原文:https://wiki.sei.cmu.edu/confluence/display/java/THI02-J.+Notify+all+waiting+threads+rather+than+a+single+thread

当线程调用Object.wait()方法后,等待被唤醒并在预设条件满足时继续执行,根据THI03-J. Always invoke wait() and await() methods inside a loop的建议,当接收到通知时,等待线程会检验预设条件是否成立,否则必须保持等待状态。
java.lang.Object提供notify()notifyAll()方法用于唤醒单个或一组线程。调用这两个方法的线程必须和等待线程持有同一个对象锁,否则,就会抛出IllegalMonitorStateException异常。notifyAll()会唤醒一组等待这个对象锁的线程,如果它们的预设条件满足,就会恢复执行。而且,如果这一组线程在进入等待状态之前持有另一个特定的对象锁,那么,这组被通知的线程中只有一个能获取这个特定的对象锁,因此,其他的线程仍然会保持等待。notify()方法会唤醒一个线程,但并不保证会唤醒哪一个。如果被唤醒的线程预设条件不满足,它还是会保持等待。这往往与我们通知唤醒的初衷不符合。
因此,只有在以下条件都满足的条件下,才允许调用notify()方法:

  • 所有等待线程使用相同的预设条件
  • 被唤醒后,所有的线程都执行一系列相同的操作。也就是任何一个线程都可以被唤醒继续执行
  • 只有一个线程需要被唤醒

对于一组线程,如果它们是一样的执行逻辑,并且提供无状态的服务,那么,就是满足以上条件的。

java.util.concurrent.locks提供了Condition.signal()Condition.signalAll()方法用于唤醒阻塞在Condition.await()的调用。
如果使用java.util.concurrent.locks.Lock,那么久需要用到Condition对象。尽管Lock对象也可以使用Object.wait(),Object.notify(),Object.notifyAll()这些方法。但是根据LCK03-J. Do not synchronize on the intrinsic locks of high-level concurrency objects的解释,应该避免调用这些方法。同步代码使用Lock对象应该关联使用一个或多个Condition,而不是使用对象自身的锁。Lock对象直接提供了加锁策略,因此,对于Lock对象,应该使用await(),signal(),signalAll()这些方法,而不是wait(),notify(),notifyAll()
只有在以下条件都满足的条件下,才允许调用signal()方法:

  • 所有等待线程的Condition对象是一样的
  • 被唤醒后,所有的线程都执行一系列相同的操作。也就是任何一个线程都可以被唤醒继续执行
  • 只有一个线程需要被唤醒

或者,以下条件都满足:

  • 每个线程使用唯一的Conditon对象
  • 每个Conditon对象都关联同一个Lock对象

如果安全地使用,signal()方法比signalAll()有更好的性能。
如果调用notify()signal()唤醒一个线程,但线程预设条件没有满足,那么它会继续等待,结果就是没有一个线程被唤醒继续执行,系统功能可能会停滞。

不规范的代码示例(notify())

这段代码需要在多线程环境下完成一个复杂的多步骤的流程。每个线程执行的步骤是由time属性决定的,没个线程都在等待自己执行的时机,线程执行完一个步骤,time加1,并通知后续线程执行。

public final class ProcessStep implements Runnable {
  private static final Object lock = new Object();
  private static int time = 0;
  private final int step; // Do Perform operations when field time
                          // reaches this value
 
  public ProcessStep(int step) {
    this.step = step;
  }
 
  @Override public void run() {
    try {
      synchronized (lock) {
        while (time != step) {
          lock.wait();
        }
 
        // Perform operations
 
        time++;
        lock.notify();
      }
    } catch (InterruptedException ie) {
      Thread.currentThread().interrupt(); // Reset interrupted status
    }
  }
 
  public static void main(String[] args) {
    for (int i = 4; i >= 0; i--) {
      new Thread(new ProcessStep(i)).start();
    }
  }
}

这段逻辑违背了活跃性条件,也就是,每个线程都有不一样的预设条件,但是notify()方法每次只唤醒一个线程,除非恰好唤醒了下一步应该执行的线程,否则,程序就会死锁。

正确的方案(notifyAll())

下面的代码中,每个线程完成自己的步骤后,会调用notifyAll()唤醒所有的线程。被唤醒的线程如果发现自己的预设条件满足,就可以继续执行,其他线程预设条件不满足,就会保持等待状态。
对比上述不规范代码,只是修改了run()方法。

public final class ProcessStep implements Runnable {
  private static final Object lock = new Object();
  private static int time = 0;
  private final int step; // Perform operations when field time
                          // reaches this value
  public ProcessStep(int step) {
    this.step = step;
  }
 
  @Override public void run() {
    try {
      synchronized (lock) {
        while (time != step) {
          lock.wait();
        }
   
        // Perform operations
   
        time++;
        lock.notifyAll(); // Use notifyAll() instead of notify()
      }
    } catch (InterruptedException ie) {
      Thread.currentThread().interrupt(); // Reset interrupted status
    }
  }
 
}

不规范代码示例(Conditon Interface)

类似之前的代码,只是使用了Conditon的方式通知。

public class ProcessStep implements Runnable {
  private static final Lock lock = new ReentrantLock();
  private static final Condition condition = lock.newCondition();
  private static int time = 0;
  private final int step; // Perform operations when field time
                          // reaches this value
  public ProcessStep(int step) {
    this.step = step;
  }
 
  @Override public void run() {
    lock.lock();
    try {
      while (time != step) {
        condition.await();
      }
 
      // Perform operations
 
      time++;
      condition.signal();
    } catch (InterruptedException ie) {
      Thread.currentThread().interrupt(); // Reset interrupted status
    } finally {
      lock.unlock();
    }
  }
 
  public static void main(String[] args) {
    for (int i = 4; i >= 0; i--) {
      new Thread(new ProcessStep(i)).start();
    }
  }
}

类似使用Object.notify()signal()方法可能唤醒任意的线程。

正确的方案(signalAll())

使用signalAll()通知所有等待的线程,在await()方法返回前,当前线程会获取这个条件关联的锁,当线程退出时,可以确保持有这个锁,预设条件满足的线程可以继续执行,其他线程会保持等待。

public class ProcessStep implements Runnable {
  private static final Lock lock = new ReentrantLock();
  private static final Condition condition = lock.newCondition();
  private static int time = 0;
  private final int step; // Perform operations when field time
                          // reaches this value
  public ProcessStep(int step) {
    this.step = step;
  }
 
  @Override public void run() {
    lock.lock();
    try {
      while (time != step) {
        condition.await();
      }
   
      // Perform operations
 
      time++;
      condition.signalAll();
    } catch (InterruptedException ie) {
      Thread.currentThread().interrupt(); // Reset interrupted status
    } finally {
      lock.unlock();
    }
  }
 
}

正确的方案(每个线程都有唯一的条件)

为每个线程分配一个自己的等待条件,所有的线程都可以访问所有的条件。

// Declare class as final because its constructor throws an exception
public final class ProcessStep implements Runnable {
  private static final Lock lock = new ReentrantLock();
  private static int time = 0;
  private final int step; // Perform operations when field time
                          // reaches this value
  private static final int MAX_STEPS = 5;
  private static final Condition[] conditions = new Condition[MAX_STEPS];
 
  public ProcessStep(int step) {
    if (step <= MAX_STEPS) {
      this.step = step;
      conditions[step] = lock.newCondition();
    } else {
      throw new IllegalArgumentException("Too many threads");
    }
  }
 
  @Override public void run() {
    lock.lock();
    try {
      while (time != step) {
        conditions[step].await();
      }
 
      // Perform operations
 
      time++;
      if (step + 1 < conditions.length) {
        conditions[step + 1].signal();
      }
    } catch (InterruptedException ie) {
      Thread.currentThread().interrupt(); // Reset interrupted status
    } finally {
      lock.unlock();
    }
  }
 
  public static void main(String[] args) {
    for (int i = MAX_STEPS - 1; i >= 0; i--) {
      ProcessStep ps = new ProcessStep(i);
      new Thread(ps).start();
    }
  }

虽然使用了signal()方法,但只有满足这个特定条件的线程才会被唤醒。需要注意,这个方案安全地前提是,不存在不安全的代码使用这个实例创建新的线程。

如果觉得我的文章对您有用,请在支付宝公益平台找个项目捐点钱。 @sxzhou Dec 1, 2017

奉献爱心