For the love of Java, Executors, FunctionalInterface: Evolve or die!
I have decided to continue on the for the love of Java with one of my favorite topics: Threads and Executor Services! Lately, I have been doing some work in "other" languages. Now remember, I am taking the high road, so I wont name them. Remember, I am not there yet. So yes I will "Python". I am not going to take too many potshots at these lesser languages (Ow! I just did burn) outside of this one sentiment, "Evolve or die". I will show you how Java has evolved from Threads to Executor services, and get into the new Virtual Threads.
Like the last blog all the code I am going to show is in one super tight commit. However, you don't have to read the commit ahead of time I am going to walk through it. Without further ado lets go!
Threading brief
The old old way
public interface Runnable {
/**
* Runs this operation.
*/
void run();
}
@Test
void oldOldWay() throws InterruptedException {
abstract class MyFuture implements Runnable {
protected volatile Object result;
private volatile boolean done = false;
@Override
public void run() {
this.done= true;
}
public Object getResult(){
return this.result;
}
public boolean isDone(){
return done;
}
}
MyFuture meaningOfLife = new MyFuture() {
@Override
public void run() {
this.result = Long.valueOf(42);
super.run();
}
};
Thread t = new Thread(meaningOfLife);
t.start();
t.join();
// we have computed the meaning of life
assertTrue(meaningOfLife.isDone());
assertEquals(Long.valueOf(42), meaningOfLife.getResult());
}
Along comes Java 1.5
Please note, at this point in the blog we are still taking about Java from ~13 years ago. Of course having everyone make their own "future" class is a silly. It is "easy" to do as you can see above, but it leaves users lots of "rope to hang themselves" with. Along comes callable:
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
Well that makes sense. Many threads are going to return "something", at minimum the something is a true/false that it finished. Callable can (and is) used outside of threading. Maybe in some languages you might call it a block.
But what about managing pools of threads? Re-using the threads? For simple tasks "fork an instance each connection" it is simple, but for more advanced tasks like workers spawning other workers and figuring out that lifecycle it isn't that clear cut. There isn't a "once size fits all answer", but I would argue the Executor Service is brilliant:
ExecutorService executor = null;
try {
//normally you would create this once at application init time
executor = Executors.newCachedThreadPool();
Future<Integer> future = executor.submit(something);
The user defined MyFuture is no longer needed. We have a first class Future in the language, and there are multiple Executor implements available (SingleThreaded, ForkJoinPool) to fit common workload types. Let see it all in action!
@Test
void oldWay(){
Callable<Integer> something = new Callable<>() {
@Override
public Integer call() {
return 4;
}
};
ExecutorService executor = null;
try {
//normally you would create this once at application init time
executor = Executors.newCachedThreadPool();
Future<Integer> future = executor.submit(something);
assertEquals(4, future.get());
} catch (ExecutionException | InterruptedException e) {
throw new RuntimeException(e);
} finally {
if (executor != null){
executor.close();
}
}
}
Are we done yet? No this is Java, you gotta "gimme some more".
Enter Function Interface
Now, from my non Java friends I sense a 20 year old complaint coming. It is the "public static void main(string [] args)" complaint. Right here:
Callable<Integer> something = new Callable<>() {
@Override
public Integer call() {
return 4;
}
};
Well well well. This too is solved in many (but not all) cases. Enter @FunctionalInterface:
@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}
It stands to reason that if a class or an interface only has a single method, we can infer the "boilerplate". To understand why we only have to look at the code of Executor.submit(Callable<T> task). The submit method takes a callable as an argument.
<T> Future<T> submit(Callable<T> task);
When you combine "callable has only one method "call()" and submit takes a callable, well there is no longer a need to name the class, or the METHOD! Lets see what that looks like:
@Test
void newWay(){
Callable<Integer> something = () -> 4;
Why stop there? We do not even need a reference to the Callable!
@Test
void moreConciseWay(){
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
assertEquals(4, executor.submit(() -> 4).get());
} catch (ExecutionException | InterruptedException e) {
throw new RuntimeException(e);
}
}
The "java sux because public static void main" crew is is right now "running for the self hills". (languages that you have to type self all the time you know who your are)
Enter Virtual threads
OMG! with all the images and trolls we are running into a danger zone in this blog. Lets bring it home with something solid, that has some technical value. You may have noticed above:
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
Things have changed over the years. Years ago SEDA or Staged Event Driven Architecture or Disruptor was though of as a elegant and performant way to do multi-threaded applications. Hardware has changed and improved and research has moved forward. Many application need a hybrid model, not "one thread per connection" not "requests moving through pools of threads". Java's lightweight VirtualThreads are not handcuffed to system processes, and they are able to yield when they are blocked on IO (waiting for a socket, or reading from a file).
Notice in the example above we were able to move to virtual threads without switching to a new suite of libraries. The code is still using the sample Callable and ExecutorService interface, a new implementation of ExecutorService is reachable by a small change.
#fortheloveofjava

.gif)


Comments
Post a Comment