文档章节

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

秋雨霏霏
 秋雨霏霏
发布于 2017/09/10 20:20
字数 2329
阅读 133
收藏 0
点赞 0
评论 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优化
      • 锁省略
      • 粗化锁

© 著作权归作者所有

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

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

easonjim
2017/06/16
0
0
再一次理解ReentrantLock

1. ReentrantLock的介绍 ReentrantLock重入锁,是实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不...

你听___
2017/11/07
0
0
自旋锁的枝枝蔓蔓

近期在读《多处理器编程的艺术》一书,英文版的,最近发现技术类书籍中文翻译版的质量越来越差,译者很多都是单纯为了钱,其实他们在IT领域很多都是大白,因此还是看原汁原味的英文版吧,虽然...

晨曦之光
2012/04/10
58
0
深入学习Lock锁(3)——重入锁ReentrantLock

1.简介 重入锁ReentrantLock,顾名思义,就是支持重进入的锁,它表示该锁能够支持一个线程对资源的重复加锁。除此之外,该锁还支持获取锁时的公平和非公平性选择。 所谓重复加锁,就是某个线...

江左煤郎
05/24
0
0
并行开发的基本概念及两个重要的定律

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

Panda_Jerry
2017/11/22
0
0
【19】Java中的Locks

原文地址:Locks in Java 作者: Jakob Jenkov --- Lock是一个类似同步代码块(synchronized block)的线程同步机制。同步代码块而言,Lock可以做到更细粒度的控制。 Lock(或者其他高级同步...

秋雨霏霏
2017/10/26
0
0
【Java并发性和多线程】Java中的锁

本文为转载学习 原文链接:http://ifeve.com/locks/ 锁像synchronized同步块一样,是一种线程同步机制,但比Java中的synchronized同步块更复杂。因为锁(以及其它更高级的线程同步机制)是由...

heroShane
2014/02/02
0
0
Java并发库(Java Concurrency)

原文地址 译文地址 译者:张坤等 Java并发性和多线程介绍(Java Concurrency / Multithreading Tutorial) 多线程的优点(Multithreading Benefits) 多线程的代价(Multithreading Costs) ...

暗之幻影
2016/12/17
19
0
CFS调度器的精彩--任何事情都是一种权衡

还记得曾经写过一篇叫做《至今不敢写一篇cfs的文章》,那时我只是默默地欣赏cfs的和谐,可是一些转瞬即逝的感悟不写出来会是很大的遗憾,其实也谈不上什么感悟,只是理解罢了,有时你瞬间领悟...

晨曦之光
2012/04/10
229
0
21、Java并发性和多线程-Java中的锁

以下内容转自http://ifeve.com/locks/: 锁像synchronized同步块一样,是一种线程同步机制,但比Java中的synchronized同步块更复杂。因为锁(以及其它更高级的线程同步机制)是由synchronize...

easonjim
2017/06/16
0
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

CoreText进阶(七)-添加自定义View和对其

CoreText进阶(七)-添加自定义View和对其 其它文章: CoreText 入门(一)-文本绘制 CoreText入门(二)-绘制图片 CoreText进阶(三)-事件处理 CoreText进阶(四)-文字行数限制和显示更多...

aron1992
14分钟前
0
0
Python爬虫 爬取百合网的女人们和男人们

学Python也有段时间了,目前学到了Python的类。个人感觉Python的类不应称之为类,而应称之为数据类型,只是数据类型而已!只是数据类型而已!只是数据类型而已!重要的事情说三篇。 据书上说...

p柯西
26分钟前
0
0
在Java中,你真的会日期转换吗

1.什么是SimpleDateFormat 在java doc对SimpleDateFormat的解释如下: SimpleDateFormatis a concrete class for formatting and parsing dates in a locale-sensitive manner. It allows fo......

Java小铺
34分钟前
0
0
Linux系统梳理---系统搭建(二):tomcat的安装和使用

上一章讲到JDK的安装使用,这一章主要记录下服务器tomcat的安装以及部署一个项目. 1.下载tomcat,这里下载的是apache-tomcat-8.5.32.tar.gz 2.创建文件夹,便于管理,和JDK一样,在usr目录下创建t...

勤奋的蚂蚁
45分钟前
0
0
ES15-聚合

1.Terms Aggregation 分组聚合 2.Filter Aggregation 过滤聚合

贾峰uk
46分钟前
0
0
【2018.07.19学习笔记】【linux高级知识 20.27-20.30】

20.27 分发系统介绍 20.28 expect脚本远程登录 20.29 expect脚本远程执行命令 20.30 expect脚本传递参数

lgsxp
49分钟前
0
0
10.32/10.33 rsync通过服务同步~10.35 screen工具

通过服务的方式同步要编辑配置文件:[root@linux-xl ~]# vim /etc/rsyncd.confport=873log file=/var/log/rsync.logpid file=/var/run/rsyncd.pidaddress=192.168.43.21[tes...

洗香香
52分钟前
0
0
与女儿谈商业模式 (3):沃尔玛的成功模式

分类:与女儿谈商业模式 | 标签: 经济学 沃尔玛 陈志武 2007-05-10 09:09阅读(11279)评论(30) 与女儿谈商业模式 (3):沃尔玛的成功模式 陈志武 /文 沃尔玛(Wal-Mart)是另一个有意思的财...

祖冲之
58分钟前
0
0
网页加载速度优化方法总结

1、减少请求 最大的性能漏洞就是一个页面需要发起几十个网络请求来获取诸如样式表、脚本或者图片这样的资源,这个在相对低带宽和高延迟的移动设备连接上来说影响更严重。 2、整合资源 对开发...

Jack088
今天
0
0
dubbo学习

https://blog.csdn.net/houshaolin/article/details/76408399

喵五郎
今天
0
0

没有更多内容

加载失败,请刷新页面

加载更多

下一页

返回顶部
顶部