类初始化时创建新线程导致死锁

RvP*_*vPr 4 java concurrency multithreading thread-safety

我只是注意到在类的静态初始化期间创建和启动多个线程会导致死锁,并且没有任何线程启动。如果我在类初始化后动态运行相同的代码,这个问题就会消失。这是预期的行为吗?

简短的示例程序:

package com.my.pkg;

import com.google.common.truth.Truth;
import org.junit.Test;

import java.util.Collection;
import java.util.concurrent.Callable;
import java.util.concurrent.Future;
import java.util.concurrent.FutureTask;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

public class MyClass {
    private static final Collection<Integer> NUMS = getNums();

    @Test
    public void fork_doesNotWorkDuringClassInit() {
        // This works if you also delete NUMS from above: 
        // Truth.assertThat(getNums()).containsExactly(0, 1, 2, 3, 4);
        Truth.assertThat(NUMS).containsExactly(0, 1, 2, 3, 4);
    }

    private static Collection<Integer> getNums() {
        return IntStream.range(0, 5)
                        .mapToObj(i -> fork(() -> i))
                        .map(MyClass::get)
                        .collect(Collectors.toList());
    }

    public static <T> FutureTask<T> fork(Callable<T> callable) {
        FutureTask<T> futureTask = new FutureTask<>(callable);
        Thread thread = new Thread(futureTask);
        thread.start();
        return futureTask;
    }

    public static <T> T get(Future<T> future) {
        try {
            return future.get();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

War*_*Dew 6

是的,这是预期的行为。

这里的基本问题是您在类初始化完成之前尝试从另一个线程访问该类。它恰好是您在类初始化期间启动的另一个线程,但这没有任何区别。

在 Java 中,类在第一次引用时被延迟初始化。当类尚未完成初始化时,引用该类的线程会尝试获取类初始化锁。获得类初始化锁的第一个线程初始化该线程,并且该初始化必须在其他线程可以继续之前完成。

在这种情况下,fork_doesNotWorkDuringClassInit()开始初始化,获取类初始化锁。但是,初始化会产生额外的线程,这些线程尝试调用 lambda callable () -> i。可调用对象是类的成员,因此这些线程随后在类初始化锁上被阻塞,该锁由启动初始化的线程持有。

不幸的是,您的初始化过程需要其他线程的结果才能完成初始化。它阻塞那些结果,而这些结果又在初始化完成时阻塞。线程最终陷入僵局。

关于类初始化的更多信息在这里:

http://docs.oracle.com/javase/specs/jls/se8/html/jls-12.html#jls-12.4.2

一般来说,Java 初始化器和构造器的功能有限——比 C++ 中的情况要有限得多。这可以防止某些类型的错误,但也可以限制您可以执行的操作。这是这些限制之一的示例。