如果一个线程没有被授予CPU时间,因为其他的线程全部获取到了,它称之为“饥饿”。这个线程“会饿死的”,因为其他的线程被允许这个CPU时间代替他。饥饿的解决方案就是公平----那就是所有的线程公平的获取一个机会去执行。
在Java中的饥饿的原因
下面三个公共的原因将会导致在Java中的线程的饥饿:
- 拥有很高优先级的线程吞掉了所有来自于低优先级线程的CPU时间。
- 线程被无限期的锁定等待进入一个同步块中,因为其他的其他的线程在它之前不断的允许访问。
- 在一个对象上等待的线程(在它上面调用wait方法)保持着无限期的等待,因为其他的线程代替它在无限期的被唤醒。
拥有很高优先级的线程吞掉了所有来自于低优先级线程的CPU时间
你可以单独的设置每一个线程的优先级。优先级越高,分配给线程的CPU时间越多。你可以设置线程的优先级在1到10之间。具体怎么执行依赖于你应用所运行的操作系统。对于更多的应用你最好离开这个不改变的优先级。
线程被无限期的锁定等待进入一个同步块中
Java的同步代码块是另外一个饥饿的原因。Java的同步代码块不能保证哪个正在等待的线程允许进入到同步代码块的顺序。这就意味着有一个理论的风险,一个线程保持着一个锁,一直尝试着进入到这个锁,因为其他的线程在它之前不断的尝试访问它。这个问题就称之为“饥饿”,那就是一个线程“会饿死”因为其他的线程允许这个CPU的时间代替它。
正在一个对象上等待的线程(在它上面调用wait方法)保持无限期的等待
这个notify方法不能保证如果多个线程在对象上已经调用了wait方法notify方法被调用,那么哪个线程会被唤醒。它可能是正在等待的线程的任何一个。因此这里有一个风险一个正在等待的线程永远不会被唤醒,因为其他等待的线程总是代替它被唤醒。
在Java中实现公平
当在Java中它是不可能实现100%公平,我们可以仍然实现我们的同步方法在线程间增加公平性。
首先让我们实现一个简单的同步代码块:
public class Synchronizer{
public synchronized void doSynchronized(){
//do a lot of work which takes a long time
}
}
如上面的代码,如果不止一个县城调用这个方法,他们的一些将会被锁住直到第一个获取访问权的线程离开这个方法。如果不止一个线程被锁住等待访问,这里就不能保证哪个线程将会获取到访问权。
使用锁代替同步代码块
为了增加等待线程的公平,首先我们将会通过一个锁改变这个代码块而不是一个同步代码块:
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();
}
}
注意下这个方法是如何不再声明为同步方法了。代替的这个关键部分被lock方法和unlock方法保护。
这个锁的类简单实现可以看下面这个:
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();
}
}
如果你看到上面的那个同步类,并且你看看这个锁的实现,你将会注意到试着访问这个lock方法的那个线程将会被锁住,如果不止一个线程同时调用lock方法。第二,如果这个锁被锁住了,这个线程在lock方法的while循环内部的wait方法被锁住了。记住在Lock实例中调用调用wait方法释放这个同步锁的一个线程,以至于正在等待进入lock方法的线程可以做这个了。结果就是多个线程都可以以调用lock方法内部的wait方法而结束。
如果你回看这个doSynchronized方法,你会注意到在lock方法和unlock方法之间的状态情况,在这两个调用之间的代码花了很长时间去执行。让我们进一步假设这个代码花费很长时间执行相对于进入这个lock方法和调用wait方法因为这个锁被锁住了。这个就意味着等待能够锁住这个锁的大部分时间并且进入到这个关键部分是在等待lock方法内部的wait方法调用,而不是尝试进入lock方法被锁住了。
正如前面同步代码块陈述的不能保证关于哪个线程获取访问权限如果不止一个线程正在等待进入的情况下。wait方法也不能保证当notify方法被调用的时候哪个线程被唤醒。以至于,Lock类的这个版本伴随着期望公平相对于doSynchronized方法的那个同步版本也不能保证。但是我们可以改变它。
这个Lock类的当前版本调用它自己的wait方法。如果代替的每一个线程在一个分别的对象上调用wait方法,以至于只有一个线程在每一个对象上调用为了wait方法,这个锁类可以去决定这些对象的哪一个区调用notify方法,因此会准确的选择什么线程被唤醒。
一个公平锁
下面就是现实之前的Lock类转化为一个公平锁称之为FairLock。你将会注意到这个实现已经改变了一点伴随着期望的同步以及去更早显示的Lock类的wait方法和notify方法。
确切的,我是如何从前面的Lock类到达了这个设计,是一个长的故事涉及到几个增加的设计步骤,每一个都修复了上一步的问题:嵌套的管理封锁,下滑的条件以及丢失的信号。那个讨论不考虑到这个内容中为了这个内容的短小,但是这个步骤的每一个都在主题上的正确的内容中讨论了。重要的是,调用lock方法的每一个线程都是放到队列中了,并且只有在队列中的第一个线程被允许锁住这个共享锁实例,如果它是被释放了。所有其他的线程将会停靠等待直到他们到达队列的顶部。
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方法不再声明为同步的了。代替的只是这个锁需要去同步就会被嵌套在同步锁的内部。
FairLock创建了一个QueueObject的新的实例,并且对于每一个调用lock方法的线程进行入队。调用unlock方法的这个线程将会获取在队列中的顶部的QueueObject并且在它上面调用doNotify方法,去唤醒等待那个对象的线程。这种方式每次只有一个等待的线程被唤醒,而不是所有等待的线程。这个部分就会管理FairLock的公平性。
注意这个锁的状态是如何被检测的,并且在相同的同步锁中设置去避免滑锁。
也要注意这个QueueObject实际上是一个信号量。这个doWait方法和doNotify方法存储在QueueObject内部的信号。这么做是为了避免只是在调用queueObject.doWait方法之前正在被先占有的一个线程引起的信号丢失,被调用unlock方法的另外一个线程,并且因此queueObject.doNotify()。这个queueObject.doWait()调用被synchronized(this)的外部被代替去避免嵌套的监控闭锁,以至于另外一个线程可以确实的当没有线程正在synchronized(this)的内部执行的时候去调用unlock方法在lock方法内。
最后,注意这个这个queueObject.doWait()是在一个try-catch块中被调用。如果一个InterruptedException异常被抛出,这个线程就会离开这个lock方法,并且我们需要让它离开队列。
注意性能
如果你比较Lock和FariLock类,你将会更要注意在FairLock类的lock方法和unlock方法的内部。这个特别的代码将会引起这个FairLock类比Lock类的同步机制稍微慢点。影响多少这个将会在你的应用中依赖在竞争区域的这个代码被这个FairLock守护去执行的多长时间。执行的时间越长,这个同步添加的开销的意义就越少。当然也依赖于这个代码被调用的次数或者一般多久被调用。
翻译地址:http://tutorials.jenkov.com/java-concurrency/starvation-and-fairness.html