java线程饥饿死锁_【译】【Java】【多线程】饥饿与公平

译注:本文展示了实现一个多线程同步工具类的过程当中,会遇到和解决那些与并发有关的问题。关于锁的更详细的文章,以及现实当中应该用哪种公平锁,推荐这篇文章。

饥饿与公平

如果一个线程没有被分配到 CPU 执行时间,该线程就处于“饥饿”状态。如果总是分配不到 CPU 执行时间(因为总是被分配到其他线程去了),那么该线程可能会被“饿死”。有一种策略用于避免出现该问题,称作“公平策略”,即保证所有的线程都能公平地得到被执行的机会。

产生饥饿的原因

在 Java 中,有三种最普遍的情形会导致饥饿的发生:

高优先级的线程总是吞占 CPU 执行时间,导致低优先级的线程没有机会;

某些线程总是能被允许进入 synchronized 块,以致某些线程总是得不到机会;

某些线程在等待指定的对象(即调用了该对象的 wait() 方法)时,完全得不到唤醒的机会,因为被唤醒的总是别的线程。

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

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

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

Java 当中的 synchronized 代码块也是导致饥饿的一个因素。它不保证线程进入的顺序,所以理论上某个线程可能永远无法进入 synchronized 块,这种情况下可以说这个线程就被“饿死”了。

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

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

如何在 Java 中实现公平策略

当然我们没办法实现 100% 的绝对公平,但还是可以通过一些结构上的设计来增加线程之间的公平性。

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

public class Synchronizer{

public synchronized void doSynchronized(){

//do a lot of work which takes a long time

}

}

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

用锁对象来代替 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 waitingThreads =

new ArrayList();

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 对象从队列中去掉。

关于执行效率的说明

我们把 Lock 和 FairLock 对比一下就会看到后者的 lock()和 unlock() 增加了很多代码,它们会导致其执行效率比前者略有下降。这个影响的程度如何,取决于 lock() 和 unlock() 之间的同步代码的执行时间,该时间越长,则影响就越小。当然同时也取决于锁本身的使用频繁程度。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值