即使尝试从 JVM 静态初始化过程获取相同的锁,线程也不会停止
Threads not coming to a halt even when trying to acquire the same lock from the JVM static initialization process
在 Java 并发实践一书中,它说
“静态初始化程序 运行 由 JVM 在 class 初始化时 class 加载后
但在 class 被任何线程使用之前。因为 JVM 在初始化期间获取锁 [JLS 12.4.2] 并且
这个锁被每个线程至少获取一次,以确保 class 已经被加载,
静态初始化期间进行的内存写入自动对所有线程可见。”(Goetz 16.2.3)
思路一:第一种解读
我首先认为这意味着 JVM 决定看到一些 class 使用静态字段,暂时让所有线程尝试获取静态初始化使用的锁,如果该锁从未被释放, 然后它将停止所有线程以永远等待该锁。
想法 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...
引用的部分当然是关于线程 using that class,否则,没有共享数据,就没有必要讨论线程安全。
JLS§12.4.2 将获取锁描述为 class 的 初始化过程 的一部分,其中线程在检测到时必须进入阻塞状态另一个线程当前正在执行初始化:
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.
- 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.
- If the
Class
object for C
indicates that C
has already been initialized, then no further action is required. Release LC
and complete normally.
…
请注意,如果 class 结果已经初始化(在 4. 中),这意味着什么都不做,但仍然获取和释放锁,这是内存可见性约束和 happens-before Brian Goetz 提到的关系。
但作为 class 初始化的正式定义的一部分,它仅适用于实际触发 class 初始化的代码,这在 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).
因为在你的例子中,这两个线程没有执行任何指定的操作,甚至没有间接执行,它们没有触发 class 初始化,因此,没有尝试获取 class初始化锁。
您可以很容易地通过从列表中插入一个操作来阻塞示例的线程,例如
static void keepLooping() {
while (true) {
try {
Thread.sleep(1000);
new AllThreadsStopper();
} catch (InterruptedException e) {}
System.out.println("This is thread " + Thread.currentThread().getName());
}
}
由于创建 class 的实例会触发 class 的初始化,线程现在被阻塞。
为了完整起见,§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 的惰性 class 加载相结合,以创建一种不需要同步的惰性初始化技术公共代码路径”。它非常高效,因为一旦初始化完成,线程就可以访问初始化的 class 而无需同步成本。在您的示例中,初始化永远不会完成,如果线程使用 class JLS §12.4.1.
,则这种优化是不可能的,并且线程必须获取锁
在 Java 并发实践一书中,它说
“静态初始化程序 运行 由 JVM 在 class 初始化时 class 加载后 但在 class 被任何线程使用之前。因为 JVM 在初始化期间获取锁 [JLS 12.4.2] 并且 这个锁被每个线程至少获取一次,以确保 class 已经被加载, 静态初始化期间进行的内存写入自动对所有线程可见。”(Goetz 16.2.3)
思路一:第一种解读
我首先认为这意味着 JVM 决定看到一些 class 使用静态字段,暂时让所有线程尝试获取静态初始化使用的锁,如果该锁从未被释放, 然后它将停止所有线程以永远等待该锁。
想法 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...
引用的部分当然是关于线程 using that class,否则,没有共享数据,就没有必要讨论线程安全。
JLS§12.4.2 将获取锁描述为 class 的 初始化过程 的一部分,其中线程在检测到时必须进入阻塞状态另一个线程当前正在执行初始化:
For each class or interface
C
, there is a unique initialization lockLC
. The mapping fromC
toLC
is left to the discretion of the Java Virtual Machine implementation. The procedure for initializingC
is then as follows:
- Synchronize on the initialization lock,
LC
, forC
. This involves waiting until the current thread can acquireLC
.- If the
Class
object forC
indicates that initialization is in progress forC
by some other thread, then releaseLC
and block the current thread until informed that the in-progress initialization has completed, at which time repeat this step.- If the
Class
object forC
indicates that initialization is in progress forC
by the current thread, then this must be a recursive request for initialization. ReleaseLC
and complete normally.- If the
Class
object forC
indicates thatC
has already been initialized, then no further action is required. ReleaseLC
and complete normally.…
请注意,如果 class 结果已经初始化(在 4. 中),这意味着什么都不做,但仍然获取和释放锁,这是内存可见性约束和 happens-before Brian Goetz 提到的关系。
但作为 class 初始化的正式定义的一部分,它仅适用于实际触发 class 初始化的代码,这在 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 ofT
is created.- A
static
method declared byT
is invoked.- A
static
field declared byT
is assigned.- A
static
field declared byT
is used and the field is not a constant variable (§4.12.4).T
is a top level class (§7.6) and anassert
statement (§14.10) lexically nested withinT
(§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).
因为在你的例子中,这两个线程没有执行任何指定的操作,甚至没有间接执行,它们没有触发 class 初始化,因此,没有尝试获取 class初始化锁。
您可以很容易地通过从列表中插入一个操作来阻塞示例的线程,例如
static void keepLooping() {
while (true) {
try {
Thread.sleep(1000);
new AllThreadsStopper();
} catch (InterruptedException e) {}
System.out.println("This is thread " + Thread.currentThread().getName());
}
}
由于创建 class 的实例会触发 class 的初始化,线程现在被阻塞。
为了完整起见,§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 的惰性 class 加载相结合,以创建一种不需要同步的惰性初始化技术公共代码路径”。它非常高效,因为一旦初始化完成,线程就可以访问初始化的 class 而无需同步成本。在您的示例中,初始化永远不会完成,如果线程使用 class JLS §12.4.1.
,则这种优化是不可能的,并且线程必须获取锁