文档章节

【16】并发中的饥饿问题以及公平性

秋雨霏霏
 秋雨霏霏
发布于 2017/09/10 20:20
字数 2329
阅读 182
收藏 0

如果某个线程因为所需要的资源被其他线程占用而得不到CPU调度,那这种情况就被称之为“饥饿”。 如果其他线程一直占用资源,那饥饿的线程就会被“饿死”(类似死锁的情况,一直在阻塞等待某个资源,或者运气比较差,每次都没有抢到)。 解决饥饿问题,通常使用“公平”策略,来保障所有的线程都有机会被CPU调度。

Java中的饥饿问题

在Java中,通常有以下几种情况导致饥饿:

  1. CPU被高优先级的线程完全占用(打满),而导致低优先级的线程一直没有机会获得CPU
  2. synchronized同步块导致某个线程频繁重入(偏向锁,以及OS调度策略,导致已经获得monitor锁的线程有更多的机会再次获得锁),而导致其他线程只能阻塞等待
  3. 某个线程在某个对象的条件队列上等待(wait()),而其他线程不断的抢入

优先级导致的饥饿问题

可以为每个线程单独设置一个优先级。 高优先级的线程可以获得更多CPU时间。 在Java中可以设置1~10个等级的优先级。 实际上,这个优先级等级依赖于具体的操作系统。 对于一般应用来说,保持默认的优先级,就可以了。

同步锁等待导致的饥饿问题

Java中的synchronized块也会导致饥饿问题。 Java的synchronized并不保障等待线程的顺序(锁释放后,随机竞争,情况比较复杂,和OS调度有关)。 这就会存在一种可能,某个线程运气极差,每次抢锁都没抢到。 而且新的线程还在不断地进入等锁的队列,从而导致这个线程就几乎是一直处于等待中。 这就使得这个“悲催”的线程就处于“饥饿”中,而且还会悲惨的“饿死”。

条件队列导致的饥饿问题

我们知道notify()方法会唤醒对象条件队列中等待的某个线程,但是,可惜这个唤醒是无序的(和VM调度,OS调度有关,甚至底层是随机选取一个,更甚至就是队列中的第一个)。 而如果,条件队列不断有新的线程进入,或者在唤醒的那一刻,刚好有其他线程抢入,那都可能导致某个运气不好的线程迟迟不能被唤醒。 对这个线程来说,就是进入一种“饥饿”的状态,甚至还会有“饿死”的风险。

Java中的公平性

首先,要明确一点。在Java中要想做到100%的公平性基本是不可能的(VM,OS都会有自己的调度策略;抢入等情况也会导致不同的情况发生),所以,我们只能说尽可能的提高同步机制中线程之间的公平性。

来看一个简单的同步块代码示例:

public class Synchronizer{

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

}

如果有多个线程调用doSynchronized()方法,没有获得monitor锁的线程就会进入阻塞等待。 但是阻塞等待的线程谁会成为下一个获得锁的线程,这谁也说不准。

使用显式Lock锁替代synchronized内部锁

对于Java的synchronized实际上是一个内部的monitor锁。对于这个内部锁,我们无法做更多的控制(不过也有一个好处,不用手动释放锁)。 而如果想要对于锁进行更多更细节的控制,则可以使用Lock类。例如:

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,不过要注意一定要记得释放锁(这个只是个简单的例子,实际使用中应该使用finally来释放锁)。

Lock是一个接口,Java提供了几种实现。不过,我们可以先自己来个简单的时间。例如:

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()方法使用了一个信号通知的标准范式(最后补充中会提到)。 这种实现,其实是将锁等待转换成了Lock实例对象的条件等待(等待转移)。不过,这个实现是可以完成加锁/解锁/等待等功能。

(不过,还是要记住这只是个简单的示例,有些问题进行了简化。比如,Lock的条件队列不仅仅是给`isLocked`的条件谓词使用的,也就是说外部其他线程也可以直接调用Lock实例对象的`wait()`方法。)

我们再回头看看doSynchronized()方法,要注意,doSynchronized()加锁之后,会执行一个非常耗时的逻辑,然后才会释放锁。 这就意味这,锁等待的时间可能会非常长。

前面也介绍过同步块的等待顺序是无法保障的。而notify()也是无法保障唤醒顺序的。 因此,这个Lock实现是一个无法保障顺序的非公平锁。

公平锁

接下来,我们把Lock改造成一个公平锁。 主要是针对同步策略以及wait()/notify()的使用上。

这里会使用几个知识点:

我们让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()方法不再直接使用synchronized。 而是只在有必要的地方使用内部synchronized代码块。

FairLock会让每一次调用lock(),创建一个QueueObject对象,并放入队列中。通过QueueObject对象来完成wait()/notify()。而且这样,由于QueueObject上等待的,只有创建这个对象的线程,所以唤醒时不存在公平性的问题。 而多个QueueObject会进入一个有序的集合,这样FairLock就实现了公平锁的特征。

有一点要注意,所有有关锁的状态(isLockedlockingThread)的判断都是在同一个synchronized块中,这样避免了条件失效的问题。

