饥饿发生的原因:

高优先级的线程占用了大部分的cpu时间,低优先级线程发生饥饿

线程被永久堵塞在一个等待进入同步块的状态

线程在等待一个本身(在其上调用wait())也处于永久等待完成的对象

java中实现公平锁

使用锁而不是同步块

公平锁

如果一个线程的cpu执行时间都被其他线程抢占了,导致得不到cpu执行,这种情况就叫做“饥饿”,这个线程就会出现饥饿致死的现象,因为永远无法得到cpu的执行。解决饥饿现象的方法就是实现公平,保证所有线程都公平的获得执行的机会。

java中发生线程饥饿的原因

高优先级的线程占用了大部分的cpu时间,低优先级线程发生饥饿

线程被永久堵塞在一个等待进入同步块的状态

线程在等待一个本身(在其上调用wait())也处于永久等待完成的对象

高优先级的线程占用了大部分的cpu时间,低优先级线程发生饥饿

你可以给每个线程单独的设置优先级。优先级越高,就会获得越高的cpu执行的机会。

线程被永久堵塞在一个等待进入同步块的状态

java 的synchronize语句块不保证线程进入语句块的顺序,所以这就存在一个可能的问题,有一个线程一直阻塞在synchronize语句块,永远都无法进入synchronize。

线程在等待一个本身(在其上调用wait())也处于永久等待完成的对象

同样的,类似synchronize,notify也不保证线程被唤醒的顺序。所以也存在一个风险,就是一个wait的线程一直处于wait的状态,永远也没有被notify所唤醒。

java中实现公平锁

虽然无法实现完全100%公平,但是我们仍然可以尽可能的提高线程的公平性。

首先,我们研究一个简单的synchronize语句块:

public class Synchronizer{

public synchronized void doSynchronized(){

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

}

}

如果超过一个线程调用这个方法,那么只有一个线程可以进入这个方法执行,其他线程都必须等待,直到该线程退出。而且我们无法知道下一个进入synchronize的语句块的线程会是那一个

使用lock而不是synchronize

为了增加等待线程的公平性,我们可以用lock来替换synchronize

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类的一个简单的实现如下:

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()方法的访问上。另外,如果锁已经锁上(校对注:这里指的是isLocked等于true时),这些线程将阻塞在while(isLocked)循环的wait()调用里面。要记住的是,当线程正在等待进入lock() 时,可以调用wait()释放其锁实例对应的同步锁,使得其他多个线程可以进入lock()方法,并调用wait()方法。

这回看下doSynchronized(),你会注意到在lock()和unlock()之间的注释:在这两个调用之间的代码将运行很长一段时间。进一步设想,这段代码将长时间运行,和进入lock()并调用wait()来比较的话。这意味着大部分时间用在等待进入锁和进入临界区的过程是用在wait()的等待中,而不是被阻塞在试图进入lock()方法中。

在早些时候提到过,同步块不会对等待进入的多个线程谁能获得访问做任何保障,同样当调用notify()时,wait()也不会做保障一定能唤醒线程因此这个版本的Lock类和doSynchronized()那个版本就保障公平性而言,没有任何区别。

但我们能改变这种情况。当前的Lock类版本调用自己的wait()方法,** 如果每个线程在不同的对象上调用wait(),那么只有一个线程会在该对象上调用wait(),Lock类可以决定哪个对象能对其调用notify(),因此能做到有效的选择唤醒哪个线程。**

实际上这就是公平锁的实现思想

公平锁

下面来讲述将上面Lock类转变为公平锁FairLock。你会注意到新的实现和之前的Lock类中的同步和wait()/notify()稍有不同。

准确地说如何从之前的Lock类做到公平锁的设计是一个渐进设计的过程,每一步都是在解决上一步的问题而前进的:Nested Monitor Lockout, Slipped Conditions和Missed Signals。这些本身的讨论虽已超出本文的范围,但其中每一步的内容都将会专题进行讨论。重要的是,每一个调用lock()的线程都会进入一个队列,当解锁后,只有队列里的第一个线程被允许锁住Farlock实例,所有其它的线程都将处于等待状态,直到他们处于队列头部。

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,取而代之的是对必需同步的代码,在synchronized中进行嵌套。

FairLock新创建了一个QueueObject的实例,并对每个调用lock()的线程进行入队列。调用unlock()的线程将从队列头部获取QueueObject,并对其调用doNotify(),以唤醒在该对象上等待的线程。通过这种方式,在同一时间仅有一个等待线程获得唤醒,而不是所有的等待线程。这也是实现FairLock公平性的核心所在。

请注意,在同一个同步块中,锁状态依然被检查和设置,以避免出现滑漏条件。

还需注意到,QueueObject实际是一个semaphore。doWait()和doNotify()方法在QueueObject中保存着信号。这样做以避免一个线程在调用queueObject.doWait()之前被另一个调用unlock()并随之调用queueObject.doNotify()的线程重入,从而导致信号丢失。queueObject.doWait()调用放置在synchronized(this)块之外,以避免被monitor嵌套锁死,所以另外的线程可以解锁,只要当没有线程在lock方法的synchronized(this)块中执行即可。

最后,注意到queueObject.doWait()在try – catch块中是怎样调用的。在InterruptedException抛出的情况下,线程得以离开lock(),并需让它从队列中移除。

性能考虑

