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

发布于 2024-10-10 18:16:31 字数 2026 浏览 3 评论 0原文

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

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初始化的第一个线程才能访问Client.getInstance方法,因为JVM会在类初始化完成之前同步Client.class对象。我阅读了相关主题的 JLS 并得出了这个结论(第 12.4.2 节,详细初始化过程,

2)然而,这不是我在真实环境中看到的行为。例如,有3个线程调用Client.getInstance(),thread-1触发Client.class初始化,并在doSomethingThatWillCallClientGetInstanceSeveralTimes()方法中多次调用Client.getInstance()。而在doSomethingThatWillCallClientGetInstanceSeveralTimes()方法完成之前,线程2获取了Client.class对象的锁(怎么可能?但确实发生了),并进入Client.getInstance方法(因为该方法是静态同步方法) 。由于某种原因,thread-2 无法返回“实例”(我猜它正在等待 Client.class 完成其初始化)。同时,线程1无法继续,因为它仍然需要在doSomethingThatWillCallClientGetInstanceSeveralTimes()中调用Client.getInstance,并且无法获取锁,因为它属于线程2。 Threaddump 告诉我线程 2 处于 RUNNABLE 状态,线程 1 处于 BLOCKED 状态,正在等待线程 2 拥有的锁。

我只能在 Windows 中的 64 位 Java 6u23 JVM 中重现此行为,而无法在 32 位 Java 6 JVM + Windows 环境中重现此行为。有人可以告诉我我在这里缺少什么吗?这种代码是否注定会产生这样的锁定?如果是,又是怎么发生的呢?难道我对JLS这部分的理解不正确?或者是JVM的问题?任何帮助表示赞赏。谢谢。

I ran into some locking issue in my application, in which contains several classes like below:

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();
}

In the doSomethingThatWillCallClientGetInstanceSeveralTimes() method, it will do quite a lot initialization work involving many classes and circularly call Client.getInstance static method several times during the initialization (I understand this is not good, however, this is a legacy code base which lasts for 20+ years).

Here is my problem:

1) I thought before the class Client initialization completes, only the first thread that triggers the class Client initialization can access Client.getInstance method because JVM will synchronize on the Client.class object before the class initialization completes. I read the JLS on related topic and came to this conclusion (section 12.4.2, Detailed Initialization Procedure, http://java.sun.com/docs/books/jls/third_edition/html/execution.html).

2) However, it was not the behavior as I saw in my real environment. For example, there are three threads calling Client.getInstance(), thread-1 triggers the Client.class initialization, and calls Client.getInstance() in doSomethingThatWillCallClientGetInstanceSeveralTimes() method several times. And before the completion of doSomethingThatWillCallClientGetInstanceSeveralTimes() method, thread-2 acquires the lock of Client.class object (how is that possible? but it did happen), and enters into Client.getInstance method (because this method is a static synchronized method). For some reason, thread-2 cannot return the "instance" (I guess it is waiting for Client.class completes its initialization). At the same time, thread-1 cannot proceed because it still needs to call Client.getInstance in doSomethingThatWillCallClientGetInstanceSeveralTimes() and cannot acquire the lock since it is owned by thread-2. Threaddump tells me that thread-2 is in RUNNABLE state, and thread-1 is in BLOCKED state waiting for the lock owned by thread-2.

I can only reproduce this behavior in 64 bit Java 6u23 JVM in Windows and cannot reproduce it a 32 bit Java 6 JVM + Windows environment. Can someone tell me what am I missing here? Is this kind of code doomed to give rise to such locking, if yes, how come? Is my understanding on JLS for this part is incorrect? Or it is a JVM issue? Any help is appreciated. Thanks.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

卸妝后依然美 2024-10-17 18:16:32

对我来说这看起来像一个错误。当一个线程调用静态块时,其他线程不应该能够访问它。该错误可能是另一个线程可以在初始化完成之前获取该类的锁。 :(

我建议你构造你的代码,这样你就不需要在启动时进行这样的锁定。这听起来确实相当复杂。例如,在你的示例中,客户端不需要扩展客户端,并且实例可以在其声明的行上初始化我会考虑以下结构。

enum Client implements AppClient {
    INSTANCE;

    public void hello() {
        System.out.println("Hello Client");
    }
}

您可以使 Client 可变或使用委托,这样它就不会暴露它可以更改状态(或实现)的事实。

To me that looks like a bug. While one thread is calling the static block, no other thread should be able to access it. The bug may be that another thread can acquire a lock on the class before initialisation is finished. :(

I would suggest you structure you code so you don't need such locking on startup. It does sound rather complicated. e.g. in your example Client doesn't need to extends client and the instance could be initialised on the line its declared on. I would consider the following structure.

enum Client implements AppClient {
    INSTANCE;

    public void hello() {
        System.out.println("Hello Client");
    }
}

You can make Client mutable or use delegation so it doesn't expose the fact it can change state (or implementation)

再浓的妆也掩不了殇 2024-10-17 18:16:32

JLS 12.4.2 在 (6) 中明确指出,初始化程序执行时,锁被释放。所以我认为你看到了一个有效的执行路径。您可能会更好地

  • 杀死静态初始化程序并在访问器中执行同步延迟初始化
  • 手动同步静态初始化程序代码
    public synchronized static AppClient getInstance() {
      synchronized(Client.class) {
          if (instance == null) {
            instance = new InnerClient();
            doSomethingThatWillCallClientGetInstanceSeveralTimes();
          }
          return instance;
      }
    }

编辑

更好 - 重新阅读规范中的这一段后,甚至可以完全删除 原始示例中的同步 - VM 将处理它。

编辑

抱歉误导 - 更详细的阅读应该揭示在(2)中,第二个线程无法获取锁(在检测到正在进行的初始化后,它“等待”)。

The JLS 12.4.2 explicitly states in (6) that while the initializer executes, locks are released. So i think you see a valid execution path. You may be better of

  • killing the static initializer and do synchronized lazy initializing in the accessor
  • manually synchronize the static initializer code
    public synchronized static AppClient getInstance() {
      synchronized(Client.class) {
          if (instance == null) {
            instance = new InnerClient();
            doSomethingThatWillCallClientGetInstanceSeveralTimes();
          }
          return instance;
      }
    }

EDIT

Even better - after re-reading this paragraph in the spec, it is even possible to completely remove the synchronization in your original example - the VM will take care of it.

EDIT

Sorry for misguiding - even more detailed reading should reveal that in (2), the second thread can't acquire the lock (after detecting ongoing initialization, it "waits").

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文