我参与使用Java 1.4 API开发Xlet。
文档说Xlet
接口方法(实际上是xlet生命周期方法)是在其特殊线程(而不是EDT线程)上调用的。我通过记录检查 - 这是真的。这对我来说有点令人惊讶,因为它不同于在EDT上调用生命周期方法的BB / Android框架,但到目前为止它还可以。
在项目代码中,我看到应用程序广泛使用Display.getInstance().callSerially(Runnable task)
调用(这是在EDT线程上运行Runnable
的LWUIT方式。)
所以基本上Xlet实现类中的一些代码片段对来自EDT线程的xlet内部状态对象进行创建/更新/读取操作,而其他一些代码片段来自生命周期线程而没有任何同步(包括该状态)变量未声明为volatile)。像这样的Smth:
class MyXlet implements Xlet {
Map state = new HashMap();
public void initXlet(XletContext context) throws XletStateChangeException {
state.put("foo", "bar"); // does not run on the EDT thread
Display.getInstance().callSerially(new Runnable() {
public void run() {
// runs on the EDT thread
Object foo = state.get("foo");
// branch logic depending on the got foo
}
});
}
..
}
我的问题是:这是否为罕见的并发问题创建了背景?是否应该明确地同步对状态的访问(或者至少应将状态声明为volatile)?
我的猜测是它取决于代码是否在多核CPU上运行,因为我知道在多核CPU上如果2个线程在自己的核心上运行,那么变量就会被缓存除非明确同步,否则每个线程都有自己的状态版本。
我想对我的担忧得到一些信任。
答案 0 :(得分:2)
是的,在您描述的场景中,必须使对共享状态的访问成为线程安全的。
您需要注意两个问题:
第一个问题,可见性(您已经提到过),仍然可以在单处理器上进行。问题是允许JIT编译器在寄存器中缓存varible,在上下文切换中,OS很可能将寄存器的内容转储到线程上下文,以便以后可以恢复。但是,这与将寄存器的内容写回对象的字段不同,因此在上下文切换之后,我们不能假设对象的字段是最新的。
例如,请使用以下代码:
class Example {
private int i;
public void doSomething() {
for (i = 0; i < 1000000; i ++) {
doSomeOperation(i);
}
}
}
由于循环变量(实例字段)i
未声明为volatile,因此允许JIT使用CPU寄存器优化循环变量i
。如果发生这种情况,那么在循环完成之前,JIT将不需要将寄存器的值写回实例变量i
。
所以,让我们假设一个线程正在执行上面的循环然后它被抢占了。新调度的线程将无法看到i
的最新值,因为i
的最新值位于寄存器中,并且该寄存器已保存到线程本地执行上下文中。至少需要将实例字段i
声明为volatile
,以强制i
的每次更新对其他线程可见。
第二个问题是一致的对象状态。以代码中的HashMap
为例,内部由几个非最终成员变量size
,table
,threshold
和modCount
组成。其中table
是一个Entry
数组,形成一个链表。当将元素放入映射或从映射中移除时,需要以原子方式更新这些状态变量中的两个或更多个以使状态保持一致。对于HashMap
,这必须在synchronized
块内或类似内容中完成,以使其成为原子。
对于第二个问题,在单处理器上运行时仍会遇到问题。这是因为操作系统或JVM可以先占线程切换线程,而当前线程是执行put或remove方法的一部分,然后切换到另一个尝试在同一HashMap
上执行其他操作的线程。 / p>
想象一下,如果您的EDT线程在发生先发制人的线程切换时调用'get'方法,并且您获得了一个试图在地图中插入另一个条目的回调,会发生什么。但是这次地图超出了载荷因子,导致地图重新调整大小并且所有条目都被重新散列和插入。