我在 Android 应用程序中创建启动线程时发现了一个非常奇怪的问题。
如果我有以下类线程:
public class TroubleThread extends Thread{
boolean running;
public boolean isRunning() {
return running;
}
public void setRunning(boolean running) {
this.running = running;
}
@Override
public void run() {
while (isRunning()){
}//end while
}//end run
}
并将其添加到活动的 onCreate(...) 方法中的某个位置,例如:
public class MyActivity extends Activity {
TroubleThread myThread;
@Override
public void onCreate(Bundle savedInstanceState) {
//....
myThread = new TroubleThread();
myThread.setRunning(true);
myThread.start();
}
}
应用程序将崩溃。
但是如果我将 run() 方法更改为:
@Override
public void run() {
while (running){ //NOTE THE USE OF DIRECT FIELD ACCESS INSTEAD OF METHOD
}//end while
}//end run
它停止崩溃。
即使我通过使用锁、notify() 和 wait() 解决了我的问题,问题仍然存在:
为什么当使用直接访问字段时,应用程序继续工作,而使用该方法时它崩溃了?
首先,由于您没有提供MCVE,其他人无法重现您的问题。 这是不幸的,因为这意味着我们无法确定问题的真正原因是什么。 我们只能提出假设。
有些人假设你的问题是由其他原因引起的;例如,GUI线程上的无限循环。 这是有道理的,但没有明确的证据证明这一点。 (而且我们看不到代码...
我的假设是这是一个"内存可见性"问题。 Java 语言规范中有一章定义了保证一个线程看到另一个线程写入内存的值的情况。 这些规则相当复杂和技术性,但本质是您需要分析一个线程的内存写入与另一个线程读取的后续内存之间是否存在先行关系。 许多事情会给你这种关系:
- 线程写入/读取共享可变变量
- 使用同一锁同步的线程
- 线程启动
- 线程连接
- 构造函数的完成(对于
final
变量)
但是,与 running
变量相关的程序中不存在这些内容。 这意味着无法保证在running
变量上循环的线程将看到另一个线程进行的setRunning
调用的结果:
- 它可能会立即看到更改
- 稍后可能会看到更改
- 它可能永远不会看到他们
所有三种行为都是可能的....在关系之前没有发生。
那么,为什么一个版本的代码与另一个版本的行为不同呢?
我们无法确定。 实际上可以肯定的是,有人需要对您的示例的本机(机器)代码进行深入分析。 这可能与优化程序在一种情况下所做的(法律)重新排序有关,而不是另一种情况。 这可能是一个微妙的时间效应。
但无论哪种方式,JLS 都表示编译器没有义务确保写入是可见的。 为什么? 因为这段代码破坏了内存模型的规则。
解决 方案:
在这种情况下,最简单的解决方案是将running
声明为 volatile
。
另一种解决方案是将isRunning()
和setRunning
声明为synchronized
方法。
这些中的任何一个都足以提供关系之前发生的事情......并保证isRunning()
看到setRunning()
所做的更新。