Java并发编程(5)——饥饿与公平

前言

上一篇中我们说的主要问题是死锁发生的条件,以及应对死锁的一些解决方法,它们分别:加锁顺序、加锁时限以及死锁检测,其中死锁检测是最优的解决方法,它通过检查与目标锁相关的数据结构,查询在整个锁的关系图中,是否有某个线程正在等待当前线程持有的锁,以判断死锁是否发生。

1. 饥饿

##1.1 定义
所有CPU时间片被其它线程占据导致其中一个线程无法分配到CPU时间片的状态被称为饥饿(线程可能因为饥饿致死)。

1.2 饥饿状态的原因

  • 高优先级的线程吞噬了所有低优先级线程的CPU时间;
  • 线程被永久阻塞在一个等待进入同步块的状态;
  • 线程在等待一个自身处于永久等待完成的对象(比如调用这个对象的wait()方法)。

1.2.1 线程优先级导致的饥饿

Java的线程类允许我们为每个线程对象设置优先级, public final void setPriority(int newPriority),优先级的范围是1-10,不过我们需要认识到:优先级并不保证我们的程序的执行逻辑,优先级表示的行为的准确解释依赖于程序的运行平台(我暂且理解为操作系统),因此,如果没有必要,我们最好不要改动优先级

1.2.2 永久阻塞

Java的同步代码区对于哪个线程能够允许被进入的次序并没有任何保证,这就意味着存在着某种风险即某个线程永远处在阻塞状态,因为其它线程总是先于它进入同步区。

1.2.3 等待着等待

另一种线程处于饥饿状态的原因就是,线程在等待着永远处于等待状态的对象。如果多个线程持有了某个处于wait()状态的对象,调用该对象的notify()方法并不能保证某个线程被唤醒,任何线程都可能继续保持在等待状态。因此存在这样的风险:某个线程从未被唤醒,因为其它线程总是能够被唤醒。

2. 公平

2.1 定义

这是解决饥饿问题的对策,保证所有线程都能够公平获得CPU运行时间的策略。

2.2 公平性解决办法

  • 使用锁,而不是同步块;
  • 公平锁;

2.2.1 使用锁代替同步块

我们来实现一个锁:

package thread;

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

我们尝试使用上述锁:

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()方法不再使用synchronized修饰,而是在方法中的业务逻辑前后使用lock()unlock()来包围。
在上述Lock实现中,如果存在多线程并发访问lock(),这些线程将阻塞在对lock()的访问上,如果锁已经锁上,那么这些线程将阻塞在while循环中的wait()调用上。我们要记住的是:当线程正在等待进入lock()时,可以调用wait()释放其锁实例对应的同步锁,使得其它多个线程可以进入lock()方法调用wait()
我们再看一下doSynchronized(),我们可以看到lock()unlock()之间的注释:这里将会消耗很长时间——和进入lock()并调用wait()方法来比较的话。这意味着大部分用于进入锁和临界区的时间被消耗在wait()上,而并非是阻塞在进入lock()方法中。
该版本的锁并不能为线程的公平性提供什么保障。

2.2.2 公平锁

下面我们来将上述的锁变为公平锁,我们可以发现新的实现与之前Lock类中的同步和wait()/notify()稍有不同。
准确地说,从原来的Lock类到公平锁是一个循序渐进的过程,每一步都建立在解决前面问题的基础上:Nested Monitor Lockout、Slipped Conditions、Missed Signal。重要的是,每调用一次lock()方法,都会进入一个队列,当解锁后,只有队列中的第一个线程能够被允许锁上FairLock实例,其他所有线程都将处于等待状态,直到它们到达队列的头部。
#####实现:

package concurrent;

import concurrent.entity.QueueObject;

import java.util.ArrayList;
import java.util.List;

public class FairLock {

    private boolean isLocked=false;

    private Thread lockingThread=null;

    private List<QueueObject> waitThreads=new ArrayList<QueueObject>();

    public void lock() throws InterruptedException {
        QueueObject queueObject=new QueueObject();
        boolean isLockedForThisThread=true;
        synchronized (this){
            this.waitThreads.add(queueObject);
        }
        while (isLockedForThisThread){
            synchronized (this){
                //这里判断是否已经加锁或者并非是队列头部,
                //出现这两种情况都导致isLockedForThisThread=true
                isLockedForThisThread=isLocked||waitThreads.get(0)!=queueObject;
                if (!isLockedForThisThread){
                    isLocked=true;
                    waitThreads.remove(queueObject);
                    lockingThread=Thread.currentThread();
                    return;
                }
            }
            try{
                queueObject.doWait();
            } catch (InterruptedException e) {
                synchronized (this){
                    waitThreads.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 (waitThreads.size()>0){
            waitThreads.get(0).notify();
        }
    }
}

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

    @Override
    public boolean equals(Object obj) {
        return this==obj;
    }
}

从FairLock的实现中我们注意到lock()方法并没有声明为synchronized,而是对需要被同步的代码,在方法中用synchronized修饰。

下面这段不是很懂

**FairLock在lock()中创建了一个QueueObject对象,每个调用lock()的线程就会被加入队列。而调用unlock()方法,则会取得队列头部的QueueObject实例,调用它的doNotify()方法,唤醒在该对象上等待的线程。**通过这种方式,在同一时间只有一个等待线程会被唤醒,而不是所有等待线程,这就是公平锁的核心所在。
还需注意到,QueueObject 实际是一个 semaphoredoWait()doNotify()方法在 QueueObject 中保存着信号。这样做避免一个线程在调用 queueObject.doWait()之前被另一个调用 unlock()并随之调用 queueObject.doNotify()的线程重入,从而导致信号丢失。queueObject.doWait()调用放置在 synchronized(this)块之外,以避免被 monitor 嵌套锁死,所以另外的线程可以解锁,只要当没有线程在 lock ()方法的 synchronized(this)块中执行即可。

2.3 注意性能开销

通过比较LockFairLock,我们可以发现FairLock的lock()unLock()上有许多深入的地方,这些差异性导致了FairLock执行起来更慢一些,但是究竟有多大影响是取决于临界区的执行时长,长一些则FairLock的影响小一些

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值