In the next years we will see an increasing number of cores in standard desktops, notebooks and server computers. The reason for this is easy: It’s cheaper to add additional cores than to build a faster single processor. So, we will have to write more software which supports concurrency to take benefit of better hardware.
To be honest, I don’t like concurrency. My personal rule is „You need a good reason to implement concurrency and if you have to do it be really careful.“ In the last years, I saw more buggy implementations than working. This is the reason why I like the fork & join library. A clear programming model which implements the boiler plate code prevents you from errors. But, please if you intend to use fork and join take some time to understand the behavior.
The sample in file #1 and #2 is very similar to the sample code in Java 7 documentation. In general, Fibonacci numbers with a recursive algorithm is not a good idea because there is a better linear solution (compare http://nayuki.eigenstate.org/page/fast-fibonacci-algorithms), but it is easier to implement and understand than others. So, let’s have a look at the sample:
// File #1: FibonacciTask.java [error handling, parameter validation and asserts removed] package com.sprunck.sample; import java.util.concurrent.RecursiveTask; public class FibonacciTask extends RecursiveTask<Long> { private static final long serialVersionUID = 1L; private final long inputValue; public FibonacciTask(long inputValue) { this.inputValue = inputValue; } @Override public Long compute() { if (inputValue == 0L) { return 0L; } else if (inputValue <= 2L) { return 1L; } else { final FibonacciTask firstWorker = new FibonacciTask(inputValue - 1L); firstWorker.fork(); final FibonacciTask secondWorker = new FibonacciTask(inputValue - 2L); return secondWorker.compute() + firstWorker.join(); } } }
// File #2: FibonacciTaskTest.java package com.sprunck.sample; import java.util.concurrent.ForkJoinPool; import junit.framework.Assert; import org.junit.Test; public class FibonacciTaskTest { // it makes no sense to create more threads than available cores (no speed improvement here) private static final int AVAILABLE_PROCESSORS = Runtime.getRuntime().availableProcessors(); // create thread pool private final ForkJoinPool pool = new ForkJoinPool(AVAILABLE_PROCESSORS); @Test public void testFibonacciArray() { // more test data: http://www.maths.surrey.ac.uk/hosted-sites/R.Knott/Fibonacci/fibtable.html long results[] = { 0L, 1L, 1L, 2L, 3L, 5L, 8L, 13L, 21L, 34L, 55L, 89L, 144L, 233L, 377L, 610L, 987L, 1597L, 2584L, 4181L, 6765L }; for (int inputValue = 0; inputValue < results.length; inputValue++) { final FibonacciTask task = new FibonacciTask(inputValue); System.out.print("Fibonacci(" + inputValue + ") = "); final long result = pool.invoke(task); System.out.println(result); Assert.assertEquals(results[inputValue], result); } } }
// Output of FibonacciTaskTest.java
Fibonacci(0) = 0 Fibonacci(1) = 1 Fibonacci(2) = 1 Fibonacci(3) = 2 Fibonacci(4) = 3 Fibonacci(5) = 5 Fibonacci(6) = 8 Fibonacci(7) = 13 Fibonacci(8) = 21 Fibonacci(9) = 34 Fibonacci(10) = 55 Fibonacci(11) = 89 Fibonacci(12) = 144 Fibonacci(13) = 233 Fibonacci(14) = 377 Fibonacci(15) = 610 Fibonacci(16) = 987 Fibonacci(17) = 1597 Fibonacci(18) = 2584 Fibonacci(19) = 4181 Fibonacci(20) = 6765
So far it is a simple and clear solution. No boiler plate code for concurrency, e.g. thread synchronization.
But I’d like to encourage you to have a deeper look in what happens in the solution. In files #3 and #4 you find an enhanced version of the same program. The only difference between the first and second version is some code to trace what happens during execution and a small slowTask() to simulate more realistic behavior.
// File #3: FibonacciTaskTraces.java package com.sprunck.sample; import java.util.concurrent.RecursiveTask; public class FibonacciTaskTraces extends RecursiveTask<Long> { private static final long serialVersionUID = 1L; // just needed to format debug output public static final String OUTPUT_PREFIX = " | "; private final String prefix; private final long inputValue; public FibonacciTaskTraces(long inputValue, final String prefix) { this.inputValue = inputValue; this.prefix = prefix; } @Override public Long compute() { if (inputValue == 0L) { slowTask(); return 0L; } else if (inputValue <= 2L) { slowTask(); return 1L; } else { final long firstValue = inputValue - 1L; System.out.println(prefix + " - Fibonacci(" + firstValue + ") <- " + Thread.currentThread().getName() + " (fork) "); final FibonacciTaskTraces firstWorker = new FibonacciTaskTraces(firstValue, prefix + OUTPUT_PREFIX); firstWorker.fork(); final long secondValue = inputValue - 2L; System.out.println(prefix + " - Fibonacci(" + secondValue + ") <- " + Thread.currentThread().getName()); final FibonacciTaskTraces secondWorker = new FibonacciTaskTraces(secondValue, prefix + OUTPUT_PREFIX); long result = secondWorker.compute() + firstWorker.join(); System.out.println(prefix + " - Fibonacci(" + inputValue + ") = " + result + " <- " + Thread.currentThread().getName() + " (join)"); slowTask(); return result; } } /** just simulate a longer running task (with out disturbing the other threads) */ private void slowTask() { for (int k = 0, i = 0; i < 1000 * 1000 * 100; i++) { i = i + k; } } }
// File #4: FibonacciTaskTracesTask.java package com.sprunck.sample; import java.util.concurrent.ForkJoinPool; import junit.framework.Assert; import org.junit.Test; public class FibonacciTaskTracesTest { // it makes no sense to create more threads than available cores (no speed improvement here) private static final int AVAILABLE_PROCESSORS = Runtime.getRuntime().availableProcessors(); // create thread pool private final ForkJoinPool pool = new ForkJoinPool(AVAILABLE_PROCESSORS); @Test public void testFibonacciArrayTraces() { // more test data: http://www.maths.surrey.ac.uk/hosted-sites/R.Knott/Fibonacci/fibtable.html long results[] = { 0L, 1L, 1L, 2L, 3L, 5L, 8L, 13L }; for (int inputValue = 0; inputValue < results.length; inputValue++) { final FibonacciTaskTraces task = new FibonacciTaskTraces(inputValue, " | "); System.out.println("invoke Fibonacci(" + inputValue + ") <- " + Thread.currentThread().getName()); final long result = pool.invoke(task); System.out.println("result = " + result + "\n"); Assert.assertEquals(results[inputValue], result); } } }
// Output of FibonacciTaskTracesTest.java
invoke Fibonacci(0) <- main result = 0 invoke Fibonacci(1) <- main result = 1 invoke Fibonacci(2) <- main result = 1 invoke Fibonacci(3) <- main | - Fibonacci(2) <- ForkJoinPool-1-worker-1 (fork) | - Fibonacci(1) <- ForkJoinPool-1-worker-1 | - Fibonacci(3) = 2 <- ForkJoinPool-1-worker-1 (join) result = 2 invoke Fibonacci(4) <- main | - Fibonacci(3) <- ForkJoinPool-1-worker-1 (fork) | - Fibonacci(2) <- ForkJoinPool-1-worker-1 | | - Fibonacci(2) <- ForkJoinPool-1-worker-2 (fork) | | - Fibonacci(1) <- ForkJoinPool-1-worker-2 | | - Fibonacci(3) = 2 <- ForkJoinPool-1-worker-2 (join) | - Fibonacci(4) = 3 <- ForkJoinPool-1-worker-1 (join) result = 3 invoke Fibonacci(5) <- main | - Fibonacci(4) <- ForkJoinPool-1-worker-1 (fork) | - Fibonacci(3) <- ForkJoinPool-1-worker-1 | | - Fibonacci(2) <- ForkJoinPool-1-worker-1 (fork) | | - Fibonacci(1) <- ForkJoinPool-1-worker-1 | | - Fibonacci(3) <- ForkJoinPool-1-worker-2 (fork) | | - Fibonacci(2) <- ForkJoinPool-1-worker-2 | | | - Fibonacci(2) <- ForkJoinPool-1-worker-2 (fork) | | | - Fibonacci(1) <- ForkJoinPool-1-worker-2 | | - Fibonacci(3) = 2 <- ForkJoinPool-1-worker-1 (join) | | | - Fibonacci(3) = 2 <- ForkJoinPool-1-worker-2 (join) | | - Fibonacci(4) = 3 <- ForkJoinPool-1-worker-2 (join) | - Fibonacci(5) = 5 <- ForkJoinPool-1-worker-1 (join) result = 5 invoke Fibonacci(6) <- main | - Fibonacci(5) <- ForkJoinPool-1-worker-1 (fork) | - Fibonacci(4) <- ForkJoinPool-1-worker-1 | | - Fibonacci(3) <- ForkJoinPool-1-worker-1 (fork) | | - Fibonacci(2) <- ForkJoinPool-1-worker-1 | | - Fibonacci(4) <- ForkJoinPool-1-worker-2 (fork) | | - Fibonacci(3) <- ForkJoinPool-1-worker-2 | | | - Fibonacci(2) <- ForkJoinPool-1-worker-2 (fork) | | | - Fibonacci(1) <- ForkJoinPool-1-worker-2 | | | - Fibonacci(2) <- ForkJoinPool-1-worker-1 (fork) | | | - Fibonacci(1) <- ForkJoinPool-1-worker-1 | | | - Fibonacci(3) = 2 <- ForkJoinPool-1-worker-2 (join) | | | - Fibonacci(3) = 2 <- ForkJoinPool-1-worker-1 (join) | | | - Fibonacci(3) <- ForkJoinPool-1-worker-2 (fork) | | | - Fibonacci(2) <- ForkJoinPool-1-worker-2 | | - Fibonacci(4) = 3 <- ForkJoinPool-1-worker-1 (join) | | | | - Fibonacci(2) <- ForkJoinPool-1-worker-2 (fork) | | | | - Fibonacci(1) <- ForkJoinPool-1-worker-2 | | | | - Fibonacci(3) = 2 <- ForkJoinPool-1-worker-2 (join) | | | - Fibonacci(4) = 3 <- ForkJoinPool-1-worker-2 (join) | | - Fibonacci(5) = 5 <- ForkJoinPool-1-worker-2 (join) | - Fibonacci(6) = 8 <- ForkJoinPool-1-worker-1 (join) result = 8 invoke Fibonacci(7) <- main | - Fibonacci(6) <- ForkJoinPool-1-worker-1 (fork) | - Fibonacci(5) <- ForkJoinPool-1-worker-1 | | - Fibonacci(4) <- ForkJoinPool-1-worker-1 (fork) | | - Fibonacci(3) <- ForkJoinPool-1-worker-1 | | | - Fibonacci(2) <- ForkJoinPool-1-worker-1 (fork) | | | - Fibonacci(1) <- ForkJoinPool-1-worker-1 | | - Fibonacci(5) <- ForkJoinPool-1-worker-2 (fork) | | - Fibonacci(4) <- ForkJoinPool-1-worker-2 | | | - Fibonacci(3) <- ForkJoinPool-1-worker-2 (fork) | | | - Fibonacci(2) <- ForkJoinPool-1-worker-2 | | | | - Fibonacci(2) <- ForkJoinPool-1-worker-2 (fork) | | | | - Fibonacci(1) <- ForkJoinPool-1-worker-2 | | | - Fibonacci(3) = 2 <- ForkJoinPool-1-worker-1 (join) | | | - Fibonacci(3) <- ForkJoinPool-1-worker-1 (fork) | | | - Fibonacci(2) <- ForkJoinPool-1-worker-1 | | | | - Fibonacci(3) = 2 <- ForkJoinPool-1-worker-2 (join) | | | | - Fibonacci(2) <- ForkJoinPool-1-worker-1 (fork) | | | | - Fibonacci(1) <- ForkJoinPool-1-worker-1 | | | - Fibonacci(4) = 3 <- ForkJoinPool-1-worker-2 (join) | | | - Fibonacci(4) <- ForkJoinPool-1-worker-2 (fork) | | | - Fibonacci(3) <- ForkJoinPool-1-worker-2 | | | | - Fibonacci(2) <- ForkJoinPool-1-worker-2 (fork) | | | | - Fibonacci(1) <- ForkJoinPool-1-worker-2 | | | | - Fibonacci(3) = 2 <- ForkJoinPool-1-worker-1 (join) | | | - Fibonacci(4) = 3 <- ForkJoinPool-1-worker-1 (join) | | | | - Fibonacci(3) = 2 <- ForkJoinPool-1-worker-2 (join) | | - Fibonacci(5) = 5 <- ForkJoinPool-1-worker-1 (join) | | | | - Fibonacci(3) <- ForkJoinPool-1-worker-2 (fork) | | | | - Fibonacci(2) <- ForkJoinPool-1-worker-2 | | | | | - Fibonacci(2) <- ForkJoinPool-1-worker-1 (fork) | | | | | - Fibonacci(1) <- ForkJoinPool-1-worker-1 | | | | | - Fibonacci(3) = 2 <- ForkJoinPool-1-worker-1 (join) | | | | - Fibonacci(4) = 3 <- ForkJoinPool-1-worker-2 (join) | | | - Fibonacci(5) = 5 <- ForkJoinPool-1-worker-2 (join) | | - Fibonacci(6) = 8 <- ForkJoinPool-1-worker-2 (join) | - Fibonacci(7) = 13 <- ForkJoinPool-1-worker-1 (join) result = 13
The output gives you now deeper look into the processing of the program. Following ways of Fibonacci numbers calculation appear:
- the first three Fibonacci numbers are processed in the main thread,
- the next Fibonacci number has been processed in just one new thread (ForkJoinPool-1-worker-1) and
- starting wiht the fifth Fibonacci number two threads (ForkJoinPool-1-worker-1 and ForkJoinPool-1-worker-2) have been used.
The algorithm is inefficient, because there are a lot of redundant operations (re-calculation of the same Fibonacci number) in the processing. In a real life application you should be careful with this kind of inefficient algorithms. Some traces help to understand what happens.
Recomendations
- The use of fork and join is easy and straightforward, but use some time to trace and understand your implementation.
- Sometimes it is helpful to implement two versions of the same algorithm (one for analysis and a second for production).
- Spend some time in designing and understanding better concurrency algorithms is a good investment.
The probability calculator has been developed in this way (Demo of probability calculator – PCALC).
Reference: How to implement fork and join in Java 7 – JSR 166 concurrency utilities ? from our JCG partner Markus Sprunck at the Software Engineering Candies blog.
Bulletproof Java Code: A Practical Strategy for Developing Functional, Reliable, and Secure Java CodeUse Java? If you do, you know that Java software can be used to drive application logic of Web services or Web applications. Perhaps you use it for desktop applications? Or, embedded devices? Whatever your use of Java code, functional errors are the enemy! To combat this enemy, your team might already perform functional testing. Even so, you're taking significant risks if you have not yet implemented a comprehensive team-wide quality management strategy. Such a strategy alleviates reliability, security, and performance problems to ensure that your code is free of functionality errors.Read this article to learn about this simple four-step strategy that is proven to make Java code more reliable, more secure, and easier to maintain. |