我在我的应用程序中遇到了一些锁定问题,其中包含如下几个类:
public interface AppClient {
void hello();
}
public class Client implements AppClient {
public synchronized static AppClient getInstance() {
return instance;
}
public void hello() {
System.out.println("Hello Client");
}
private final static class InnerClient implements AppClient {
public void hello() {
System.out.println("Hello InnerClient");
}
}
private static AppClient instance;
static {
instance = new InnerClient();
doSomethingThatWillCallClientGetInstanceSeveralTimes();
}
}
public class Application {
new Thread() {
AppClient c = Client.getInstance();
c.hello();
}.start();
new Thread() {
AppClient c = Client.getInstance();
c.hello();
}.start();
// ...
new Thread() {
AppClient c = Client.getInstance();
c.hello();
}.start();
}
在doSomethingThatWillCallClientGetInstanceSeveralTimes()方法中,它会做很多涉及很多类的初始化工作,并在初始化期间多次循环调用Client.getInstance静态方法(我明白这不好,但是,这是一个遗留的代码库持续20多年)。
这是我的问题:
1)我认为在类初始化客户端初始化完成之前,只有触发类Client初始化的第一个线程才能访问Client.getInstance方法,因为JVM将在类初始化完成之前在Client.class对象上进行同步。我阅读了相关主题的JLS并得出了这个结论(第12.4.2节,详细的初始化过程,http://java.sun.com/docs/books/jls/third_edition/html/execution.html)。
2)但是,这不是我在真实环境中看到的行为。例如,有三个线程调用Client.getInstance(),thread-1触发Client.class初始化,并多次调用doSomethingThatWillCallClientGetInstanceSeveralTimes()方法中的Client.getInstance()。在doSomethingThatWillCallClientGetInstanceSeveralTimes()方法完成之前,thread-2获取了Client.class对象的锁(这怎么可能?但它确实发生了),并进入Client.getInstance方法(因为这个方法是一个静态同步方法) 。由于某种原因,thread-2无法返回“实例”(我猜它正在等待Client.class完成其初始化)。同时,thread-1无法继续,因为它仍然需要在doSomethingThatWillCallClientGetInstanceSeveralTimes()中调用Client.getInstance并且由于它由thread-2拥有而无法获取锁。 Threaddump告诉我thread-2处于RUNNABLE状态,而thread-1处于BLOCKED状态,等待thread-2拥有的锁。
我只能在Windows中的64位Java 6u23 JVM中重现此行为,并且无法在32位Java 6 JVM + Windows环境中重现它。谁能告诉我在这里我错过了什么?这种代码注定会引起这样的锁定,如果是的话,怎么来的?我对JLS这部分的理解是不正确的?或者它是JVM问题?任何帮助表示赞赏。感谢。
答案 0 :(得分:3)
对我来说,这看起来像一个bug。当一个线程正在调用静态块时,没有其他线程能够访问它。该错误可能是另一个线程在初始化完成之前可以获取该类的锁定。 :(
我建议您构建代码,以便在启动时不需要这样的锁定。听起来确实相当复杂。例如在您的示例中,客户端不需要扩展客户端,并且可以在其声明的行上初始化实例。我会考虑以下结构。
enum Client implements AppClient {
INSTANCE;
public void hello() {
System.out.println("Hello Client");
}
}
您可以使客户端变为可变或使用委托,因此它不会暴露它可以更改状态(或实现)的事实
答案 1 :(得分:1)
JLS 12.4.2在(6)中明确指出初始化程序执行而时,锁被释放。所以我认为你看到了一个有效的执行路径。
你可能更好public synchronized static AppClient getInstance() { synchronized(Client.class) { if (instance == null) { instance = new InnerClient(); doSomethingThatWillCallClientGetInstanceSeveralTimes(); } return instance; } }
编辑
更好 - 在重新阅读规范中的这一段后,甚至可以在原始示例中完全删除同步 - 虚拟机会处理它。
编辑
很抱歉误导 - 更详细的阅读应该显示在(2)中,第二个线程无法获取锁定(在检测到正在进行的初始化之后,它“等待”)。