Java Reference
In-Depth Information
The timing given by our DemoRunner is not scientific, but it will do for basic comparisons. On my
computer, Listing1.printPrimes takes roughly 50 seconds to complete. (On your computer, it could take
much longer; if you are running the code at home and it takes too long, reduce MAX_BITLENGTH to get a
more reasonable result.) My computer, however, is barely breaking a sweat: only four of my eight cores are
busied at all, and they hang out at about 50% utilization each. Let's see if we can use concurrency to make
better use of our hardware.
We will start by using classic Java threads; those are what most developers are probably familiar
with. Lambdas help somewhat here. However, thread management was tricky, and people wanted to use
thread pools, so Java introduced the concept of Executors to simplify things. Lambdas help even more
with executors. Most important, however, we get the concept of streams of execution in Java 8, which truly
leverage the power of lambdas to make concurrency easy. We will see precisely how by the end of the
chapter.
Lambdas and Classic Java Threading
The first Java concurrency structure was the Thread class, and it still underlies all the other concurrency
structures in the Java language. Lambdas work well with the Thread class, because the Thread class is built
around the Runnable interface. As we saw in chapter 2, a lambda is implicitly converted into an interface
implementation through type inference, so you can write this code:
Runnable r = () -> System.out.println("Hello, World!");
The Thread class has a constructor that takes a Runnable . In the world with Java 8 lambdas, this means
that the Thread class has a constructor that takes a lambda: that lambda is converted into a Runnable by
the compiler. This means that lambdas make writing Thread code much simpler: instead of having an
anonymous inner class or some distant interface implementation, you can simply pass your lambda right in.
A naïve approach to using the Thread class to solve our kata is to spawn a thread for each value to be
calculated. Unfortunately, threads are not cheap, and you will quickly exhaust the resources of your virtual
machine that way. Instead, we will spawn a certain number of threads, and split the work to be done evenly
among them. Those threads will do the CPU-intensive work, so we will have one of those per processor core.
We will have another thread that will be responsible for printing the values to standard out, since that is the
I/O intensive work, and having multiple threads attempting to write to standard out is a bottleneck. Both
these threads will be defined by passing the lambda right in.
Now, one catch with threads is that they swallow errors by default. If you provide some work for a thread
to do, and that work explodes with a RuntimeException , then the thread just silently dies. In order to get
some kind of error messaging, you have to set a Thread.UncaughtExceptionHandler instance. This can be
set either on the thread or globally, but the same type inference stunt will help make this code readable:
Thread.UncaughtExceptionHandler is an interface, so we can implement it inline. This code:
Thread.setDefaultUncaughtExceptionHandler(
new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(final Thread t, final Throwable e) {
System.err.println("Exception in " + t + ": " + e);
}
}
);
 
Search WWH ::




Custom Search