3

While playing with Java parallel streams, I experienced deadlocks when some parallel operations are done within a static initializer block.

When using a sequential Stream, everything works fine:

import java.util.Arrays;
public class Example1 {
    static {
        // displays the numbers from 1 to 10 ordered => no thread issue
        Arrays.asList(1,2,3,4,5,6,7,8,9,10)
             .forEach(s->System.out.println(s));
    }
    public static final void main(String[] args) {}
}

When processing the stream in parallel, everyting work (the numbers are displayed without order):

import java.util.Arrays;
public class Example2 {
    static {
        // displays the numbers from 1 to 10 unordered => no thread issue
        Arrays.asList(1,2,3,4,5,6,7,8,9,10).parallelStream()
             .forEach(s->System.out.println(s));
    }
    public static final void main(String[] args) {}
}

However, when processing the Stream with forEachOrdered(), a deadlock occurs (I suppose this is related to the interaction between the main thread and the ForkJoinPool management):

import java.util.Arrays;
public class Example3 {
    static {
        // hangs forever (deadlock between the main thread which loads the class and the underlying ForkJoinPool which join several tasks)
        Arrays.asList(1,2,3,4,5,6,7,8,9,10).parallelStream()
                .forEachOrdered(s->System.out.println(s));
    }
    public static final void main(String[] args) {}
}

But when spawning the Stream processing in a separate Thread, everything goes well:

import java.util.Arrays;
public class Example4 {
    static {
        // displays the numbers from 1 to 10 ordered => no thread issue
        new Thread(()->
            Arrays.asList(1,2,3,4,5,6,7,8,9,10).parallelStream()
                 .forEachOrdered(s->System.out.println(s))
        ).start();
    }
    public static final void main(String[] args) {}
}

From what I've seen from the Thread Dump, the main Thread is waiting on the ForkJoinPool used in the .forEachOrdered() to finish his work, but the first worker Thread in the pool is blocked waiting for something (most probably blocked by the main thread).

I would really appreciate to understand why the deadlock occurs in some cases and not in other cases. This is obviously not due only to the usage of static initializer block, parallel stream and lambda because Example2, Example3 and Example4 use these three concepts, but only Example3 causes a deadlock.

While this question may look like a duplicate of Why does parallel stream with lambda in static initializer cause a deadlock?, it is not. My question goes beyond the linked one as it provide Example2 for which we have static initializer block, parallel stream and lambda, but no deadlock. This is why the question title contains "may lead to deadlock but not necessarily".

