java - 在等待退出信号时处理InterruptedException(Android中的错误?)

标签 java android multithreading interrupted-exception

我遇到了下面的代码,我想知道它是否确实符合我的想法:

synchronized(sObject) {
    mShouldExit = true;   
    sObject.notifyAll()    
    while (!mExited) {
      try {
           sObject.wait();
        } catch (InterruptedException ex) {
           Thread.currentThread().interrupt();
        }
     }
}

关于上下文:另一个线程检查mShouldExit(在sObject监视器内部),在这种情况下将退出。

对我来说,这似乎不是正确的模式。如果发生中断,它将再次设置中断状态,因此当它返回sObject.wait()时,将出现另一个InterruptedException等。等等。因此,它永远无法进入真正的等待状态(sObject.wait()),即它将永远不会释放sObject监视器。这可能会导致无限循环,因为另一个线程无法将mExiting设置为true,因为它永远无法进入sObject的监视器。 (因此,我认为interrupt()调用是一个错误,不能在此处使用。)我是否缺少某些内容?

请注意,该代码段是官方Android框架源代码的一部分。

更新:实际上,情况更糟,因为在GL渲染开始时,Android中使用了相同的模式。 GLSurfaceView.GLThread.surfaceCreated()的官方源代码:
   public void surfaceCreated() {
        synchronized(sGLThreadManager) {
            if (LOG_THREADS) {
                Log.i("GLThread", "surfaceCreated tid=" + getId());
            }
            mHasSurface = true;
            sGLThreadManager.notifyAll();
            while((mWaitingForSurface) && (!mExited)) {
                try {
                    sGLThreadManager.wait();
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }
    }

您可以通过类似的方式重现该错误:确保UI线程的状态标志尚未中断,然后添加GLSurfaceView并开始GL渲染(通过setRenderer(...),但在某些设备上,请确保GLSurfaceView的状态为Visibility.VISIBLE,否则渲染将无法启动)。

如果执行上述步骤,您的UI线程将陷入无限循环,因为上面引用的代码将继续生成InterruptedException(由于wait()),因此GL线程将永远无法将mWaitingForSurface设置为false。

根据我的测试,似乎这样一个无限循环也将导致无休止的GC_CONCURRENT垃圾回收序列(或者至少是logcat中的此类消息)。有趣的是,某人之前在stackoverflow上有一个未知的,定义不明确的问题,可能与以下原因有关:
How to solve GC_concurrent freed?

难道也许他的UI线程的interrupted标志设置为true,并且他使用GLSurfaceView作为他提到的 map ?只是一个假设,一个可能的情况。

最佳答案

简短版本:该代码是错误的,并且将导致无限循环(我仍然有疑问,但可能取决于JVM实现)。设置中断状态是正确的事情,但是它应该退出循环,最终使用Thread.isInterrupted()检查相同的中断状态。

普通读者的长版:

问题是如何响应用户的“取消”按钮或由于某些其他应用程序逻辑而停止当前正在执行某些工作的线程。

最初,Java支持“停止”方法,该方法抢先停止了线程。这种方法已被证明是不安全的,因为它没有给被停止的线程任何(简便)的清理,释放资源,避免暴露被部分修改的对象的方法,等等。

因此,Java演变为“合作”线程“中断”系统。这个系统非常简单:一个线程正在运行,其他人对其调用“中断”,在该线程上设置了一个标志,由线程负责检查它是否已被中断并采取相应的措施。

因此,正确的Thread.run(或Runnable.run,Callable等)的方法实现应类似于:

public void run() {
  while (!Thread.getCurrentThread().isInterrupted()) {
    // Do your work here
    // Eventually check isInterrupted again before long running computations
  }
  // clean up and return
}

只要您的线程正在执行的所有代码都在您的run方法内部,并且您绝不会调用长时间阻塞的东西,这通常就没了,这通常不是这种情况,因为如果您生成一个Thread是因为您拥有事情要做很久。

最简单的阻塞方法是Thread.sleep(millis),它实际上是唯一要做的事情:它在给定的时间内阻塞线程。

现在,如果中断在线程位于Thread.sleep(600000000)内时到达,而没有任何其他支持,则到达检查isInterrupted的点将花费很多时间。

甚至在某些情况下,您的线程将永远不会退出。例如,您的线程正在计算某些东西,并将结果发送到有限大小的BlockingQueue中,您调用queue.put(myresult),它将阻塞直到使用者释放队列中的某些空间为止,如果与此同时使用者已经被中断(或死亡或其他原因),空间将永远不会到达,该方法将不会返回,对.isInterrupted的检查将永远不会执行,您的线程将被卡住。

为了避免这种情况,所有(大多数)中断线程的方法(应该)都抛出InterruptedException。该异常只是告诉您“我一直在等待,但是在此同时线程被中断了,您应该尽快清理并退出”。

与所有异常(exception)一样,除非您知道该怎么做,否则应该重新抛出它,并希望调用堆栈中位于您上方的人知道。

InterruptedException甚至更糟,因为抛出它们时,将清除“中断状态”。这意味着简单地捕获并忽略它们将导致通常不会停止的线程:
public void run() {
  while (!Thread.getCurrentThread().isInterrupted()) {
    try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      // Nothing here
    }
  }
}

在此示例中,如果中断在sleep()方法期间到达(这是99.9999999999%的时间),它将抛出InterruptedException,清除中断标志,由于中断标志为false,因此该循环将继续进行,并且线程将不停。

这就是为什么如果您使用.isInterrupted正确实现了“while”,并且确实需要捕获InterruptedException,并且您没有任何特殊的要求(如清理,返回等),至少可以这样做再次设置中断标志。

您发布的代码中的问题在于,“while”仅取决于mExited来决定何时停止,而不是onisInterrupted。
while (!mExited && !Thread.getCurrentThread().isInterrupted()) {

或者它可能在被中断时退出:
} catch (InterruptedException e) {
  Thread.currentThread().interrupt();
  return; // supposing there is no cleanup or other stuff to be done
}

如果您不控制线程,则将isInterrupted标志设置回true也很重要。例如,如果您处于正在某种类型的线程池中执行的可运行对象中,或者在您不拥有并控制该线程的任何方法中的任何内部(一个简单的情况:一个servlet),则不知道是否中断是针对“您”(在servlet的情况下,客户端关闭了连接,并且容器试图阻止您释放线程以供其他请求使用),或者是针对整个线程(或系统)(针对容器正在关闭,停止所有操作)。

在这种情况下(占代码的99%),如果无法重新抛出InterruptedException(不幸的是,已检查),则将堆栈向上传播到线程已被中断的线程池的唯一方法是设置在返回之前将其标记为true。

这样,它将在堆栈中向上传播,最终生成更多InterruptedException,直到线程所有者(可以是Executor的jvm本身,或者是任何其他线程池),这些线程可以做出适当的 react (重用线程,让它死掉, System.exit(1)...)

其中大部分内容都在《 Java Concurrency in Practice》的第7章中进行了介绍,这是一本非常不错的书,我向所有对计算机编程感兴趣的人(不仅仅是Java)推荐这本书,这会导致问题和解决方案在许多其他环境中相似,并且解释也很简单。写得很好。

为什么Sun决定检查InterruptedException,为什么大多数文档建议无情地重新抛出InterruptedException,以及为什么他们决定在抛出该异常时清除Interrupted标志,而正确的做法是在大多数情况下将其再次设置为true,所以仍然开放进行辩论。

但是,如果.wait在检查中断标志之前释放了锁,它将从另一个线程打开一扇小门来修改mExited boolean 值。不幸的是,wait()方法是 native 的,因此应检查该特定JVM的源。这不会改变您发布的代码编码不正确的事实。

关于java - 在等待退出信号时处理InterruptedException(Android中的错误?),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/11220572/

相关文章:

java - Tomcat 服务器无法通过 Servlet 连接到 MongoDB

java - 我认为已初始化的变量发送未初始化的错误

java - 定时器的问题 - android

java - map 在 OpenStreetMap (osm) 中不可见

c++ - 基于C++11的线程类库

java - 如何从 MATLAB 代码捕获 java 异常

android - 为 ListView 强制 onSizeChanged

Android - Gradle - 如何下载包含的 jar 的传递依赖项

android - RxJava : return a cached value synchronously/immediately

java - 修复线程不安全 Java 代码的策略?