Parallel and combined Lambda expression behavior in Java

  • 2020-06-15 08:15:04
  • OfStack

From serial to parallel

Serial refers to step by step processing, that is, normally, code line by line.

If we expand our usual iterator-style loop, we serialize the operation defined in the loop body:


sum += arr.get(0);
sum += arr.get(1);
sum += arr.get(2);
//...

At the beginning of the book, it is mentioned that Java needs to support parallel computing for collections (and Lambda makes this possible).

These functions will all be implemented in the library code, and for us consumers, the complexity of implementing parallelism is greatly reduced (at the very least by calling the relevant methods).

In addition, the two concepts of concurrency and parallelism are actually different. If you don't understand them, please help yourself. Here is just a popular quote:

One is about code structure and one is about code execution.

If we wanted to distribute a computing task evenly among the four cores of CPU, we would assign one computing thread to each core, with each thread performing subtasks of the entire task.

There is a very graphic pseudocode in the book:


if the task list contains more than N/4 elements {
 leftTask = task.getLeftHalf()
 rightTask = task.getRightHalf()
 doInparallel {
 leftResult = leftTask.solve()
 rightResult = rightTask.solve()
 }
 result = combine(leftResult, rightResult)
} else {
 result = task.solveSequentially()
}

In the code, every 4 task elements are divided into 1 group, which is processed in parallel by 4 kernels, and then the result is merged once in every two groups, and finally the final result of the whole task queue is obtained.

From the overall processing flow, the task queues are recursively grouped, each group is processed in parallel, and the results are recursively merged (the merge is achieved through a pipeline termination operation).

Prior to Java8, developers implemented the pattern using an fork/join framework for collections.

Now, however, it is very easy to optimize your code for performance.

Remember the final code from the previous section:


long validContactCounter = contactList.stream()
 .map(s -> new Contact().setName(s))
 .filter(Contact::call)
 .count();

Minor changes:


long validContactCounter = contactList.parallelStream()
 .map(s -> new Contact().setName(s))
 .filter(Contact::call)
 .count();

Note that stream() becomes parallelStream()

Meanwhile, the following figure shows how to decompose the above tasks according to the four checks, and finally merge the results and terminate the pipeline.

Note that the purpose of recursive decomposition is to make the subtasks small enough to execute serially.

Portfolio behavior

Java writers should know that there are no pure "functions" in Java, only "methods." That is, a function in Java must depend on a class or exist as a class.

In other languages, there are pure functions that declare 1 function in the syntax of CoffeeScript:


eat = (x) -> 
 alert("#{x} has been eatten!")

This approach is very similar to the syntax of the Lambda expression, meaning that the Lambda expression looks more like a functional expression than an anonymous inner class.

For functions, one core operation is composition. If you want one of the solutions sqrt(sqr(b) - 4 * a * c), you are combining multiple subfunctions.

For object orientation, we decompose it by decoupling, and we want to decompose the behavior of a function in the same way.

First, following the example used in the last two sections, make a slight change to the Contact class, splitting the name attribute into first and last names:


private String firstName;
private String lastName;

Suppose we now want to sort contacts, the standard way to create a custom sort is to create an Comparator:


public interface Comparator<T> {
 int compare(T o1, T o2);
 //...
}

We wanted to sort the contacts by comparing the first letter of the first name:


Comparator<Contact> byFirstName = new Comparator<Contact>() {
 @Override
 public int compare(Contact o1, Contact o2) {
 return Character.compare(o1.getFirstName().charAt(0), o2.getFirstName().charAt(0));
 }
};

Lambda writing:


Comparator<Contact> byFirstNameLambdaForm = (o1, o2) ->
 Character.compare(o1.getFirstName().charAt(0), o2.getFirstName().charAt(0));

After writing this code, IDEA immediately reminded me that the code could be replaced with ES91en. comparingInt(...) ", but this is later, do not list for the moment.

In the code above, we find the composite behavior, i.e Comparator<Contact> compare (...). The method also USES o. getFirstName() and Character. compare(...). These two methods (charAt(...) will not be considered here for the sake of brevity. ), in java. util. function, we found the prototype of this function:


public interface Function<T, R> {
 R apply(T t);
 //...
}

Receives 1 argument of type T and returns 1 result of type R.

Now let's extract the comparison key "first letter of comparison name" into an instance of a function object:


if the task list contains more than N/4 elements {
 leftTask = task.getLeftHalf()
 rightTask = task.getRightHalf()
 doInparallel {
 leftResult = leftTask.solve()
 rightResult = rightTask.solve()
 }
 result = combine(leftResult, rightResult)
} else {
 result = task.solveSequentially()
}
0

Then extract the specific comparison behavior of "compare first letters" :


if the task list contains more than N/4 elements {
 leftTask = task.getLeftHalf()
 rightTask = task.getRightHalf()
 doInparallel {
 leftResult = leftTask.solve()
 rightResult = rightTask.solve()
 }
 result = combine(leftResult, rightResult)
} else {
 result = task.solveSequentially()
}
1

With keyExtractor and keyComparator, let's reassemble Comparator 1:


if the task list contains more than N/4 elements {
 leftTask = task.getLeftHalf()
 rightTask = task.getRightHalf()
 doInparallel {
 leftResult = leftTask.solve()
 rightResult = rightTask.solve()
 }
 result = combine(leftResult, rightResult)
} else {
 result = task.solveSequentially()
}
2

At this point, we sacrifice simplicity but gain flexibility, which means that if we change the comparison key to last name instead of first name, we simply change keyExtractor to:


Function<Contact, Character> keyExtractor = o -> o.getLastName().charAt(0);

Thankfully, the library designers took into account the universality of this 1 natural comparison requirement and thus provided static methods for the Comparator interface. , the corresponding Comparator can be generated for the key by passing in the extraction rule of the comparison key:


if the task list contains more than N/4 elements {
 leftTask = task.getLeftHalf()
 rightTask = task.getRightHalf()
 doInparallel {
 leftResult = leftTask.solve()
 rightResult = rightTask.solve()
 }
 result = combine(leftResult, rightResult)
} else {
 result = task.solveSequentially()
}
4

Even if we wanted to change the rules of comparison, such as the length of a contact's first and last names, we would only need to make a few changes:


if the task list contains more than N/4 elements {
 leftTask = task.getLeftHalf()
 rightTask = task.getRightHalf()
 doInparallel {
 leftResult = leftTask.solve()
 rightResult = rightTask.solve()
 }
 result = combine(leftResult, rightResult)
} else {
 result = task.solveSequentially()
}
5

This was a major improvement, and it really focused our attention on the rules of comparison, rather than building the necessary glue code in bulk.

comparing (...). By accepting a simple behavior, you can construct a more complex behavior based on that behavior.

Wow!!!!

Even better, for streams and pipes, we need even fewer changes:


if the task list contains more than N/4 elements {
 leftTask = task.getLeftHalf()
 rightTask = task.getRightHalf()
 doInparallel {
 leftResult = leftTask.solve()
 rightResult = rightTask.solve()
 }
 result = combine(leftResult, rightResult)
} else {
 result = task.solveSequentially()
}
6

summary

Code of this chapter:


if the task list contains more than N/4 elements {
 leftTask = task.getLeftHalf()
 rightTask = task.getRightHalf()
 doInparallel {
 leftResult = leftTask.solve()
 rightResult = rightTask.solve()
 }
 result = combine(leftResult, rightResult)
} else {
 result = task.solveSequentially()
}
7

And operation results:


Bar Ma
Foo Jack
Olala Awesome

Related articles: