从另一个线程初始化相同 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 初始化。
在你的Simple
和NonParallel
版本中,当前线程正在做这一切,递归class初始化是允许的(实际上被忽略),所以isFlag()
是已执行,即使 class 初始化尚未完成。
然而,在您的 Parallel
版本中,对 isFlag()
的调用是从另一个线程完成的,因此其他线程必须等待 class 完全初始化。由于您的构造函数不会 return 直到线程 运行,并且线程不能 运行 直到构造函数 return 和 完成 class初始化,你有一个死锁。
结论:不能并行执行class初始化代码。 Class 初始化必须在单线程中完成。
如果需要,您可以在 class 初始化期间 启动 线程,但不能等待它们完成(如果它们也访问您的 class,他们不这样做有什么意义呢?)。
我试过你的代码并做了两件事:
- 首先,我将 lambda 设为
Parallel
的静态内部 class ...以防万一;这并没有改变任何东西。
- 由于您评论说线程卡在
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:
Synchronize on the initialization lock, LC, for C. This involves waiting until the current thread can acquire LC.
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.
(强调已添加。)所以这会提出以下建议:
- 主线程在计算
private static Parallel i = new Parallel();
时开始 class 初始化并启动线程。然后它等待 latch.await()
。 Class Parallel
的对象应该表明初始化是 "in progress."
- 启动的线程还尝试引用
Parallel
的静态成员。每个线程都看到初始化正在进行并决定等到主线程(现在正在等待线程对闩锁进行倒计时)完成。显然这是一个僵局。
我遇到了一个非常奇怪的问题,除了将问题分成两个 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();
,其他线程卡在。编辑:如下面的 Jaims 所示,线程甚至根本没有启动。Parallel.isFlag()
这对我来说没有任何意义。为什么这不起作用,但第一种情况是?本质上他们在做同样的事情。
我想知道 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 初始化。
在你的Simple
和NonParallel
版本中,当前线程正在做这一切,递归class初始化是允许的(实际上被忽略),所以isFlag()
是已执行,即使 class 初始化尚未完成。
然而,在您的 Parallel
版本中,对 isFlag()
的调用是从另一个线程完成的,因此其他线程必须等待 class 完全初始化。由于您的构造函数不会 return 直到线程 运行,并且线程不能 运行 直到构造函数 return 和 完成 class初始化,你有一个死锁。
结论:不能并行执行class初始化代码。 Class 初始化必须在单线程中完成。
如果需要,您可以在 class 初始化期间 启动 线程,但不能等待它们完成(如果它们也访问您的 class,他们不这样做有什么意义呢?)。
我试过你的代码并做了两件事:
- 首先,我将 lambda 设为
Parallel
的静态内部 class ...以防万一;这并没有改变任何东西。 - 由于您评论说线程卡在
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:
Synchronize on the initialization lock, LC, for C. This involves waiting until the current thread can acquire LC.
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.
(强调已添加。)所以这会提出以下建议:
- 主线程在计算
private static Parallel i = new Parallel();
时开始 class 初始化并启动线程。然后它等待latch.await()
。 ClassParallel
的对象应该表明初始化是 "in progress." - 启动的线程还尝试引用
Parallel
的静态成员。每个线程都看到初始化正在进行并决定等到主线程(现在正在等待线程对闩锁进行倒计时)完成。显然这是一个僵局。