我正在研究an example of a simple android game,但对其同步逻辑有疑问。
给出两个字段:
private boolean mRun = false;
private final Object mRunLock = new Object();
工作线程类中的方法setRunning
:
public void setRunning(boolean b) {
synchronized (mRunLock) {
mRun = b;
}
}
和同一类中的方法run
:
public void run() {
while (mRun) {
Canvas c = null;
try {
c = mSurfaceHolder.lockCanvas(null);
synchronized (mSurfaceHolder) {
if (mMode == STATE_RUNNING) updatePhysics();
synchronized (mRunLock) {
if (mRun) doDraw(c);
}
}
} finally {
if (c != null) {
mSurfaceHolder.unlockCanvasAndPost(c);
}
}
}
}
在mRun
语句中不同步while
是否正确?我认为setRunning
被检查为mRun
时可能会调用true
。
您需要保留'synchronized'语句。如果不这样做(尽管请注意,不是真正的java的android可能未遵循与实际java相同的内存模型),那么任何线程都可以自由为其想要的任何实例的任何字段创建一个临时克隆,并在某个未定义的稍后时间将对克隆的任何写入与任何其他线程的克隆进行同步。
为了避免这些“克隆” *的问题,您需要建立CBCA关系(“先于/后于”)-如果线程模型确保线程A中的X行肯定在线程B中的Y行之后,则Y行所做的任何字段写入都将保证在X行中可见。
换句话说,如果使用同步语句,如果run()方法中的mRunLock锁必须'等待'setRunning方法完成运行,则您刚刚在两者之间建立了CBCA关系,这很关键,因为这意味着setRunning
完成的mRun写入现在可见。如果您不这样做,则它可能是可见的,也可能不是,这取决于手机中的芯片和月相。
请注意,布尔写是原子的。因此,与在编写字段时阅读而不会发生的任何问题无关紧要(如果将字段的类型递减为原子的,这本身就不是问题,除了double和long之外的所有其他原语都是)。确保任何更改的可见性。
在纯简Java中,您可能为此使用AtomicBoolean
,并避免使用任何已同步的东西。还要注意,如果在不同的锁上嵌套了synced()(您先锁上mSurfaceHolder,然后再锁上mRunLock),则可能导致死锁(如果有任何代码“反向”执行此操作)(首先锁上mRunLock,然后锁上mSurfaceHolder)。
您是否在使用此代码时遇到任何问题,或者只是想知道“它是否正确”?如果是后者:是,那是正确的。
*)尽管此克隆操作听起来很乏味且容易出错,但唯一的选择是,任何其他线程都可以立即看到任何线程写入的任何字段。那会使一切变得缓慢。 VM不知道哪个写入有可能很快被另一个线程读取,如果您对现代CPU架构一无所知,则每个内核都有自己的缓存,其缓存比系统内存快几个数量级(100到1000倍!)。 。 “所有写入必须始终在任何地方都可见”的这种替代方案在很大程度上意味着字段永远不会在任何缓存中。这对于性能而言将是灾难性的。因此,这种内存模型基本上是必不可少的。有些语言没有它。它们往往比Java慢几个数量级。
我认为代码不正确。
您可能应该做类似的事情:
while (true) {
synchronized (mRunLock) {
if (mRun) break;
}
// ...
}
没有这个,您不能保证在条件读取之前会写入mRun
。
如果没有它,它将无法正常工作,因为您正在循环内的同步块中读取mRun
;如果执行了读取操作,则该值将被更新。但是,您在下一次迭代中在循环表达式中读取的值可能与在synchronized (mRunLock) { if (mRun) doDraw(c); }
中的上一次迭代中读取的值相同。]
至关重要的是,不能保证可以在初始迭代中使用。
不过,使mRun
易变比使用同步要容易。