Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Deadlock caused by creating a new thread during class initialization

I just noticed that creating and starting a number of threads, during a class' static initialization, results in a deadlock and none of the threads getting started. This problem disappears if I run the same code dynamically, after the class has been initialized. Is this expected behavior?

Short sample program:

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);
        }
    }
}
like image 771
RvPr Avatar asked Mar 08 '23 13:03

RvPr


2 Answers

Yes, this is expected behavior.

The basic issue here is that you are attempting to access the class from another thread before class initialization is complete. It happens to be another thread that you start during class initialization, but that doesn't make any difference.

In Java, classes are initialized lazily, at first reference. When a class has not completed initialization, threads that reference the class attempt to obtain the class initialization lock. The first thread to obtain the class initialization lock initializes the thread, and that initialization must complete before the other threads can proceed.

In this case, fork_doesNotWorkDuringClassInit() starts initialization, obtaining the class initialization lock. However, initialization spawns additional threads, which attempt to call the lambda callable () -> i. The callable is a member of the class, so these threads are then blocked on the class initialization lock, which is held by the thread that started the initialization.

Unfortunately, your initialization process requires the results from the other threads before initialization can be completed. It blocks on those results, which are in turn blocked on the initialization completion. The threads end up deadlocked.

More information on class initialization here:

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

In general, Java initializers and constructors are limited in what they can do - considerably more limited than is the case in, say, C++. This can prevent certain types of errors, but can also restrict what you can do. This is an example of one of those restrictions.

like image 138
Warren Dew Avatar answered Apr 25 '23 06:04

Warren Dew


During class static initialization, a lock is held on the class itself to block other threads which try to use the class so they wait for the static initialization to complete. This is often referred to as the "classloader lock" or the "static init" lock1.

If the code doing static initialization calls tries to access other static state of the class on the same thread, you won't get a deadlock since the lock is recursive and allows the owning thread back in: this is required by the JLS. This also applies to recursive initialization where the static init for class A ends up triggering the initialization of class B whose static init ultimately accesses static state in class A. While it won't deadlock, you'll often see the default values (e.g., null, 0, etc) for static members which haven't been initialized yet.

When you trigger the same type of situation as describe above across threads, you get a deadlock, since the static init lock won't let other threads back in.


1 The former name is not necessary accurate since the classloader itself may internally use other locks to protect its structures beyond the static init lock.

like image 36
BeeOnRope Avatar answered Apr 25 '23 07:04

BeeOnRope