另外,QueueObject作为一个信号使用,它的doWait()doNotify()方法完成信号机制。 这里doWait()方法,也是使用了一个信号处理范式来避免信号丢失问题。

最后,注意queueObject.doWait()方法是需要在一个try - catch块中使用。 当InterruptedException异常时(锁等待的线程被中断了),能够将对应的QueueObject移出队列。这一步,很重要。否则,队列中第一个QueueObject是一个已经废弃的等待,那就导致这个锁没有机会得到释放。

性能问题

从上面的代码,就能看出公平锁要比一个非公平锁要复杂的多。这也会导致公平锁的性能比较低。 在使用时,需要考虑临界区中任务逻辑执行的时间,是否值得使用公平锁。 另外,还要考虑竞争的激烈程度,是否需要使用公平锁。


补充几点

  • 条件队列(信号处理)的使用范式
synchronized(lock){
    while ( !conditionPredicate() )
        lock.wait();
}
  • 公平/非公平几点对比

    • 非公平
      • 默认
      • 吞吐量好
      • 各线程表现差异大
      • 闯入锁
        • 让闯入者获得锁继续运行,比唤醒等待线程,再让等待线程开始工作要快的多
    • 公平
      • 伸缩性好
      • 因挂起/重新开始线程的代价带了巨大的性能开销
        • 为了维护等待线程的公平调度
  • 显式锁使用时的注意事项

    • Lock 的实现必须提供具有与内部加锁(monitor)相同的内存可见性的语义
    • 需要确保显式的释放锁
      • finally
    • Java 6 引入偏向锁,平均来说和内部锁的优势不再那么的大了,但在极端情况下仍然占有一定的优势
    • 在Java 5 中,线程转储无法体现
    • 难以被JIT优化
      • 锁省略
      • 粗化锁

© 著作权归作者所有

共有 人打赏支持
秋雨霏霏
粉丝 150
博文 94
码字总数 168411
作品 0
杭州
CTO(技术副总裁)
私信 提问
18、Java并发性和多线程-饥饿与公平

以下内容转自http://ifeve.com/starvation-and-fairness/: 如果一个线程因为CPU时间全部被其他线程抢走而得不到CPU运行时间,这种状态被称之为“饥饿”。而该线程被“饥饿致死”正是因为它得...

easonjim
2017/06/16
0
0
ReentrantLock(重入锁)功能详解和应用演示

1. ReentrantLock简介 jdk中独占锁的实现除了使用关键字synchronized外,还可以使用ReentrantLock。虽然在性能上ReentrantLock和synchronized没有什么区别,但ReentrantLock相比synchronized而...

takumiCX
07/19
0
0
15《Java核心技术》之synchronized和ReentrantLock有什么区别呢?

一、提出问题 从今天开始,我们将进入 Java 并发学习阶段。软件并发已经成为现代软件开发的基础能力,而 Java 精心设计的高效并发机制,正是构建大规模应用的基础之一,所以考察并发基本功也...

飞鱼说编程
11/05
0
0
多线程并发相关的几个重要基础知识点解析

问:volatile 变量和 atomic 变量有什么不同? 答:volatile 变量和 atomic 变量看起来很像,但功能却不一样。 volatile 变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不...

Java填坑之路
08/08
0
0
并行开发的基本概念及两个重要的定律

并行开发的基本概念: 同步(Synchronous)和异步(Asynchronous) 同步和异步方法的区别

Panda_Jerry
2017/11/22
0
0

没有更多内容

加载失败,请刷新页面

加载更多

windows上类似dnsmasq的软件Dual DHCP DNS Server

官网地址:http://dhcp-dns-server.sourceforge.net/官网定向的下载地址:https://sourceforge.net/projects/dhcp-dns-server/files/ 设置参考地址:http://blog.51cto.com/zhukeqiang/18264......

xueyuse0012
15分钟前
0
0
LinkedHashMap源码解析

前言 HashMap中的元素时无序的,也就是说遍历HashMap的时候,顺序和放入的顺序是不一样的。 如果需要有序的Map,就可以采用LinkedHashMap. LinkedHashMap通过维护一个包含所有元素的双向链表,...

grace_233
23分钟前
1
0
初识flask

文档 0.10.1版本 http://www.pythondoc.com/flask/index.html 1.0.2版本 https://dormousehole.readthedocs.io/en/latest/ 安装flask $ pip3 install flaskCollecting flask Downloading......

yimingkeji
昨天
2
0
Akka系统《sixteen》译

Actor是一个封装状态(state)和行为(behavior)的对象,它们只通过交换消息通信(放入收件人邮箱的邮件)。从某种意义上说,Actor是最严格的面向对象编程形式,但它更适合将他们视为人:在与Act...

woshixin
昨天
1
0
技术工坊|如何开发一款以太坊钱包(深圳)

【好消息!】HiBlock区块链技术工坊已经成功举办了26期,其中北京1期,西安1期,成都2期,上海22期。经常有社区的小伙伴问定期举办技术工坊的除了上海以外,其他城市有没有?现在区块链技术工...

HiBlock
昨天
1
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部