Julien Kronegg
  • 4,282
  • 42
  • 53
  • @shmosel If you haven't seen edit, you may want to respond. – Sotirios Delimanolis Oct 30 '18 at 14:23
  • *My question goes beyond the linked one as it provide `Example2` for which we have static initializer block, parallel stream and lambda.* Technically true, but the parallel stream doesn't run inside the initializer, so why is it relevant? – shmosel Oct 30 '18 at 16:42
  • @shmosel I'm not sure to understand your comment (in `Example2`, the stream elements are processed by the main thread and by other worker threads from the `ForkJoinPool`, so the main Thread process the static initializer and the parallel stream). – Julien Kronegg Oct 31 '18 at 19:54
  • 1
    Oh, I was looking at Example4. I guess that's an interesting observation (assuming it's correct - I didn't test it). But then you didn't actually explain it in the answer. I'll reopen. – shmosel Oct 31 '18 at 19:57
  • `Example4` is easy to explain as its initializer doesn’t wait for the completion of the stream operation. The other scenarios are subject to stream implementation details and a JVM bug discussed in [Why using parallel streams in static initializer leads to not stable deadlock](https://stackoverflow.com/q/53724687/2711488). – Holger Jun 19 '19 at 09:30

1 Answers1

2

This deadlock behavior has two root causes:

  1. The main Thread is waiting that another Thread (let's say OtherThread) finishes its work (in the Example3, the OtherThread is one of the Thread of the ForkJoinPool used by the forEachOrdered() operation)
  2. The OtherThread uses a Lambda expression which will be defined by the main Thread but later (recall: Lambdas are created at runtime, not at compile time). In the Example3, this Lambda is the one in the .forEachOrdered().

Let's review the examples and explain why they produce or not a deadlock.

Example1

Only one Thread (main) does the following operations:

  1. processes the static initializer block
  2. do the foreach on each element
  3. creates the lambda expression at runtime when processing the first stream element

Since there is only one thread, no deadlock can occur.

Example2

In order to have a better understanding of the processing, we can rewrite it as :

import java.util.Arrays;
public class Example2Instrumented {
    static {
        // displays the numbers from 1 to 10 unordered => no thread issue
        System.out.println(Thread.currentThread().getName()+" : "+"static initializer");
        Arrays.asList(1,2,3,4,5,6,7,8,9,10)
             .parallelStream()
             .forEach(s->System.out.println(Thread.currentThread().getName()+" : "+s));
    }
    public static final void main(String[] args) {}
}

This produces the following result:

main : static initializer
main : 7
main : 6
ForkJoinPool.commonPool-worker-2 : 9
ForkJoinPool.commonPool-worker-4 : 5
ForkJoinPool.commonPool-worker-9 : 3
ForkJoinPool.commonPool-worker-11 : 2
ForkJoinPool.commonPool-worker-2 : 10
ForkJoinPool.commonPool-worker-4 : 4
ForkJoinPool.commonPool-worker-9 : 1
ForkJoinPool.commonPool-worker-13 : 8

The main Thread processes the static initializer, then starts the forEach and build the lambda at runtime when processing the first element. The other stream elements are processed by the workers Threads from the ForkJoinPool. There is no deadlock because the main Thread processed the first element and built the lambda.

Example3

We can rewrite Example3 without the Lambda to break the deadlock:

import java.util.Arrays;
import java.util.function.Consumer;
public class Example3NoDeadlock {
    static {
        // displays the numbers from 1 to 10 ordered => no thread issue anymore
        Arrays.asList(1,2,3,4,5,6,7,8,9,10).parallelStream()
                .forEachOrdered(
                    new Consumer<Integer>() {
                        @Override
                        public void accept(Integer t) {
                            System.out.println(t);          
                    }});
    }
    public static final void main(String[] args) {}
}

Since the Consumer Class is constructed at compile time (contrary to lambdas that are built at runtime), this breaks the deadlock cycle. This prooves that at least the lambda is involved in the deadlock.

To have a better understanding, we could instrument the code as follow:

import java.util.Arrays;
import java.util.function.Consumer;
public class Example3Instrumented {
    static {
        System.out.println("static initializer");
        // hangs forever (deadlock between the main thread which loads the class and the underlying ForkJoinPool which join several tasks)
        Arrays.asList(1,2,3,4,5,6,7,8,9,10).parallelStream()
            .peek(new Consumer<Integer>() {
                @Override
                public void accept(Integer t) {
                        System.out.println(Thread.currentThread().getName()+" "+t);
            }})
                .forEachOrdered(s->System.out.println(s));
    }
    public static final void main(String[] args) {}
}

This produces the following output:

main : static initializer
ForkJoinPool.commonPool-worker-6 1
ForkJoinPool.commonPool-worker-9 3
main 7
ForkJoinPool.commonPool-worker-4 2
ForkJoinPool.commonPool-worker-13 6
ForkJoinPool.commonPool-worker-11 8
ForkJoinPool.commonPool-worker-15 5
ForkJoinPool.commonPool-worker-2 9
ForkJoinPool.commonPool-worker-4 10
ForkJoinPool.commonPool-worker-9 4

The main Thread processes the static initializer, then starts processing the forEachOrdered by creating a Task for each element in the stream (to maintain the order, a complex tree-based algorithm is used, see ForEachOps.ForEachOrderedTask: tasks are created and it looks from the code that there is each task is waiting that another task is completed to run). All the tasks are submitted to the ForkJoinPool. I think the deadlock occures because the first Task is processed by a worker Thread from the ForkJoinPool and this Thread waits on the main Thread to build the lambda. And the main Thread has already started processing its Task and is waiting for another worker thread to complete its Task to run. Hence the deadlock.

Example4

In the Example4, we spawn a new Thread that is ran asynchronously (i.e. we don't wait for the result). This is why the main Thread is not locked and has now the time to build the Lambdas at runtime.

Conclusion

The takeaway lesson is : if you mix static initializers, threads and lambdas, you should really understand how these concepts are implemented, otherwise you may have deadlocks.

Julien Kronegg
  • 4,282
  • 42
  • 53
  • Correction: the instance for the lambda expression is always created by the main thread, as it is constructed and passed to the Stream even before the parallel operation starts. It’s the execution of its body, which gets compiled to a method of the surrounding class, which waits for the class initializer. However, it seems that its successful execution by the main thread (which is not required to wait) can bring it into a state where the other threads may execute it as well. That’s very interesting, as that behavior doesn’t seem to be backed up by the specification. – Holger Dec 11 '18 at 09:24