饥饿和公平

原文地址  By Jakob Jenkov  翻译 Simon-SZ  校对:方腾飞

如果一个线程因为CPU时间全部被其他线程抢走而得不到CPU运行时间,这种状态被称之为“饥饿”。而该线程被“饥饿致死”正是因为它得不到CPU运行时间的机会。解决饥饿的方案被称之为“公平性” – 即所有线程均能公平地获得运行机会。

 下面是本文讨论的主题:

1. Java中导致饥饿的原因:

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

2. 在Java中实现公平性方案,需要:

  • 使用锁,而不是同步块。
  • 公平锁。
  • 注意性能方面。

Java中导致饥饿的原因

在Java中,下面三个常见的原因会导致线程饥饿:

  1. 高优先级线程吞噬所有的低优先级线程的CPU时间。
  2. 线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。
  3. 线程在等待一个本身(在其上调用wait())也处于永久等待完成的对象,因为其他线程总是被持续地获得唤醒。

高优先级线程吞噬所有的低优先级线程的CPU时间

你能为每个线程设置独自的线程优先级,优先级越高的线程获得的CPU时间越多,线程优先级值设置在1到10之间,而这些优先级值所表示行为的准确解释则依赖于你的应用运行平台。对大多数应用来说,你最好是不要改变其优先级值。

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

Java的同步代码区也是一个导致饥饿的因素。Java的同步代码区对哪个线程允许进入的次序没有任何保障。这就意味着理论上存在一个试图进入该同步区的线程处于被永久堵塞的风险,因为其他线程总是能持续地先于它获得访问,这即是“饥饿”问题,而一个线程被“饥饿致死”正是因为它得不到CPU运行时间的机会。

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

如果多个线程处在wait()方法执行上,而对其调用notify()不会保证哪一个线程会获得唤醒,任何线程都有可能处于继续等待的状态。因此存在这样一个风险:一个等待线程从来得不到唤醒,因为其他等待线程总是能被获得唤醒。

在Java中实现公平性

虽Java不可能实现100%的公平性,我们依然可以通过同步结构在线程间实现公平性的提高。

首先来学习一段简单的同步态代码:

查看源代码

打印帮助

1 public class Synchronizer{
2  
3     public synchronized void doSynchronized(){
4  
5     //do a lot of work which takes a long time
6  
7     }
8 }

如果有一个以上的线程调用doSynchronized()方法,在第一个获得访问的线程未完成前,其他线程将一直处于阻塞状态,而且在这种多线程被阻塞的场景下,接下来将是哪个线程获得访问是没有保障的。

使用锁方式替代同步块

为了提高等待线程的公平性,我们使用锁方式来替代同步块。

查看源代码

打印帮助

1 public class Synchronizer{
2     Lock lock = new Lock();
3     public void doSynchronized() throws InterruptedException{
4         this.lock.lock();
5         //critical section, do a lot of work which takes a long time
6         this.lock.unlock();
7     }
8 }

注意到doSynchronized()不再声明为synchronized,而是用lock.lock()和lock.unlock()来替代。

下面是用Lock类做的一个实现:

查看源代码

打印帮助

01 public class Lock{
02  
03     private boolean isLocked      = false;
04  
05     private Thread lockingThread = null;
06  
07     public synchronized void lock() throws InterruptedException{
08  
09     while(isLocked){
10  
11         wait();
12  
13     }
14  
15     isLocked = true;
16  
17     lockingThread = Thread.currentThread();
18  
19 }
20  
21 public synchronized void unlock(){
22  
23     if(this.lockingThread != Thread.currentThread()){
24  
25          throw new IllegalMonitorStateException(
26  
27               "Calling thread has not locked this lock");
28  
29          }
30  
31     isLocked = false;
32  
33     lockingThread = null;
34  
35     notify();
36  
37     }
38 }

注意到上面对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实例,所有其它的线程都将处于等待状态,直到他们处于队列头部。

