Jest obecnie tylko jedna naprawdę dobra odpowiedź, która wyczerpuje całkowicie Twoje pytanie i pojawiła się oficjalnie pół roku temu. Brzmi ona:
"ForkJoinPool" - z zadaniami, które dziedziczą po ForkJoinTask<V> (typowo RecursiveTask i RecursiveAction, ale nie koniecznie):
http://download.oracle.com/javase/tutorial/essential/concurrency/forkjoin.html
http://download.oracle.com/javase/7/docs/api/java/util/concurrent/ForkJoinPool.html
http://download.oracle.com/javase/7/docs/api/java/util/concurrent/ForkJoinTask.html
Trochę info z czasów, kiedy ten executor nie był jeszcze częścią Javy:
http://www.javacodegeeks.com/2011/02/java-forkjoin-parallel-programming.html
Ta pula wątków domyślnie obsługuje ilość wątków równą ilości dostępnych procesorów, ale można to zmienić i wymusić od 1 do 32K wątków w puli. Jeżeli zadania równomiernie dzielą zadanie główne na części, to każdy z wątków będzie rozplanowany przez systemowego managera wątków na osobnym rdzeniu/procesorze. W efekcie dostaniesz dokładnie to czego potrzebujesz - maksymalną wydajność skalowaną ilością rdzeni. W przypadku procesora jednojajowego wszystko sprowadzi się do sekwencyjnego wywoływania kolejnych utworzonych obiektów zadań (np. przez fork(), invoke()) bo wątek roboczy w puli będzie tylko jeden. Tak więc na takim procku będzie niewielki narzut marnowanej mocy przetwarzania, ale za to na maszynach wieloprocesorowych całe zadanie wykona się tak szybko jak dobrze uda Ci się podzielić zadanie na pod-zadania. Zadania można robić zupełnie odmienne i niezależne od siebie. W takim wypadku trudniej tak wszystko dopasować, aby ich czas wykonywania był mniej więcej podobny. Jednak nawet jeżeli to się nie uda, to każde zadanie, które się szybciej zakończy zwolni wątek, który pobierze sobie z wewnętrznej kolejki następne zadanie do wykonania. Dlatego im mniej jest dostępnych wątków, tym ważniejsze jest aby zadania były podobnej długości i jednocześnie niezbyt krótkie. Bo tylko mocno obciążone wątki są przerzucane przez systemowy manager wątków pod kontrolę różnych rdzeni. Z drugiej strony jeżeli zadań jest dużo (30 spokojnie wystarczy), to ten wykonawca niemal to gwarantuje.
Zrywa on ostatecznie powiązanie zadania z wątkiem, co jest zaletą bo wreszcie nie trzeba zajmować się zarządzaniem wątkami, na rzecz zajęcia się dobrym rozplanowaniem podziału pracy.
Teraz praktycznie każdy algorytm można spróbować przerobić na wersję równoległą. Dzięki temu wykonawcy jest to dużo łatwiejsze w Javie niż kiedykolwiek wcześniej i łatwiejsze niż w jakimkolwiek języku, który podobnego mechanizmu w ogóle nie ma.
Jakie minusy?
- Jest utrudnione korzystanie z synchronizatorów takich jak CountDownLatch, CyclicBarrier bo w skrajnym przypadku jednego wątku (czyli jednego procesora) cały mechanizm ForkJoinTasków musi ostro kombinować, żeby się nie zablokować lub nie zakleszczyć (a to kosztuje trochę magii i może się nie udać). Rozwiązanie jest proste - nie trzeba ich wcale używać.
- Przy nieumiejętnym podziale zadania - szczególnie jeżeli wadliwie wykorzysta się dynamiczne zarządzaniem podziałami zadania - można zająć za dużo zasobów na raz, co zwykle kończy się wysypką StackOverflow. Na szczęście pierwsza próba złego rozdziału od razu sypie wyjątkami, więc wiadomo że się skaszaniło. Z drugiej strony stackTrace nie za wiele daje informacji o miejscu sypnięcia ponieważ nierzadko ma naturę taką jak samo zadanie - rekurencyjną.
Ale coś za coś.
ps. Zapomniałem jeszcze napisać, że ForkJoinTask<V> posiada trzy bardzo przydatne metody "adapt", które konwertują zwykłe zadania Runnable lub Callable.