如果比较Lock和FairLock类,你会注意到在FairLock类中lock()和unlock()还有更多需要深入的地方。这些额外的代码会导致FairLock的同步机制实现比Lock要稍微慢些。究竟存在多少影响,还依赖于应用在FairLock临界区执行的时长。执行时长越大,FairLock带来的负担影响就越小,当然这也和代码执行的频繁度相关。

java 饥饿现象,Java并发之“饥饿”和“公平锁”(Starvation and Fairness)相关推荐

  1. JAVA基础 | 一张图读懂非公平锁与公平锁

    在Java并发编程中,公平锁与非公平锁是很常见的概念,ReentrantLock.ReadWriteLock默认都是非公平模式,非公平锁的效率为何高于公平锁呢?究竟公平与非公平有何区别呢? 首先先简单 ...

  2. java 饥饿现象,Java单例模式、饥饿模式代码实例

    class MyThreadScopeData { // 单例 private MyThreadScopeData() { } // 提供获取实例方法 public static synchroniz ...

  3. Java进阶:ReentrantLock实现原理解析(公平锁、非公平锁、可重入锁、自旋锁)

    概述 本篇将介绍公平锁.非公平锁.可重入锁.自旋锁相关理论知识,同时结合相关源码和Demo进行解析,主要是以ReentrantLock作为例子. 公平锁 公平锁定义 公平锁是指线程按照申请所的顺序来获 ...

  4. java投票锁_Java并发编程锁之独占公平锁与非公平锁比较

    Java并发编程锁之独占公平锁与非公平锁比较 公平锁和非公平锁理解: 在上一篇文章中,我们知道了非公平锁.其实Java中还存在着公平锁呢.公平二字怎么理解呢?和我们现实理解是一样的.大家去排队本着先来 ...

  5. java 共享锁 独占锁_Java并发编程锁之独占公平锁与非公平锁比较

    Java并发编程锁之独占公平锁与非公平锁比较 公平锁和非公平锁理解: 在上一篇文章中,我们知道了非公平锁.其实Java中还存在着公平锁呢.公平二字怎么理解呢?和我们现实理解是一样的.大家取排队本着先来 ...

  6. Java多线程——线程池的饥饿现象

    概述 定长线程池的使用过程中会存在饥饿现象,也就是当多线程情况下,当池中所有线程都被占用后,被占用的线程又需要空闲线程去进行下一步的操作,此时又获取不到池中空闲的线程,此时就出现了饥饿现象. 示例 p ...

  7. java线程饥饿原理_java 多线程饥饿现象的问题解决方法

    java 多线程饥饿现象的问题解决方法 当有线程正在读的时候,不允许写 线程写,但是允许其他的读线程进行读.有写线程正在写的时候,其他的线程不应该读写.为了防止写线程出现饥饿现象,当线程正在读,如果写 ...

  8. java线程饥饿原理,Java线程饥饿和锁的公平性「译」

    一个线程因为被其它线程抢占了而分配不到时间片,这就是[饥饿].这个线程[饿的要死]因为只有别的线程可以得到CPU时间片,就它得不到.解决饥饿的方法叫着公平性--所有的线程都有机会得到运行. 线程饥饿的 ...

  9. 线程的死锁、活锁和饥饿现象

    目录 1.死锁 2.活锁 3.饥饿 一个资源应该单独使用一把锁. 比如,一个对象中有多个共享资源,但有多个线程需要使用其中的不同资源 此时如果把对象整体作为一把锁,那并发就很低. 可以考虑,把每个共享 ...

最新文章

  1. 如何设置网页自动刷新(JSP,JS,HTML)
  2. 第80节:Java中的MVC设计模式
  3. c char*转int_C语言中的char类型也有signed和unsigned?字符也有正负之分吗?
  4. Mysql (21)---连接的使用
  5. 关于python变量_关于python变量练习题
  6. php 单例 重连,PHP单例模式详解
  7. HDU 4278 卡特兰,区间DP
  8. 多因子量化投资模型策略深度研究
  9. NOIP2016普及组T1(买铅笔)题解
  10. MongodB数据库安装教程
  11. 免费的pdf编辑软件
  12. c语言股票最大收益_C语言买卖股票问题
  13. 室内外无缝定位导航,GPS系统可以实现吗?
  14. 新浪UC 单文件精简版
  15. Tasteless challenges hard WP
  16. moles-packer_Hashicorp的Packer-是否适合PHP开发人员?
  17. 高端运动耳机哪个品牌最好、最好的运动耳机品牌排行
  18. linux 对函数的未定义的引用,对libncurses中函数的未定义引用
  19. 计算机网络发展及相关概念
  20. ffmpeg源码中ffplay音视频同步原理及实现

热门文章

  1. 小H超级推广王更新升级有哪些变化
  2. CRM 365资源管理器 V1.0 发布
  3. Android 客户端与服务器端交互实现登录功能
  4. 在Windows上开发ROS的正确姿势
  5. mysql check约束 date_MySQL怎么使用check约束
  6. a33 linux 硬解码_全志A33 lichee 开发板 Linux中断编程原理说明
  7. 人脸对齐之GBDT(ERT)算法解读
  8. Golang + Swift 5,我们想认真做一款好的表情包工具
  9. 5G协议栈NGFI接口
  10. 创新实训工作记录2018-5-31