查看源代码

打印帮助

01 public class FairLock {
02     private boolean           isLocked       = false;
03     private Thread            lockingThread  = null;
04     private List<QueueObject> waitingThreads =
05             new ArrayList<QueueObject>();
06  
07   public void lock() throws InterruptedException{
08     QueueObject queueObject           = new QueueObject();
09     boolean     isLockedForThisThread = true;
10     synchronized(this){
11         waitingThreads.add(queueObject);
12     }
13  
14     while(isLockedForThisThread){
15       synchronized(this){
16         isLockedForThisThread =
17             isLocked || waitingThreads.get(0) != queueObject;
18         if(!isLockedForThisThread){
19           isLocked = true;
20            waitingThreads.remove(queueObject);
21            lockingThread = Thread.currentThread();
22            return;
23          }
24       }
25       try{
26         queueObject.doWait();
27       }catch(InterruptedException e){
28         synchronized(this) { waitingThreads.remove(queueObject); }
29         throw e;
30       }
31     }
32   }
33  
34   public synchronized void unlock(){
35     if(this.lockingThread != Thread.currentThread()){
36       throw new IllegalMonitorStateException(
37         "Calling thread has not locked this lock");
38     }
39     isLocked      = false;
40     lockingThread = null;
41     if(waitingThreads.size() > 0){
42       waitingThreads.get(0).doNotify();
43     }
44   }
45 }

 

查看源代码

打印帮助

01 public class QueueObject {
02  
03     private boolean isNotified = false;
04  
05     public synchronized void doWait() throws InterruptedException {
06  
07     while(!isNotified){
08         this.wait();
09     }
10  
11     this.isNotified = false;
12  
13 }
14  
15 public synchronized void doNotify() {
16     this.isNotified = true;
17     this.notify();
18 }
19  
20 public boolean equals(Object o) {
21     return this == o;
22 }
23  
24 }

首先注意到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带来的负担影响就越小,当然这也和代码执行的频繁度相关。 

时间: 2024-10-08 12:42:52

饥饿和公平的相关文章

Slipped Conditions

