【问题标题】:Class initialization and synchronized class method类初始化和同步类方法
【发布时间】:2011-01-07 07:41:29
【问题描述】:

在我的应用程序中,有一个如下所示的类:

public class Client {
    public synchronized static print() {
        System.out.println("hello");
    }

    static {
        doSomething(); // which will take some time to complete
    }
}

该类将用于多线程环境,多个线程可能同时调用Client.print()方法。不知道thread-1有没有机会触发类初始化,在类初始化完成之前thread-2进入print方法打印出“hello”字符串?

我在生产系统(64 位 JVM + Windows 2008R2)中看到了这种行为,但是,我无法在任何环境中用简单的程序重现这种行为。

在 Java 语言规范第 12.4.1 节 (http://java.sun.com/docs/books/jls/second_edition/html/execution.doc.html) 中,它说:

类或接口类型 T 将在以下任何一项第一次出现之前立即初始化:

  • T 是一个类,并创建了一个 T 的实例。
  • T 是一个类,调用了 T 声明的静态方法。
  • 分配了一个由 T 声明的静态字段。
  • 使用了由 T 声明的静态字段,并且对该字段的引用不是编译时常量(第 15.28 节)。对编译时常量的引用必须在编译时解析为编译时常量值的副本,因此使用此类字段永远不会导致初始化。

根据这一段,类的初始化会在调用静态方法之前进行,但是不清楚类的初始化是否需要在调用静态方法之前完成 .根据我的直觉,JVM应该在进入其静态方法之前要求完成类初始化,我的一些实验支持我的猜测。但是,我确实在另一个环境中看到了相反的行为。有人可以帮我解释一下吗?

感谢您的帮助。

【问题讨论】:

    标签: java multithreading concurrency synchronization


    【解决方案1】:

    我对引用文字的理解是,类初始化过程在调用T声明的静态方法之前完成(初始化 em>。

    将被初始化表示初始化过程已经开始并已终止。

    因此(据我所知)不可能,虽然由于线程 A 调用了 print 而执行了静态初始化程序,但另一个线程已经可以调用 print

    JLS 的Chapter 12.4.2 描述了详细的初始化过程,它负责在多线程环境中初始化类。

    【讨论】:

    • 静态初始化器是一个简单的类方法,它在锁(类加载器的)下调用。
    【解决方案2】:

    如果您的“多线程环境”使用多个类加载器来加载您的 Client 类,那么您可能会获得多个 Client 实例,每个实例都会在运行任何 Client.print() 调用之前运行静态初始化程序.你会看到类似

    doSomething
    hello
    doSomething
    hello
    hello
    hello
    

    我有一些示例代码显示了这一点,但当前版本运行起来有点繁琐。如果你愿意,我可以清理并发布它。

    【讨论】:

    • 啊,我完全忘记了一个 JVM 中有多个类加载器。好收获!
    • 好点,我从来没想过。但是,我的应用程序似乎并非如此:(
    • 确实如此,但值得注意的是每个类加载器都会有一个单独的类副本。
    【解决方案3】:

    执行被认为是类初始化一部分的静态块:

    类的初始化包括执行其静态初始化程序和类中声明的静态字段(类变量)的初始化程序...

    JVM 规范保证这将以线程安全的方式完成。引用JLS section 12.4.2 Detailed Initialization Procedure:

    因为 Java 编程语言是多线程的,所以类或接口的初始化需要仔细同步,因为其他线程可能同时尝试初始化同一个类或接口。还有一种可能性是,作为该类或接口初始化的一部分,可以递归地请求类或接口的初始化;例如,A 类中的变量初始化器可能会调用不相关的 B 类的方法,而 B 类的方法又可能会调用 A 类的方法。Java 虚拟机的实现负责处理同步和递归初始化……

    更详细地说,它是通过获取 Class 对象的锁来实现的:

    那么初始化一个类或接口的过程如下:

    1. 在类上同步 (§14.19) 表示类的对象或 待初始化接口

    您的方法是static synchronized,它也需要锁定 Class 对象。由于 JVM 在类初始化期间获取了相同的锁,因此一个线程无法初始化类而其他线程在其上执行 static synchronized 方法。 所有报价均来自 JLS 我希望这会有所帮助。顺便说一句,你怎么知道在类初始化完成之前打印发生了?

    编辑:实际上我认为只有static synchronized 不能与类初始化并行执行是错误的。在类初始化完成之前,类上的任何方法都无法执行。

    【讨论】:

    • @Petro,“由于 JVM 在类初始化期间获取了相同的锁”,这是不准确的,因为在 JLS 部分 12.4.2 中,它说“否则,记录该类的初始化的事实对象现在正在由当前线程进行并释放对 Class 对象的锁定。"。所以类锁在类初始化完成之前就被释放了。
    • 您可以通过 1) 线程 1 触发可能需要很长时间才能完成的类初始化来验证此行为。 2) thread-2 使用 synchronized 语句来获取类锁,如 synchronized(MyClass.class)。如果时机合适,thread-2将进入同步语句块。
    • “你怎么知道打印发生在类初始化完成之前”,我在类静态初始化程序的末尾添加了一些打印语句来验证。
    • @nybon 您在第 6 步释放锁是正确的(否则,请记录 Class 对象的初始化现在正在进行中的事实......)。但是释放锁并不意味着该类已准备好使用。看一下第2步(如果其他线程正在对类或接口进行初始化,则等待这个Class对象(暂时释放锁)。当当前线程从等待中唤醒时,重复此步骤。) .由于初始化过程存储在 JVM 中,因此没有必要持有锁。访问由 JVM 保护。
    • 我为此实现了简单的单元测试,并且类初始化总是发生在任何静态同步语句执行之前。
    【解决方案4】:

    如果代码运行在一些容器比如Servlet中,你可以在容器的生命周期中初始化它。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2023-01-07
      • 1970-01-01
      相关资源
      最近更新 更多