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

时间:2017-07-21 20:19:53

标签: 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.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);
        }
    }
}

2 个答案:

答案 0 :(得分:2)

是的,这是预期的行为。

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

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

在这种情况下,fork_doesNotWorkDuringClassInit()开始初始化,获取类初始化锁。但是,初始化会产生其他线程,这些线程会尝试调用lambda callable () -> i。 callable是类的成员,因此这些线程在类初始化锁上被阻塞,该初始化锁由开始初始化的线程保存。

不幸的是,初始化过程需要在完成初始化之前来自其他线程的结果。它会阻止这些结果,这些结果在初始化完成时又被阻止。线程最终陷入僵局。

有关类初始化的更多信息:

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

通常,Java初始化器和构造器在它们可以做的事情上受到限制 - 比C ++中的情况要严格得多。这可以防止某些类型的错误,但也可以限制您可以执行的操作。这是其中一个限制的一个例子。

答案 1 :(得分:1)

在类静态初始化期间,类本身会保持锁定以阻止尝试使用该类的其他线程,以便它们等待静态初始化完成。这通常被称为“classloader lock”或“static init”lock 1

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

当您触发与上面跨线程相同类型的情况时,会出现死锁,因为静态初始化锁定不会让其他线程重新进入。

1 以前的名称不一定准确,因为类加载器本身可以在内部使用其他锁来保护其结构,而不是静态初始化锁。