Slipped Conditions

原文链接 作者:Jakob Jenkov 译者:余绍亮 校对:丁一

所谓Slipped conditions,就是说, 从一个线程检查某一特定条件到该线程操作此条件期间,这个条件已经被其它线程改变,导致第一个线程在该条件上执行了错误的操作。这里有一个简单的例子:

01 public class Lock {
02     private boolean isLocked = true;
03  
04     public void lock(){
05       synchronized(this){
06         while(isLocked){
07           try{
08             this.wait();
09           } catch(InterruptedException e){
10             //do nothing, keep waiting
11           }
12         }
13       }
14  
15       synchronized(this){
16         isLocked = true;
17       }
18     }
19  
20     public synchronized void unlock(){
21       isLocked = false;
22       this.notify();
23     }
24 }

我们可以看到,lock()方法包含了两个同步块。第一个同步块执行wait操作直到isLocked变为false才退出,第二个同步块将isLocked置为true,以此来锁住这个Lock实例避免其它线程通过lock()方法。

我们可以设想一下,假如在某个时刻isLocked为false, 这个时候,有两个线程同时访问lock方法。如果第一个线程先进入第一个同步块,这个时候它会发现isLocked为false,若此时允许第二个线程执行,它也进入第一个同步块,同样发现isLocked是false。现在两个线程都检查了这个条件为false,然后它们都会继续进入第二个同步块中并设置isLocked为true。

这个场景就是slipped conditions的例子,两个线程检查同一个条件, 然后退出同步块,因此在这两个线程改变条件之前,就允许其它线程来检查这个条件。换句话说,条件被某个线程检查到该条件被此线程改变期间,这个条件已经被其它线程改变过了。

为避免slipped conditions,条件的检查与设置必须是原子的,也就是说,在第一个线程检查和设置条件期间,不会有其它线程检查这个条件。

解决上面问题的方法很简单,只是简单的把isLocked = true这行代码移到第一个同步块中,放在while循环后面即可:

01 public class Lock {
02     private boolean isLocked = true;
03  
04     public void lock(){
05       synchronized(this){
06         while(isLocked){
07           try{
08             this.wait();
09           } catch(InterruptedException e){
10             //do nothing, keep waiting
11           }
12         }
13         isLocked = true;
14       }
15     }
16  
17     public synchronized void unlock(){
18       isLocked = false;
19       this.notify();
20     }
21 }

现在检查和设置isLocked条件是在同一个同步块中原子地执行了。

一个更现实的例子

也许你会说,我才不可能写这么挫的代码,还觉得slipped conditions是个相当理论的问题。但是第一个简单的例子只是用来更好的展示slipped conditions。

饥饿和公平中实现的公平锁也许是个更现实的例子。再看下嵌套管程锁死中那个幼稚的实现,如果我们试图解决其中的嵌套管程锁死问题,很容易产生slipped conditions问题。 首先让我们看下嵌套管程锁死中的例子:

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

我们可以看到synchronized(queueObject)及其中的queueObject.wait()调用是嵌在synchronized(this)块里面的,这会导致嵌套管程锁死问题。为避免这个问题,我们必须将synchronized(queueObject)块移出synchronized(this)块。移出来之后的代码可能是这样的:

01 //Fair Lock implementation with slipped conditions problem
02 public class FairLock {
03   private boolean isLocked = false;
04   private Thread lockingThread  = null;
05   private List waitingThreads =
06             new ArrayList();
07  
08   public void lock() throws InterruptedException{
09     QueueObject queueObject = new QueueObject();
10  
11     synchronized(this){
12       waitingThreads.add(queueObject);
13     }
14  
15     boolean mustWait = true;
16     while(mustWait){
17  
18       synchronized(this){
19         mustWait = isLocked || waitingThreads.get(0) != queueObject;
20       }
21  
22       synchronized(queueObject){
23         if(mustWait){
24           try{
25             queueObject.wait();
26           }catch(InterruptedException e){
27             waitingThreads.remove(queueObject);
28             throw e;
29           }
30         }
31       }
32     }
33  
34     synchronized(this){
35       waitingThreads.remove(queueObject);
36       isLocked = true;
37       lockingThread = Thread.currentThread();
38     }
39   }
40 }

