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

public class Lock {private boolean isLocked = true;public void lock(){synchronized(this){while(isLocked){try{this.wait();} catch(InterruptedException e){//do nothing, keep waiting}}}synchronized(this){isLocked = true;}}public synchronized void unlock(){isLocked = false;this.notify();}
}

我们可以看到,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循环后面即可:

public class Lock {private boolean isLocked = true;public void lock(){synchronized(this){while(isLocked){try{this.wait();} catch(InterruptedException e){//do nothing, keep waiting}}isLocked = true;}}public synchronized void unlock(){isLocked = false;this.notify();}
}

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

一个更现实的例子

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

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

//Fair Lock implementation with nested monitor lockout problem
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();synchronized(this){waitingThreads.add(queueObject);while(isLocked || waitingThreads.get(0) != queueObject){synchronized(queueObject){try{queueObject.wait();}catch(InterruptedException e){waitingThreads.remove(queueObject);throw e;}}}waitingThreads.remove(queueObject);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;if(waitingThreads.size() > 0){QueueObject queueObject = waitingThread.get(0);synchronized(queueObject){queueObject.notify();}}}
}
public class QueueObject {}

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

//Fair Lock implementation with slipped conditions problem
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();synchronized(this){waitingThreads.add(queueObject);}boolean mustWait = true;while(mustWait){synchronized(this){mustWait = isLocked || waitingThreads.get(0) != queueObject;}synchronized(queueObject){if(mustWait){try{queueObject.wait();}catch(InterruptedException e){waitingThreads.remove(queueObject);throw e;}}}}synchronized(this){waitingThreads.remove(queueObject);isLocked = true;lockingThread = Thread.currentThread();}}
}

注意:因为我只改动了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)块中的代码必须向上移到第一个同步块中。为适应这种变动,代码需要做点小改动。下面是改动过的代码:

//Fair Lock implementation without nested monitor lockout problem,
//but with missed signals problem.
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();synchronized(this){waitingThreads.add(queueObject);}boolean mustWait = true;while(mustWait){synchronized(this){mustWait = isLocked || waitingThreads.get(0) != queueObject;if(!mustWait){waitingThreads.remove(queueObject);isLocked = true;lockingThread = Thread.currentThread();return;}}     synchronized(queueObject){if(mustWait){try{queueObject.wait();}catch(InterruptedException e){waitingThreads.remove(queueObject);throw e;}}}}}
}

我们可以看到对局部变量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()之前调用,信号也不会丢失。

原创文章,转载请注明: 转载自并发编程网 – ifeve.com本文链接地址: Slipped Conditions

Slipped Conditions相关推荐

  1. 【转】JAVA 并发性和多线程 -- 读感 (二 线程间通讯,共享内存的机制)

    原文地址:https://www.cnblogs.com/edenpans/p/6020113.html 参考文章:http://ifeve.com/java-concurrency-thread-d ...

  2. 18、Java并发性和多线程-饥饿与公平

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

  3. 多线程常见问题及分析

    1.多线程的并发性和多线程介绍: 在过去单CPU时代,单任务在一个时间点只能执行单一程序.之后发展到多任务阶段,计算机能在同一时间点并行执行多任务或多进程.虽然并不是真正意义上的"同一时间点 ...

  4. java 饥饿现象,Java并发之“饥饿”和“公平锁”(Starvation and Fairness)

    饥饿发生的原因: 高优先级的线程占用了大部分的cpu时间,低优先级线程发生饥饿 线程被永久堵塞在一个等待进入同步块的状态 线程在等待一个本身(在其上调用wait())也处于永久等待完成的对象 java ...

  5. 线程饥饿和线程公平的处理分析

    如果一个线程因为CPU时间全部被其他线程抢走而得不到CPU运行时间,这种状态被称之为"饥饿".而该线程被"饥饿致死"正是因为它得不到CPU运行时间的机会.解决饥 ...

  6. Python + Selenium: expected_conditions介绍

    **本文为转载文章,由于不明确出处,故未标明** expected_conditions是Selenium的一个模块,selenium.webdriver.support.expected_condi ...

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

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

  8. pandas使用query函数基于判断条件获得dataframe中满足条件的数据行(row)的索引列表(index of rows matching conditions in dataframe)

    pandas使用query函数基于判断条件获得dataframe中满足条件的数据行(row)的索引列表(index of rows matching conditions in dataframe) ...

  9. SAP ME12 修改采购信息记录,系统提示:Condition type P000 does not allow supplementary conditions

    SAP ME12 修改采购信息记录,系统提示:Condition type P000 does not allow supplementary conditions 1,执行事务代码ME12,进入采购 ...

最新文章

  1. 亮相 LiveVideoStackCon 2021,网易云信解构自研大规模传输网 WE-CAN
  2. 基于Android 虹软人脸、人证对比,活体检测
  3. Windows下安装OpenSSL及其使用
  4. Github基本操作的学习与温习
  5. 剑指Offer - 面试题4. 二维数组中的查找(双指针)
  6. Mapper XML Files详解
  7. 在机器学习、大数据等领域工作,该学Hadoop还是Spark?
  8. 【第十届“泰迪杯”数据挖掘挑战赛】B题:电力系统负荷预测分析 ARIMA、AutoARIMA、LSTM、Prophet、多元Prophet 实现
  9. 解析美团联盟,美团分销联盟,美团福利宝,外卖美天赚区别和玩法
  10. 斐波那契数列的几种求解方法
  11. 写论文的公式怎么写最便捷?
  12. 基本了解云计算是什么东东了
  13. EXCEL单元格换行操作
  14. oa系统需要的服务器配置,oa办公系统需要服务器配置
  15. win7、win10系统双屏显示任务栏
  16. 反向放大器为何要使用同相增益(也称作噪声增益),来计算带宽
  17. 分布式全站爬虫——以搜狗电视剧为例
  18. [Cu (L) (Phen )]·1/4H2O配合物
  19. 来了老弟,表格的渲染
  20. nginx配置文件映射外网服务器

热门文章

  1. 关于积分过期的设计思路方案(原创)
  2. 50道简单python函数题_Python练习题 函数设计(55~67)一
  3. 学生信息管理系统(序列化与反序列化实现)
  4. iOS 视图透明度与视图颜色透明度
  5. Qt5.9设置窗口透明而控件不透明
  6. 20个国外知名网站手机版网页设计欣赏
  7. 鼠标放上去盒子向上滑动
  8. 数独java界面基础_Java从基础到进阶学习之路—-数独小游戏制作(二) | 学步园...
  9. 【RT-Thread 开源作品秀】基于 RT-Thread 的“数码小精灵”设计与实现
  10. 汇编语言-002(.data、Equal、$、EQU 、MOV 、MOVSX、MOVZX)