Java 类初始化期间创建新线程导致的死锁

Java 类初始化期间创建新线程导致的死锁,java,multithreading,concurrency,thread-safety,Java,Multithreading,Concurrency,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

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

简短示例程序:

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);
        }
    }
}
package com.my.pkg;
导入com.google.common.truth.truth;
导入org.junit.Test;
导入java.util.Collection;
导入java.util.concurrent.Callable;
导入java.util.concurrent.Future;
导入java.util.concurrent.FutureTask;
导入java.util.stream.collector;
导入java.util.stream.IntStream;
公共类MyClass{
私有静态最终集合NUMS=getNums();
@试验
public void fork_在ClassInit()期间不起作用{
//如果您还从上面删除NUM,则此操作有效:
//断言(getNums()).containsExactly(0,1,2,3,4);
真理。断言(NUMS)。包含(0,1,2,3,4);
}
私有静态集合getNums(){
返回IntStream.range(0,5)
.mapToObj(i->fork(()->i))
.map(MyClass::get)
.collect(Collectors.toList());
}
公共静态FutureTask fork(可调用可调用){
FutureTask FutureTask=新的FutureTask(可调用);
线程线程=新线程(futureTask);
thread.start();
返回未来任务;
}
公共静态T get(未来){
试一试{
返回future.get();
}捕获(例外e){
抛出新的运行时异常(e);
}
}
}

是的,这是预期的行为

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

在Java中,类在第一次引用时是惰性初始化的。当类尚未完成初始化时,引用该类的线程将尝试获取类初始化锁。获取类初始化锁的第一个线程初始化该线程,该初始化必须在其他线程继续之前完成

在这种情况下,
fork\u在ClassInit()期间不工作。
开始初始化,获得类初始化锁。但是,初始化会产生额外的线程,这些线程尝试调用lambda callable
()->i
。可调用线程是类的成员,因此这些线程随后在类初始化锁上被阻塞,该锁由启动初始化的线程持有

不幸的是,初始化过程需要其他线程的结果才能完成初始化。它会阻止这些结果,而这些结果又会在初始化完成时被阻止。线程以死锁告终

有关类初始化的详细信息,请参见此处:


一般来说,java初始化器和构造函数在它们能做的方面受到限制,比C++中的情况要有限得多。这可以防止某些类型的错误,但也可以限制您可以执行的操作。这是其中一个限制的示例。

在类静态初始化期间,类本身会被锁定,以阻止尝试使用该类的其他线程,从而等待静态初始化完成。这通常被称为“类加载器锁”或“静态初始化”锁1

如果执行静态初始化调用的代码尝试访问同一线程上类的其他静态状态,则不会出现死锁,因为锁是递归的,并允许所属线程返回:这是JLS所要求的。这也适用于递归初始化,其中
类A
的静态初始化最终触发
类B
的初始化,其静态初始化最终访问
类A
中的静态。虽然它不会死锁,但您经常会看到尚未初始化的静态成员的默认值(例如,
null
0
,等等)

当您跨线程触发上述相同类型的情况时,就会出现死锁,因为静态init锁不会让其他线程返回



1前一个名称不一定准确,因为类加载器本身可能在内部使用其他锁来保护静态init锁之外的结构

Thread=新线程(未来任务)==>错误<代码>未来=执行者提交(可调用)==>很好。你不应该手动启动线程,使用。不,那不是一个程序。这甚至不是一门完整的课。如果你需要疑难解答帮助,请给出一个答案。@alfasin,这是一个教条式的回答。使用
ExecutorService
是有原因的,创建裸
Thread
对象也是有原因的。一些反对调用
新线程(…)
的论点可以通过调用
threadFactory.newThread(…)
来解决。我对流不太了解,但这看起来像是一个禁忌:
.collector(Collectors.toList()).stream()
。“我错了,还是只是一个关于把T流转换成T流的方法吗?@上面的示例程序Alfain不是为了成为好代码。更确切地说,这是为了突出我试图更好地理解的令人惊讶的行为。非常有趣。不过有一个问题。您提到:“初始化会产生额外的线程,这些线程尝试调用类的get()方法。然后,这些线程在类初始化锁上被阻止”。即使我将get()方法完全移到类之外,死锁仍然会发生。为什么呢?为什么新线程继续被类初始化锁阻止,即使它们没有访问任何类v