从另一个线程初始化相同 class 的静态字段时访问静态方法

Accessing a static method when initializing a static field of same class from another thread

我遇到了一个非常奇怪的问题,除了将问题分成两个 classes 之外我无法解决。

我想知道是否有不拆分 class 的解决方案,更重要的是我想知道是否有人知道为什么 Java 引擎决定采取行动它的方式。

问题: 我有一个带有静态方法、静态字段和构造函数的 class。静态字段被初始化为 class 本身的一个实例。在实例初始化期间,我想访问上述静态方法。见以下代码:

public class Simple {
    public Simple() {
        int count = 4;

        for (int i = 0; i < count; i++) {
            System.out.println("Simple: " + Simple.isFlag()); 
        }

    }

    private static Simple i = new Simple();

    public static boolean isFlag() {
        return true;
    }

    public static void run() {

    }
}

public class Main {

    public static void main(String[] args) {
        Simple.run();
    }

}

这段代码 运行 绝对没问题。输出如下所示:

Simple: true
Simple: true
Simple: true
Simple: true

输出是在我调用 run() 方法后生成的,因为 stativ 字段 i 仅在我访问该 class 的第一个静态成员后才被初始化。

我现在想做完全相同的事情,除了多线程。看这里:

public class Parallel {
    public Parallel() {
        int count = 4;

        CountDownLatch latch = new CountDownLatch(4);
        for (int i = 0; i < count; i++) {
            Thread t = new Thread(() -> {
                System.out.println("Parallel: " + Parallel.isFlag());
                latch.countDown();

                Thread.currentThread().interrupt();
            });

            t.start();
        }

        try {
            latch.await();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

    }

    private static Parallel i = new Parallel();

    public static boolean isFlag() {
        return true;
    }

    public static void run() {

    }
}

public class Main {

