详解Java Slipped Conditions

所谓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();
   }
  }
 }
}1public 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()之前调用,信号也不会丢失。

以上就是详解Java Slipped Conditions的详细内容,更多关于Java Slipped Conditions的资料请关注我们其它相关文章!

(0)

相关推荐

  • Java堆内存又溢出了!教你一招必杀技(推荐)

    JAVA堆内存管理是影响性能主要因素之一. 堆内存溢出是JAVA项目非常常见的故障,在解决该问题之前,必须先了解下JAVA堆内存是怎么工作的. 先看下JAVA堆内存是如何划分的,如图: 1.JVM内存划分为堆内存和非堆内存,堆内存分为年轻代(Young Generation).老年代(Old Generation),非堆内存就一个永久代(Permanent Generation). 2.年轻代又分为Eden和Survivor区.Survivor区由FromSpace和ToSpace组成.Eden

  • Java 堆内存溢出原因分析

    前言 任何使用过基于 Java 的企业级后端应用的软件开发者都会遇到过这种低劣.奇怪的报错,这些报错来自于用户或是测试工程师: java.lang.OutOfMemoryError:Java heap space. 为了弄清楚问题,我们必须返回到算法复杂性的计算机科学基础,尤其是"空间"复杂性.如果我们回忆,每一个应用都有一个最坏情况特征.具体来说,在存储维度方面,超过推荐的存储将会被分配到应用程序上,这是不可预测但尖锐的问题.这导致了堆内存的过度使用,因此出现了"内存不够&

  • Java编程常见内存溢出异常与代码示例

    Java 堆是用来存储对象实例的, 因此如果我们不断地创建对象, 并且保证 GC Root 和创建的对象之间有可达路径以免对象被垃圾回收, 那么当创建的对象过多时, 会导致 heap 内存不足, 进而引发 OutOfMemoryError 异常. /** * @author xiongyongshun * VM Args: java -Xms10m -Xmx10m -XX:+HeapDumpOnOutOfMemoryError */ public class OutOfMemoryErrorTe

  • Java 内存溢出的原因和解决方法

    你是否遇到过Java应用程序卡顿或突然崩溃的情况?您可能遇到过Java内存泄漏.在本文中,我们将深入研究Java内存泄漏的确切原因,并推荐一些最好的工具来防止内存泄漏发生. 什么是JAVA内存泄漏? 简单地说,Java内存泄漏是指对象不再被应用程序使用,而是在工作内存中处于活动状态. 在Java和大多数其他编程语言中,垃圾收集器的任务是删除不再被应用程序引用的对象.如果不选中,这些对象将继续消耗系统内存,并最终导致崩溃.有时java内存泄漏崩溃不会输出错误,但通常错误会以java.lang.Ou

  • 详解Java内存溢出的几种情况

    JVM(Java虚拟机)是一个抽象的计算模型.就如同一台真实的机器,它有自己的指令集和执行引擎,可以在运行时操控内存区域.目的是为构建在其上运行的应用程序提供一个运行环境.JVM可以解读指令代码并与底层进行交互:包括操作系统平台和执行指令并管理资源的硬件体系结构. 1. 前言 JVM提供的内存管理机制和自动垃圾回收极大的解放了用户对于内存的管理,大部分情况下不会出现内存泄漏和内存溢出问题.但是基本不会出现并不等于不会出现,所以掌握Java内存模型原理和学会分析出现的内存溢出或内存泄漏,对于使用J

  • 完美解决java读取大文件内存溢出的问题

    1. 传统方式:在内存中读取文件内容 读取文件行的标准方式是在内存中读取,Guava 和Apache Commons IO都提供了如下所示快速读取文件行的方法: Files.readLines(new File(path), Charsets.UTF_8); FileUtils.readLines(new File(path)); 实际上是使用BufferedReader或者其子类LineNumberReader来读取的. 传统方式的问题: 是文件的所有行都被存放在内存中,当文件足够大时很快就会

  • 解决Java导入excel大量数据出现内存溢出的问题

    问题:系统要求导入40万条excel数据,采用poi方式,服务器出现内存溢出情况. 解决方法:由于HSSFWorkbook workbook = new HSSFWorkbook(path)一次性将excel load到内存中导致内存不够. 故采用读取csv格式.由于csv的数据以x1,x2,x3形成,类似读取txt文档. private BufferedReader bReader; /** * 执行文件入口 */ public void execute() { try { if(!path.

  • Java虚拟机常见内存溢出错误汇总

    一.引言 从事java开发的小伙伴在平时的开发工作中,应该会遇见各式各样的异常和错误,在实际工作中积累的异常或者错误越多,趟过的坑越多,就会使我们编码更加的健壮,就会本能地避开很多严重的坑.以下介绍几个Java虚拟机常见内存溢出错误.以此警示,避免生产血案. 二.模拟Java虚拟机常见内存溢出错误 1.内存溢出之栈溢出错误 package com.jayway.oom; /** * 栈溢出错误 * 虚拟机参数:-Xms10m -Xmx10m * 抛出异常:Exception in thread

  • Java内存溢出实现原因及解决方案

    1.JVM Heap(堆)溢出:java.lang.OutOfMemoryError: Java heap space JVM在启动的时候会自动设置JVM Heap的值, 可以利用JVM提供的-Xmn -Xms -Xmx等选项可进行设置.Heap的大小是Young Generation 和Tenured Generaion 之和.在JVM中如果98%的时间是用于GC,且可用的Heap size 不足2%的时候将抛出此异常信息. 解决方法:手动设置JVM Heap(堆)的大小. Java堆用于储存

  • Java虚拟机内存溢出与内存泄漏

    一.基本概念 内存溢出:简单地说内存溢出就是指程序运行过程中申请的内存大于系统能够提供的内存,导致无法申请到足够的内存,于是就发生了内存溢出. 内存泄漏:内存泄漏指程序运行过程中分配内存给临时变量,用完之后却没有被GC回收,始终占用着内存,既不能被使用也不能分配给其他程序,于是就发生了内存泄漏. 内存溢出 out of memory,是指程序在申请内存时,没有足够的内存空间供其使用,出现out of memory: 内存泄露 memory leak,是指程序在申请内存后,无法释放已申请的内存空间

随机推荐