注意:因为我只改动了lock()方法,这里只展现了lock方法。

现在lock()方法包含了3个同步块。

第一个,synchronized(this)块通过mustWait = isLocked || waitingThreads.get(0) != queueObject检查内部变量的值。

第二个,synchronized(queueObject)块检查线程是否需要等待。也有可能其它线程在这个时候已经解锁了,但我们暂时不考虑这个问题。我们就假设这个锁处在解锁状态,所以线程会立马退出synchronized(queueObject)块。

第三个,synchronized(this)块只会在mustWait为false的时候执行。它将isLocked重新设回true,然后离开lock()方法。

设想一下,在锁处于解锁状态时,如果有两个线程同时调用lock()方法会发生什么。首先,线程1会检查到isLocked为false,然后线程2同样检查到isLocked为false。接着,它们都不会等待,都会去设置isLocked为true。这就是slipped conditions的一个最好的例子。

解决Slipped Conditions问题

要解决上面例子中的slipped conditions问题,最后一个synchronized(this)块中的代码必须向上移到第一个同步块中。为适应这种变动,代码需要做点小改动。下面是改动过的代码:

01 //Fair Lock implementation without nested monitor lockout problem,
02 //but with missed signals problem.
03 public class FairLock {
04   private boolean isLocked = false;
05   private Thread lockingThread  = null;
06   private List waitingThreads =
07             new ArrayList();
08  
09   public void lock() throws InterruptedException{
10     QueueObject queueObject = new QueueObject();
11  
12     synchronized(this){
13       waitingThreads.add(queueObject);
14     }
15  
16     boolean mustWait = true;
17     while(mustWait){
18       synchronized(this){
19         mustWait = isLocked || waitingThreads.get(0) != queueObject;
20         if(!mustWait){
21           waitingThreads.remove(queueObject);
22           isLocked = true;
23           lockingThread = Thread.currentThread();
24           return;
25         }
26       }    
27  
28       synchronized(queueObject){
29         if(mustWait){
30           try{
31             queueObject.wait();
32           }catch(InterruptedException e){
33             waitingThreads.remove(queueObject);
34             throw e;
35           }
36         }
37       }
38     }
39   }
40 }

我们可以看到对局部变量mustWait的检查与赋值是在同一个同步块中完成的。还可以看到,即使在synchronized(this)块外面检查了mustWait,在while(mustWait)子句中,mustWait变量从来没有在synchronized(this)同步块外被赋值。当一个线程检查到mustWait是false的时候,它将自动设置内部的条件(isLocked),所以其它线程再来检查这个条件的时候,它们就会发现这个条件的值现在为true了。

synchronized(this)块中的return;语句不是必须的。这只是个小小的优化。如果一个线程肯定不会等待(即mustWait为false),那么就没必要让它进入到synchronized(queueObject)同步块中和执行if(mustWait)子句了。

细心的读者可能会注意到上面的公平锁实现仍然有可能丢失信号。设想一下,当该FairLock实例处于锁定状态时,有个线程来调用lock()方法。执行完第一个 synchronized(this)块后,mustWait变量的值为true。再设想一下调用lock()的线程是通过抢占式的,拥有锁的那个线程那个线程此时调用了unlock()方法,但是看下之前的unlock()的实现你会发现,它调用了queueObject.notify()。但是,因为lock()中的线程还没有来得及调用queueObject.wait(),所以queueObject.notify()调用也就没有作用了,信号就丢失掉了。如果调用lock()的线程在另一个线程调用queueObject.notify()之后调用queueObject.wait(),这个线程会一直阻塞到其它线程调用unlock方法为止,但这永远也不会发生。

公平锁实现的信号丢失问题在饥饿和公平一文中我们已有过讨论,把QueueObject转变成一个信号量,并提供两个方法:doWait()和doNotify()。这些方法会在QueueObject内部对信号进行存储和响应。用这种方式,即使doNotify()在doWait()之前调用,信号也不会丢失。 

时间: 2024-11-01 16:27:05

