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

时间:2012-06-27 06:28:28

标签: 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线程可能将其中断标志设置为true,并且他正在使用GLSurfaceView作为他提到的地图?只是一个假设,一个可能的场景。

1 个答案:

答案 0 :(得分:15)

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

休闲读者的长版:

问题是如何阻止当前正在执行某些工作的线程,以响应"取消"用户按钮或其他应用程序逻辑。

最初,Java支持"停止"方法,抢先停止一个线程。此方法已被证明是不安全的,因为没有给予已停止的线程任何(简单)方法来清理,释放资源,避免暴露部分修改的对象等等。

因此,Java发展成为一个合作伙伴"线程"中断"系统。这个系统非常简单:一个线程正在运行,另一个人正在调用"中断"在它上面,在线程上设置了一个标志,它的线程责任是检查它是否被中断并采取相应的行动。

因此,正确的Thread.run(或Callable等的Runnable.run)方法实现应该是这样的:

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

只要你的Thread执行的所有代码都在你的run方法中,并且你永远不会调用阻塞很长时间的东西......这通常不是这种情况,因为如果你产生一个Thread就是因为你有很长的事要做。

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

现在,如果在你的线程在Thread.sleep(600000000)内部时中断到来,没有任何其他支持,它将需要很多时间才能到达它检查isInterrupted的位置。

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

为了避免这种情况,中断线程的所有(大多数)方法(应该)抛出InterruptedException。那个例外只是告诉你"我正在等待这个和那个,但同时线程被打断了,你应该尽快清理并退出"。

与所有例外情况一样,除非你知道该怎么做,否则你应该重新抛出它,并希望调用堆栈中有你的人知道。

InterruptedExceptions更糟糕,因为当他们被抛出“中断状态”时#34;被清除了。这意味着只需捕获并忽略它们就会产生一个通常不会停止的线程:

public void run() {
  while (!Thread.getCurrentThread().isInterrupted()) {
    try {
      Thread.sleep(1000);
    } catch (InterruptedException e) {
      // Nothing here
    }
  }
}

在这个例子中,如果中断在sleep()方法(99.9999999999%的时间)内到达,它将抛出InterruptedException,清除中断标志,然后循环将继续,因为中断标志为false,并且线程不会停止。

这就是为什么你实施"而#34;正确地说,使用.isInterrupted,你真的需要捕获InterruptedException,并且你没有任何特殊的东西(比如清理,返回等等)来处理它,至少你能做的就是再次设置中断标志。

您发布的代码中的问题是"而#34;完全依赖于mExited来决定何时停止,而不是依赖于isInterrupted。

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也很重要。例如,如果你在一个正在某个线程池中执行的runnable中,或者在任何你不拥有和控制线程的任何方法中(一个简单的例子:一个servlet),你就不会这样做。 ;知道中断是否适用于"你" (在servlet的情况下,客户端关闭连接,容器试图阻止你为其他请求释放线程)或者它是否针对整个线程(或系统)(容器正在关闭)下来,停止一切)。

在那种情况下(99%的代码),如果你不能重新抛出InterruptedException(遗憾的是,检查过),那么将堆栈传播到线程池的线程已被中断的唯一方法是,在返回之前将标志设置回true。

这样,它将向上传播堆栈,最终生成更多InterruptedException,直到线程所有者(可以是jvm本身,Executor或任何其他线程池)可以正确反应(重用)线程,让它死,System.exit(1)...)

Java Concurrency in Practice的第7章介绍了大部分内容,这是一本非常好的书,我建议任何对计算机编程感兴趣的人,不仅仅是Java,导致问题和解决方案在许多其他环境中相似,并且解释得很好。

为什么Sun决定检查InterruptedException,当大多数文档建议无情地重新抛出它时,为什么他们决定在抛出异常时清除被中断的标志,当正确的事情是在大多数情况下再次设置为true时,仍然可以辩论。

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