9

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

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(due to wait()),因此 GL 线程将永远无法设置mWaitingForSurface为 false。

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

难道他的 UI 线程可能将其中断标志设置为 true,并且他正在使用 GLSurfaceView 作为他提到的地图吗?只是一个假设,一个可能的场景。

4

1 回答 1

17

简短版本:该代码是错误的,并且会导致无限循环(我仍然有疑问,但可能取决于 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.sleep(millis),它实际上是它唯一做的事情:它在给定的时间内阻塞线程。

现在,如果在您的线程位于 Thread.sleep(600000000) 中时中断到达,而没有任何其他支持,它需要很长时间才能到达它检查 isInterrupted 的点。

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

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

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

InterruptedExceptions 更糟糕,因为当它们被抛出时,“中断状态”被清除。这意味着简单地捕获和忽略它们将导致线程通常不会停止:

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

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

这就是为什么如果你使用.isInterrupted正确实现你的“while”,你真的需要捕获InterruptedException,而且你没有任何特殊的事情(比如清理、返回等)来处理它,至少你可以做到再次设置中断标志。

您发布的代码中的问题是“while”仅依赖于 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 也很重要。例如,如果您在某种线程池中执行的可运行对象中,或者在您不拥有和控制线程的任何方法中(一个简单的案例:servlet),您不知道是否中断是针对“你”的(在 servlet 的情况下,客户端关闭了连接并且容器试图阻止你为其他请求释放线程),或者它是否针对整个线程(或系统)(容器正在关闭,停止一切)。

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

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

大部分内容都包含在 Java Concurrency in Practice 的第 7 章中,这是一本非常好的书,我推荐给任何对一般计算机编程感兴趣的人,而不仅仅是 Java,因为在许多其他环境中问题和解决方案是相似的,并且解释是写得很好。

为什么 Sun 决定检查 InterruptedException,当大多数文档建议无情地重新抛出它时,以及为什么他们决定在抛出该异常时清除中断标志,而正确的做法是在大多数情况下再次将其设置为 true,保持打开状态辩论。

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

于 2012-07-24T03:09:56.007 回答