Slipped Conditions的相关文章

饥饿和公平

原文地址  By Jakob Jenkov  翻译 Simon-SZ  校对:方腾飞 如果一个线程因为CPU时间全部被其他线程抢走而得不到CPU运行时间,这种状态被称之为"饥饿".而该线程被"饥饿致死"正是因为它得不到CPU运行时间的机会.解决饥饿的方案被称之为"公平性" – 即所有线程均能公平地获得运行机会.  下面是本文讨论的主题: 1. Java中导致饥饿的原因: 高优先级线程吞噬所有的低优先级线程的CPU时间. 线程被永久堵塞在一个等待进

并发译文翻译计划(三)

为了促进并发编程在中国的推广和研究,让更多的同学能阅读到国外的文献.所以打算将国外的编程文献翻译成中文,但是我一个人的精力有限,所以希望征集译者帮忙一起翻译.这是一篇比较基础的文章,希望翻译后对新手有很大帮助. Introduction to Java Concurrency(译者:jiyou) Benefits (译者:古圣昌) Costs      (译者:古圣昌) Creating and Starting Threads(译者:阿里-章筱虎) Race Conditions and Cr

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

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

Do not wait until the conditions are perfect to begin. Beginning makes the conditions perfect(转)

  名言金句总是不嫌多,美国<公司>杂志(Inc.)列出让你在 2015 年受用无穷的十大金句,每天选一则当作一天的心灵指导,不只学习前人的精神和智慧,也能转化成工作和生活的动力!Cheers! 1. "Do not wait until the conditions are perfect to begin. Beginning makes the conditions perfect."--Alan Cohen 「别等到大环境好转再开始行动:行动才是让环境变好的原因.」

Usage of OPNET IT tool to Simulate and Test the Security of Cloud under varying Firewall conditions

Usage of OPNET IT tool to Simulate and Test the Security of Cloud under varying Firewall conditions GRADUATE PROJECT REPORT Ankush Veer Reddy Vee The main aim of this project is to evaluate the cloud security against the firewall implementation under

Conditions for C#

If current process is 64 bit?              returnIntPtr.Size == 8; If current OS has Wow64     function?            [DllImport("kernel32.dll", CharSet = CharSet.Unicode)]             public extern static IntPtrGetModuleHandle(string moduleName);

23.4. conditions if and case

表 23.1. 文件目录表达式 Primary 意义 [ -a FILE ] 如果 FILE 存在则为真. [ -b FILE ] 如果 FILE 存在且是一个块特殊文件则为真. [ -c FILE ] 如果 FILE 存在且是一个字特殊文件则为真. [ -d FILE ] 如果 FILE 存在且是一个目录则为真. [ -e FILE ] 如果 FILE 存在则为真. [ -f FILE ] 如果 FILE 存在且是一个普通文件则为真. [ -g FILE ] 如果 FILE 存在且已经设置了S

1.4. conditions if and case

表 1.1. 文件目录表达式 Primary 意义 [ -a FILE ] 如果 FILE 存在则为真. [ -b FILE ] 如果 FILE 存在且是一个块特殊文件则为真. [ -c FILE ] 如果 FILE 存在且是一个字特殊文件则为真. [ -d FILE ] 如果 FILE 存在且是一个目录则为真. [ -e FILE ] 如果 FILE 存在则为真. [ -f FILE ] 如果 FILE 存在且是一个普通文件则为真. [ -g FILE ] 如果 FILE 存在且已经设置了SG

php-java和PHP有纯中文的官方手册吗?还是只有部分的翻译?

问题描述 java和PHP有纯中文的官方手册吗?还是只有部分的翻译? java和PHP有纯中文的官方手册吗?还是只有部分的翻译?我英文不好,能不能学呢? 解决方案 http://cn2.php.net/manual/zh/index.php 解决方案二: java的JDK有中文版的API,PHP网上也有中文手册.现在网络资源这么丰富,只有肯上心,学东西还是很容易的. 选定一个完整的视频教程,跟着练习,上手是很容易的. 我就是2010年暑假跟着传智播客视频教程自学的Java,现在已经很熟练了.祝好