我有一个类似这样的服务器:
class Server {
private WorkingThing worker;
public void init() {
runInNewThread({
// this will take about a minute
worker = new WorkingThing();
});
}
public Response handleRequest(Request req) {
if (worker == null) throw new IllegalStateException("Not inited yet");
return worker.work(req);
}
}
正如您所看到的,有处理请求的线程和启动服务器的线程。请求可以在初始化完成之前传入,因此需要使用IllegalStateException
进行检查。
现在,为了使这个线程安全(这样请求处理程序线程就不会在init之后看到过时的null
值的worker
版本),我必须使worker变得易失性,对其进行同步,或者诸如此类。
然而,在init完成后,worker
将不再更改,因此它实际上是最终的。因此,似乎任何可能发生的锁争用都是浪费。那么,我在这里能做的最有效的事情是什么?
现在我知道这在实际意义上并不重要(阅读网络请求等繁重的工作,一个锁有什么重要?),但出于好奇,我想知道。
关于volatile的一个注意事项:标记一个变量volatile比使用同步(它不涉及锁)更便宜,而且通常足够便宜,你不会注意到。特别是,在x86体系结构上,读取易失性变量的成本并不比读取非易失性可变成本高。然而,写入volatile的代价更高,而且变量是volatile这一事实可能会阻止一些编译器优化。
因此,使用volatile可能是在您的场景中为您提供最佳性能/复杂性比的选项。
你没有那么多选择。最终,它可以归结为确保员工的安全发布。安全出版习惯用法包括:
- 从静态初始化程序初始化实例
- 将对实例的引用标记为最终引用
- 将对实例的引用标记为volatile
- 同步所有访问
在您的情况下,只有最后两个选项可用,使用volatile更有效。
您应该声明worker是易失性。
有两个原因
-
如果由于重新排序和可见性的影响而不将其声明为volatile您可以看到对不完整构造的Worker对象的非null引用。这样你就可以具有不良影响。如果您使用最终变量。
-
理论上,您的主线程可能在很长一段时间内看不到工作对象的非null引用。所以要避免。
因此,得出结论,如果worker是不可变的,那么对于第2点,您应该使其不可变。始终避免不可预测的结果。宣布其不稳定将解决这些问题。
对初始请求使用简单锁定:
public synchronized void init() {
if(worker!=null) return;
runInNewThread({
synchronized(Server.this){
worker = new WorkingThing();
Server.this.notify();
}
});
this.wait();
}
public Response handleRequest(Request req) {
if (worker == null) synchronized(this) {
this.wait();
}
return worker.work(req);
}
这是因为在访问worker之间存在同步点。
我使用ava.util.courrent.atomic.AtomicReference如果它为null,请抛出或等待并重试(如果init足够快)。