原文链接 作者:Jakob Jenkov 译者:余绍亮 校对:丁一 所谓Slipped conditions,就是说, 从一个线程检查某一特定条件到该线程操作此条件期间,这个条件已经被其它线程改变,导致第一个线程在该条件上执行了错误的操作.这里有一个简单的例子: 01 public class Lock { 02     private boolean isLocked = true; 03   04     public void lock(){ 05       synchronized(t

Java中的锁

原文链接 作者:Jakob Jenkov 译者:申章 校对:丁一 锁像synchronized同步块一样,是一种线程同步机制,但比Java中的synchronized同步块更复杂.因为锁(以及其它更高级的线程同步机制)是由synchronized同步块的方式实现的,所以我们还不能完全摆脱synchronized关键字(译者注:这说的是Java 5之前的情况). 自Java 5开始,java.util.concurrent.locks包中包含了一些锁的实现,因此你不用去实现自己的锁了.但是你仍然需

剖析同步器

原文链接 作者:Jakob Jenkov 译者:丁一 虽然许多同步器(如锁,信号量,阻塞队列等)功能上各不相同,但它们的内部设计上却差别不大.换句话说,它们内部的的基础部分是相同(或相似)的.了解这些基础部件能在设计同步器的时候给我们大大的帮助.这就是本文要细说的内容. 注:本文的内容是哥本哈根信息技术大学一个由Jakob Jenkov,Toke Johansen和Lars Bjørn参与的M.Sc.学生项目的部分成果.在此项目期间我们咨询Doug Lea是否知道类似的研究.有趣的是在开发Jav

Java并发性和多线程介绍目录

Java并发性和多线程介绍 多线程的优点 多线程的代价 并发编程模型 如何创建并运行java线程 竞态条件与临界区 线程安全与共享资源 线程安全及不可变性 Java内存模型 JAVA同步块 线程通信 Java ThreadLocal Thread Signaling (未翻译) 死锁 避免死锁 饥饿和公平 嵌套管程锁死 Slipped Conditions Java中的锁 Java中的读/写锁 重入锁死 信号量 阻塞队列 线程池 CAS 剖析同步器 无阻塞算法 阿姆达尔定律 文章转自 并发编程网

小米式饥饿营销成主流是社会倒退

随着以苹果和小米.乐视为代表的科技公司颠覆传统制造业的销售模式,饥饿营销获得空前的成功,很多公司开始争相效仿,地产,汽车等行业也在打造饥饿营销,随着饥饿营销的泛滥,消费者的吐槽和抱怨也越来越多.从群效应导致饥饿营销成为营销主流,饥饿营销过度和虚假宣传必然导致社会倒退! 这并不耸人听闻!这是事实.因为它极大增加了消费者的成本,造成巨大的社会资源浪费,多数公司在实施饥饿营销时,控制不当,对消费者的感情造成严重的伤害. 从某种程度上来说,"饥饿营销"是一次"欺骗"消费者的

从移动电源危机看小米的“饥饿营销”

中介交易 SEO诊断 淘宝客 云主机 技术大厅 移动电源的质量问题一直备受关注,最近国家质检总局对全国移动电源一次检测中,采样32批次产品.224件样品,包括近几年大红大紫的小米公司产品,竟然全部存在质量问题.作为日常用品,质量得不到保障,谁该为此负责? 移动电源质量门 质检总局称,抽检的移动电源主要是存在"容量虚标"."输出电压过低或过高"."塑料外壳不阻燃"以及"重物冲击可能短路自燃"等安全风险. 7个批次的电芯容量,即电

口碑网李治国:以公平的排序方式立足于整个市场

本地搜索是本地化生活社区的重要工具,贯穿分类信息和生活黄页等内容,在美国已占有1/4以上搜索市场,在韩国已经超过30%市场,但在中国,这个领域刚刚起步. 2004年6月8日,李治国先生和其他几位创业团队成员创办了口碑网,现有会员已经超过300万.口碑网致力于做百姓的生活好向导,目前已是中国最大的本地化"吃.住.玩"生活社区之一.近日艾瑞网独家专访了口碑网CEO李治国,请他分享本地搜索网站的运营经验. 广告是目前最简单也是最大的市场 艾瑞网:首先想请您谈谈对未来盈利方式的一些看法? 李治

PS合成实例:饥饿的鲸鱼跃出水面抢夺食物

本教程学习如何用Photoshop合成饥饿的鲸鱼跃出水面抢夺海鸥食物的场景,主要介绍了通道,图层蒙版,笔刷和调整层等工具命令的综合使用. 先看效果图 首先打开一幅挪威海域图片 建新层,起名漩涡,滤镜>渲染>云彩,之后滤镜>扭曲>极坐标 Ctrl+T垂直方向压扁些,位置如下 添加蒙版去除多余部分,图层模式柔光,效果如下 漩涡层用曲线加暗一些 建新层,导入瀑布笔刷,前景色白色在漩涡中心点一下 图层模式改为柔光.导入裂缝素材,ctrl+T变形如下 [1] [2] [3]  下一页

编写“公平”的ASP图形计数器

"技术天地"中的<编写ASP图形计数器>一文,详细的说明了如何利用流行的ASP来编写计数器.但是,美中不足的是,如果某个用户反复点击"刷新"按钮,那么计数器还是要不断的增加的,这对网站点击率评比来说是不公平的,也失去了计数器做为正常统计功能的作用.如何在技术上避免这种情况的发生呢? 我认为要防止上网用户连续按下"刷新"计数器也连续增加的问题,最好的办法就是利用ASP的Session对象,我们可以借助Session对象首先判断该用户是否