java - 即使尝试从 JVM 静态初始化过程获取相同的锁,线程也不会停止

标签 java multithreading concurrency thread-safety static-initialization

在《Java Concurrency in Practice》一书中,这样说

“静态初始化程序由 JVM 在类初始化时、类加载后运行 但在任何线程使用该类之前。因为 JVM 在初始化期间获取锁 [JLS 12.4.2] 并且 每个线程至少获取一次该锁,以确保该类已加载, 静态初始化期间进行的内存写入自动对所有线程可见。”(Goetz 16.2.3)

想法1:第一种解释

我首先想到这意味着 JVM 决定看到某个类使用静态字段,暂时让所有线程尝试获取静态初始化使用的锁,如果该锁从未释放,那么它将停止所有线程线程永远等待该锁。

想法 2:更有意义的可能解释,尤其是对于示例代码的行为方式

如果情况是这样,只有在静态字段初始化之后,JVM 才会让所有线程尝试获取静态初始化所使用的锁,这样就可以了。不是第一个使用静态字段的其他线程会很好并且不会停止,因为它没有等待锁。不过,我不确定情况是否如此。谁能确认想法 2 是正确的解释吗?

最后这个程序看起来像这样,并不断打印 thread-0 和 thread-1

public class StaticBlockAndLineInterpretation {
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> keepLooping()).start();
        new Thread(() -> keepLooping()).start();
        Thread.sleep(2500);
        int x = AllThreadsStopper.threadStopper;
    }

    static void keepLooping() {
        while (true) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {}
            System.out.println("This is thread " + Thread.currentThread().getName());
        }
    }
}

class AllThreadsStopper {
    static int threadStopper;

    static {
        try {
            threadStopper = haltAllThreadsAndNeverReturn();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private static int haltAllThreadsAndNeverReturn() throws InterruptedException {
        System.out.println("haltAllThreadsAndNeverReturn called");
        new CountDownLatch(1).await();
        return 0;
    }
}


console output snippet:
    This is thread Thread-0
    This is thread Thread-1
    This is thread Thread-0
    This is thread Thread-1
    haltAllThreadsAndNeverReturn called
    This is thread Thread-0
    This is thread Thread-1
    This is thread Thread-0
    This is thread Thread-1
    This is thread Thread-0 and so forth...

最佳答案

引用的部分当然是关于使用该类的线程,否则,如果没有共享数据,则无需讨论线程安全性。

JLS§12.4.2描述了获取锁作为类的初始化过程的一部分,其中当线程检测到另一个线程当前正在执行初始化时必须进入阻塞状态:

For each class or interface C, there is a unique initialization lock LC. The mapping from C to LC is left to the discretion of the Java Virtual Machine implementation. The procedure for initializing C is then as follows:

  1. Synchronize on the initialization lock, LC, for C. This involves waiting until the current thread can acquire LC.
  2. If the Class object for C indicates that initialization is in progress for C by some other thread, then release LC and block the current thread until informed that the in-progress initialization has completed, at which time repeat this step.
  3. If the Class object for C indicates that initialization is in progress for C by the current thread, then this must be a recursive request for initialization. Release LC and complete normally.
  4. If the Class object for C indicates that C has already been initialized, then no further action is required. Release LC and complete normally.

请注意,这意味着如果类已经初始化(在 4. 中),但仍然获取和释放锁,这是内存可见性约束的正式定义,并且发生在<之前/em> Brian Goetz 提到的关系。

但作为类初始化正式定义的一部分,它仅适用于实际触发类初始化的代码,这在 JLS §12.4.1., “When Initialization Occurs” 中指定。 :

A class or interface type T will be initialized immediately before the first occurrence of any one of the following:

  • T is a class and an instance of T is created.
  • A static method declared by T is invoked.
  • A static field declared by T is assigned.
  • A static field declared by T is used and the field is not a constant variable (§4.12.4).
  • T is a top level class (§7.6) and an assert statement (§14.10) lexically nested within T (§8.1.3) is executed.

When a class is initialized, its superclasses are initialized (if they have not been previously initialized), as well as any superinterfaces (§8.1.5) that declare any default methods (§9.4.3) (if they have not been previously initialized).

由于在您的情况下,两个线程没有执行任何指定的操作,甚至没有间接执行,因此它们没有触发类初始化,因此不会尝试获取类初始化锁。

通过从列表中插入操作,您可以轻松地导致示例线程的阻塞,例如

static void keepLooping() {
    while (true) {
        try {
            Thread.sleep(1000);
            new AllThreadsStopper();
        } catch (InterruptedException e) {}
        System.out.println("This is thread " + Thread.currentThread().getName());
    }
}

由于创建类实例会触发类的初始化,因此线程现在会被阻塞。

为了完整起见,请参见第 12.4.2 节。还提到:

An implementation may optimize this procedure by eliding the lock acquisition in step 1 (and release in step 4/5) when it can determine that the initialization of the class has already completed, provided that, in terms of the memory model, all happens-before orderings that would exist if the lock were acquired, still exist when the optimization is performed.

Brian Goetz 的书就是讲的,他说“这种技术可以与 JVM 的延迟类加载相结合,创建一种不需要在公共(public)代码路径上同步的延迟初始化技术 ”。它非常高效,因为一旦初始化完成,线程就可以访问已初始化的类,而无需同步成本。在您的示例中,初始化永远不会完成,这种优化是不可能的,并且线程必须获取锁(如果它们根据 JLS §12.4.1 使用该类)。

关于java - 即使尝试从 JVM 静态初始化过程获取相同的锁,线程也不会停止,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/48941427/

相关文章:

java - Android 中的多个 Onactivityresult 方法

java - 删除 Open Office UNO Java api 中的字段

java - 使用 Spring Controller 处理 Paypal IPN 请求?

.net - 在线程上调用方法,使用异步编程模型的工作任务以及使用BackgroundWorkerThread的工作之间有什么基本区别?

Java - 信号量的广泛使用

java - 使用 GSON 处理随机生成和不一致的 JSON 字段/键名称

multithreading - 在线程之间发送数据的惯用方法是什么?

C# 定期从不同线程返回值

go - 与 channel 同时写入

concurrency - linux内核如何避免死锁?