    public static void main(String[] args) {
        Parallel.run();
    }

}

这个returns没什么。主线程卡在latch.await();,其他线程卡在Parallel.isFlag()。编辑:如下面的 Jaims 所示,线程甚至根本没有启动。

这对我来说没有任何意义。为什么这不起作用,但第一种情况是?本质上他们在做同样的事情。

我想知道 Java 引擎如何决定何时等待和何时不等待。这可以在代码中的某处更改吗?

此外,这与 CountDownLatch 无关,仅与多线程有关。看看这个最终样本:

public class NonParallel {
    public NonParallel() {
        int count = 4;

        CountDownLatch latch = new CountDownLatch(4);
        for (int i = 0; i < count; i++) {
            System.out.println("NonParallel: " + NonParallel.isFlag());
            latch.countDown();
        }

        try {
            latch.await();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    private static NonParallel i = new NonParallel();

    public static boolean isFlag() {
        return true;
    }

    public static void run() {

    }
}

public class Main {

    public static void main(String[] args) {
        NonParallel.run();
    }

}

这很好用。输出如下:

NonParallel: true
NonParallel: true
NonParallel: true
NonParallel: true

编辑:none 当对象初始化不是 class 初始化的一部分时适用。这纯粹是关于 class 初始化,只有在使用本问题中描述的静态对象时才会发生。看这里:

public class NonStaticParallel {
    public NonStaticParallel() {
        int count = 4;

        CountDownLatch latch = new CountDownLatch(4);
        for (int i = 0; i < count; i++) {
            Thread t = new Thread(() -> {
                System.out.println("NonStaticParallel: " + isFlag());
                latch.countDown();

            });

            t.start();
        }

        try {
            latch.await();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }

    }


    public  static  boolean isFlag() {
        return true;
    }

    public static void run() {
        new NonStaticParallel();
    }

}

这个可以正常工作:

Parallel: true
Parallel: true
Parallel: true
Parallel: true

答案:

Andreas 解释了正在发生的事情。

Jaims 是正确的,线程甚至根本没有启动。这可能是因为他们需要初始化 class 并且因此立即被阻止。 (如果我们使用 运行nables 在它们自己的 classes 中而不是 lambda 或匿名内部 classes 那么它们通常 运行 当然,除非他们访问正在初始化的 class 的任何静态成员)

Yoshi 提供了 link 和规范的摘录,因此被标记为正确答案,因为这正是我想要的。

在正确创建对象之前,您的线程不会启动。考虑以下片段:

public class Main {
    public static void main(String[] args) {
        Parallel.run();
    }
}

class Parallel {
    private static Parallel i = new Parallel();
    public Parallel() {
        try {
            System.out.println("Inside constructor.");

            for (int i = 0; i < 4; i++) {
                Thread t = new Thread(() -> {
                    System.out.println("Running thread.");
                });
                System.out.println("Starting thread.");
                t.start();
            }
            System.out.println("Sleeping 2 seconds.");
            Thread.sleep(2000);
            System.out.println("Leaving constructor.");
        } catch (InterruptedException ex) {
            Logger.getLogger(Parallel.class.getName()).log(Level.SEVERE, null, ex);
        }
    }
    public static void run() {

    }
}

它将产生以下输出:

Inside constructor.
Starting thread.
Starting thread.
Starting thread.
Starting thread.
Sleeping 2 seconds.
Leaving constructor.
Running thread.
Running thread.
Running thread.
Running thread.

线程在构造函数中启动了 4 次,如输出所示。它开始休眠 2 秒,离开构造函数,然后 运行s 你的线程。不像你的线程需要 2 秒才能到达 运行。

所以你的问题的核心问题是你正在调用 latch.await(),但你的线程永远没有机会真正 运行。这意味着闩锁不会递减,只是一直等待。您可以将逻辑移至 run() 方法,但我不确定您首先要实现的目标是什么。例如

public static void run() {
    int count = 4;

    CountDownLatch latch = new CountDownLatch(4);
    for (int i = 0; i < count; i++) {
        Thread t = new Thread(() -> {
            try {
                Thread.sleep(2000);
                latch.countDown();
            } catch (InterruptedException ex) {
                Logger.getLogger(Parallel.class.getName()).log(Level.SEVERE, null, ex);
            }
        });
        System.out.println("Starting thread.");
        t.start();
    }

    try {
        System.out.println("Current count: " + latch.getCount());
        latch.await();
        System.out.println("Current count: " + latch.getCount());
    } catch (InterruptedException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
}

当您调用run()时,当前线程将开始class初始化。任何引用 class 的代码,例如调用 isFlag() 也需要 class 初始化。

在你的SimpleNonParallel版本中,当前线程正在做这一切,递归class初始化是允许的(实际上被忽略),所以isFlag()是已执行,即使 class 初始化尚未完成。

然而,在您的 Parallel 版本中,对 isFlag() 的调用是从另一个线程完成的,因此其他线程必须等待 class 完全初始化。由于您的构造函数不会 return 直到线程 运行,并且线程不能 运行 直到构造函数 return 和 完成 class初始化,你有一个死锁

结论:不能并行执行class初始化代码。 Class 初始化必须在单线程中完成。

如果需要,您可以在 class 初始化期间 启动 线程,但不能等待它们完成(如果它们也访问您的 class,他们不这样做有什么意义呢?)。

我试过你的代码并做了两件事:

  1. 首先,我将 lambda 设为 Parallel 的静态内部 class ...以防万一;这并没有改变任何东西。
  2. 由于您评论说线程卡在 Parallel.isFlag() 上,我尝试仅用 true 替换调用...并且成功了!

所以,我做了一些研究,发现了这个,这听起来像是对正在发生的事情的一个有希望的解释:http://docs.oracle.com/javase/specs/jls/se7/html/jls-12.html#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.

(强调已添加。)所以这会提出以下建议:

  1. 主线程在计算 private static Parallel i = new Parallel(); 时开始 class 初始化并启动线程。然后它等待 latch.await()。 Class Parallel 的对象应该表明初始化是 "in progress."
  2. 启动的线程还尝试引用 Parallel 的静态成员。每个线程都看到初始化正在进行并决定等到主线程(现在正在等待线程对闩锁进行倒计时)完成。显然这是一个僵局。