静态初始化器和静态同步方法锁定问题

时间:2011-01-06 06:44:59

标签: java multithreading static synchronization classloader

我在我的应用程序中遇到了一些锁定问题,其中包含如下几个类:

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问题?任何帮助表示赞赏。感谢。

2 个答案:

答案 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)中,第二个线程无法获取锁定(在检测到正在进行的初始化之后,它“等待”)。