26

I have collection of elements that I want to process in parallel. When I use a List, parallelism works. However, when I use a Set, it does not run in parallel.

I wrote a code sample that shows the problem:

public static void main(String[] args) {
    ParallelTest test = new ParallelTest();

    List<Integer> list = Arrays.asList(1,2);
    Set<Integer> set = new HashSet<>(list);

    ForkJoinPool forkJoinPool = new ForkJoinPool(4);

    System.out.println("set print");
    try {
        forkJoinPool.submit(() ->
            set.parallelStream().forEach(test::print)
        ).get();
    } catch (Exception e) {
        return;
    }

    System.out.println("\n\nlist print");
    try {
        forkJoinPool.submit(() ->
            list.parallelStream().forEach(test::print)
        ).get();
    } catch (Exception e) {
        return;
    }   
}

private void print(int i){
    System.out.println("start: " + i);
    try {
        TimeUnit.SECONDS.sleep(1);
    } catch (InterruptedException e) {
    }
    System.out.println("end: " + i);
}

This is the output that I get on windows 7

set print
start: 1
end: 1
start: 2
end: 2

list print
start: 2
start: 1
end: 1
end: 2

We can see that the first element from the Set had to finish before the second element is processed. For the List, the second element starts before the first element finishes.

Can you tell me what causes this issue, and how to avoid it using a Set collection?

Stuart Marks
  • 127,867
  • 37
  • 205
  • 259
Nemo
  • 587
  • 6
  • 12
  • try it with more than two elements, like 10. elements or something. the results with 2 is too vague – nafas Mar 11 '15 at 11:44
  • When you try with 10 you still cannot parallel all set elements. And I need to run all elements in parallel. – Nemo Mar 11 '15 at 11:46
  • Any way this is the output for 10 (with pool of 10 executors) elements set print start: 8 start: 0 start: 4 start: 6 start: 2 end: 2 end: 6 end: 4 end: 0 start: 1 end: 8 start: 9 start: 5 start: 7 start: 3 end: 3 end: 5 end: 9 end: 7 end: 1 list print start: 7 start: 3 start: 0 start: 6 start: 9 start: 8 start: 5 start: 4 start: 2 start: 1 end: 0 end: 6 end: 7 end: 9 end: 2 end: 3 end: 8 end: 5 end: 1 end: 4 Not all set elements run in parallel – Nemo Mar 11 '15 at 11:51

1 Answers1

37

I can reproduce the behavior you see, where the parallelism doesn't match the parallelism of the fork-join pool parallelism you've specified. After setting the fork-join pool parallelism to 10, and increasing the number of elements in the collection to 50, I see the parallelism of the list-based stream rising only to 6, whereas the parallelism of the set-based stream never gets above 2.

Note, however, that this technique of submitting a task to a fork-join pool to run the parallel stream in that pool is an implementation "trick" and is not guaranteed to work. Indeed, the threads or thread pool that is used for execution of parallel streams is unspecified. By default, the common fork-join pool is used, but in different environments, different thread pools might end up being used. (Consider a container within an application server.)

In the java.util.stream.AbstractTask class, the LEAF_TARGET field determines the amount of splitting that is done, which in turn determines the amount of parallelism that can be achieved. The value of this field is based on ForkJoinPool.getCommonPoolParallelism() which of course uses the parallelism of the common pool, not whatever pool happens to be running the tasks.

Arguably this is a bug (see OpenJDK issue JDK-8190974), however, this entire area is unspecified anyway. However, this area of the system definitely needs development, for example in terms of splitting policy, the amount of parallelism available, dealing with blocking tasks, among other issues. A future release of the JDK may address some of these issues.

Meanwhile, it is possible to control the parallelism of the common fork-join pool through the use of system properties. If you add this line to your program,

System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "10");

and you run the streams in the common pool (or if you submit them to your own pool that has a sufficiently high level of parallelism set) you will observe that many more tasks are run in parallel.

You can also set this property on the command line using the -D option.

Again, this is not guaranteed behavior, and it may change in the future. But this technique will probably work for JDK 8 implementations for the forseeable future.

UPDATE 2019-06-12: The bug JDK-8190974 was fixed in JDK 10, and the fix has been backported to an upcoming JDK 8u release (8u222).

Stuart Marks
  • 127,867
  • 37
  • 205
  • 259
  • Coming from [here](http://stackoverflow.com/questions/36947336/why-does-the-parallel-stream-not-use-all-the-threads-of-the-forkjoinpool?noredirect=1), question about _Arguably this is a bug_: would the fix be to use the parallelism of the current `ForkJoinPool` (default or otherwise) or something else? – Sotirios Delimanolis Apr 30 '16 at 00:13
  • Nope, not a bug. This is how `ForkJoinPool` works - it has something like a back-pressure mechanism against unnecessary thread proliferation. I've added an answer to the question you've linked, explaining that behavior. – Dimitar Dimitrov Apr 30 '16 at 00:36
  • 1
    @SotiriosDelimanolis See Dimitar's comment here too. I see you guys also discussing this on the [other question](http://stackoverflow.com/questions/36947336/why-does-the-parallel-stream-not-use-all-the-threads-of-the-forkjoinpool) – Stuart Marks Apr 30 '16 at 01:42
  • 3
    @DimitarDimitrov I think this is simpler than how you're making it out to be. The "arguably this is a bug" statement is in regard to the splitting behavior within streams. It always splits based on the parallelism of the common pool. But if the stream is targeted toward another pool (using an undocumented hack) the splitting is still governed by the parallelism of the common pool, not that of the targeted pool. – Stuart Marks Apr 30 '16 at 01:46
  • 1
    @StuartMarks Yep, you are exactly right about the `AbstractTask` behavior and my answer about the `ForkJoinPool` back-pressure being the root cause is wrong. Thanks you guys for bearing with me - I'll update my answer accordingly. As for @SotiriosDelimanolis's comment here, regardless of whether this can be classified as a bug and whether using a non-default pool should be supported, fixing this may need more substantial changes, as right now the stream doesn't know about the parallelism level of the pool in which it will be running. – Dimitar Dimitrov Apr 30 '16 at 08:40
  • But why the behavior is different for set and list? – nanpakal Aug 12 '17 at 07:10
  • 1
    @pppavan Probably because the elements of an ArrayList are densely packed in an array, so every split is full. Elements of a HashSet are spread relatively sparsely across buckets in its table. – Stuart Marks Aug 12 '17 at 17:40
  • @StuartMarks I do set the `System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism", "10");` but while in debug mode if I query the `ForkJoinPool.commonPool().getParallelism()` I get the result as 7 ( which is processor count - 1 ). Why is that ? – d-coder Jun 12 '19 at 12:30
  • 1
    @d-coder Probably because the FJ pool has been created already. Once it's created you can't change its size by changing the property. Try setting the property at the top of `main()`. I don't think anything prior to `main()` creates an FJ pool (but this might vary depending on the JDK version). If that fails, try setting the property on the command line. – Stuart Marks Jun 12 '19 at 17:30