So let's see how we can use the ForkJoinPool to run multiple threads in our
application. So this is the code that we just saw on the slide.
And what this code will do is sum up all the values in an array. To
do this, we have a method called compute, and compute takes an
array of values and then two integers,
the start and the end. And this is the start point and the
endpoint within the array that we are going to run this function
over. This function is called recursively.
So we break the array down into blocks.
And then for each of those blocks,
we call the function, passing in the whole array and then the
start point and the endpoint of the block that we'd like to
compute. Down here inside main,
we call compute, passing it this array of integers.
The start point is 0, and the endpoint is the size of the
array. Within compute, if the range, i.e. end ‑ start is
less than sum at threshold, then we sum up the values within that range.
If the range is outside that threshold,
then we break the range into two parts, a left and a right, and
we call compute recursively on those parts. And we are breaking
this rate into 19 parts here.
Why 19?
Well, I'll explain that in the next clip. There's a println here
that you can uncomment if you run these demos yourself, and that
will print out the name of the thread on which this quarter
compute is currently running.
Now initially,
we are running on a single thread, so that will just print out
the same thread name over, and over, and over.
If I look back inside main, we call compute twice. The first time calling it,
I'm just warming up the Java virtual machine.
I want to make sure that things like the just‑in‑time compiler have been
given enough time to do their work before we actually measure the time that
this computation takes. For this run of the code,
we are going to compute 500,000,000 values in the array.
So let me run this. Now this will take a few
seconds. The first time we run compute,
it takes a little while to warm up the JVM, but I'm only
measuring the time for the second run of compute.
And we can see the computation runs in about 1.3
seconds, and the sum is correct.
Each element in the array holds the value 1.
And if we sum up 500,000,000 elements, we should get the answer 500,000,000.
So can we better that time? This code is logically
the same as the previous code.
But here, we are going to use the ForkJoinPool and multiple
threads to do the calculation for us.
So now, when I break the array down into chunks,
the calculation for each of those chunks will be passed off to its own thread.
So rather than having one thread doing the calculation,
we'll have many threads doing the calculation. To kick this off, I call
ForkJoinPool.commonPool().invoke, and I pass it a reference to this Sum
object. And the Sum object derives from RecursiveTask.
And this is what we need to execute things inside the ForkJoinPool.
This class has a method called compute that returns
the result of the computation.
Now inside here,
what we are doing is we're splitting the array into
values, again, based on this threshold.
And then we are calling compute on those individual blocks of
values. That compute is handed off to a thread.
When that thread finishes,
we add its answer to the ongoing answer, and we should end up with
the same value, but this should take less time.
Why this limit of 19? Well, on this machine,
this is a Mac M1 Ultra. I have 20 cores, and the ForkJoin common
pool lets me use 19 of those for computation.
Hence,
we can break this array into 19 parts and then calculate
each of those blocks on its own thread.
Again, I'm starting this twice.
So I call Sum.sumArray, which kicks off the computation, and I do it twice.
And it's the second time that we measure the cost.
And again, that allows us to warm up the JVM before we do any measurement.
So if I run this, again,
it takes a little while to run, as we have to warm up the processors.
But now notice this is printing out two values.
The number of cores available is 19. That comes from
ForkJoinPool.getCommonPoolParallelism. And then the time to execute this,
that's about 0.6 of a second, so less than half the time it
took us when running this over 1 core.
So there are a couple of things to note here.
First of all,
we're not getting a 20 times improvement in the runtime for this code.
Why is that? Well, there's overhead. Every time we
create, and use, and schedule a thread,
there's some overhead involved in that, so we're never
going to see that full improvement.
Also, the tasks we're running run very, very quickly.
So the overhead sometimes overwhelms the actual speed of the task itself.
So it may not be necessary to use a ForkJoinPool if the
work you're doing is fairly simple.
The other thing to note, if I look at this code, is that the code is
more complex when using the ForkJoinPool than it was when I was just
doing a straightforward computation.
And they are often the two prices you have to pay, complexity goes up, and you don't always get the speed increase that you expect.