According to the OCP book one must avoid stateful operations otherwise known as stateful lambda expression. The definition provided in the book is 'a stateful lambda expression is one whose result depends on any state that might change during the execution of a pipeline.'
They provide an example where a parallel stream is used to add a fixed collection of numbers to a synchronized ArrayList using the .map()
function.
The order in the arraylist is completely random and this should make one see that a stateful lambda expression produces unpredictable results in runtime. That's why its strongly recommended to avoid stateful operations when using parallel streams so as to remove any potential data side effects.
They don't show a stateless lambda expression that provides a solution to the same problem (adding numbers to a synchronized arraylist) and I still don't get what the problem is with using a map function to populate an empty synchronized arraylist with data... What is exactly the state that might change during the execution of a pipeline? Are they referring to the Arraylist itself? Like when another thread decides to add other data to the ArrayList when the parallelstream is still in the process adding the numbers and thus altering the eventual result?
Maybe someone can provide me with a better example that shows what a stateful lambda expression is and why it should be avoided. That would be very much appreciated.
Thank you
答案 0 :(得分:2)
为了试图给出一个例子,让我们考虑以下Consumer
(注意:这样一个函数的用处不在此处):
public static class StatefulConsumer implements IntConsumer {
private static final Integer ARBITRARY_THRESHOLD = 10;
private boolean flag = false;
private final List<Integer> list = new ArrayList<>();
@Override
public void accept(int value) {
if(flag){ // exit condition
return;
}
if(value >= ARBITRARY_THRESHOLD){
flag = true;
}
list.add(value);
}
}
这是一个消费者,它会将项目添加到List
(我们不考虑如何取回列表,也不考虑线程的安全性)并且有一个标志(代表有状态)。
这背后的逻辑是,一旦达到阈值,消费者应该停止添加项目。
你的书试图说的是,因为没有保证顺序,函数必须使用Stream
的元素,所以输出是非确定性的。
因此,他们建议您仅使用无状态函数,这意味着它们将始终使用相同的输入生成相同的结果。
答案 1 :(得分:2)
第一个问题是:
List<Integer> list = new ArrayList<>();
List<Integer> result = Stream.of(1, 2, 3, 4, 5, 6)
.parallel()
.map(x -> {
list.add(x);
return x;
})
.collect(Collectors.toList());
System.out.println(list);
您不知道结果会是什么,因为您要将元素添加到非线程安全的集合ArrayList
。
但即使你这样做:
List<Integer> list = Collections.synchronizedList(new ArrayList<>());
执行相同的操作list
没有可预测的顺序。多个线程添加到此同步集合。通过添加同步集合,您可以保证添加所有元素(而不是普通ArrayList
),但在中顺序它们将出现在未知中。
请注意,list
没有订单保证,这称为处理订单。对于此特定示例,result
保证为:[1, 2, 3, 4, 5, 6]
。
根据问题,您通常可以摆脱stateful
操作;对于您的示例,返回synchronized List
将是:
Stream.of(1, 2, 3, 4, 5, 6)
.filter(x -> x > 2) // for example a filter is present
.collect(Collectors.collectingAndThen(Collectors.toList(),
Collections::synchronizedList));
答案 2 :(得分:2)
有状态lambda表达式是一个结果取决于在执行管道期间可能更改的任何状态的表达式。在 另一方面,无状态lambda表达式是其结果的表达式 不依赖于在执行期间可能发生变化的任何状态 管道
File Search...
可能的输出:
List < Integer > data = Collections.synchronizedList(new ArrayList < > ());
Arrays.asList(1, 2, 3, 4, 5, 6, 7).parallelStream()
.map(i -> {
data.add(i);
return i;
}) // AVOID STATEFUL LAMBDA EXPRESSIONS!
.forEachOrdered(i -> System.out.print(i+" "));
System.out.println();
for (int e: data) {
System.out.print(e + " ");
强烈建议您在使用时避免有状态操作 并行流,以消除任何潜在的数据副作用。在 事实上,它们通常应该在任何地方的串行流中避免 可能,因为他们阻止你的流利用 并行化。
答案 3 :(得分:1)
以下是有状态操作每次返回不同结果的示例:
public static void main(String[] args) {
Set<Integer> seen = new HashSet<>();
IntStream stream = IntStream.of(1, 2, 3, 1, 2, 3);
// Stateful lambda expression
IntUnaryOperator mapUniqueLambda = (int i) -> {
if (!seen.contains(i)) {
seen.add(i);
return i;
}
else {
return 0;
}
};
int sum = stream.parallel().map(mapUniqueLambda).peek(i -> System.out.println("Stream member: " + i)).sum();
System.out.println("Sum: " + sum);
}
在我运行代码的情况下,我得到了以下输出:
Stream member: 1
Stream member: 0
Stream member: 2
Stream member: 3
Stream member: 1
Stream member: 2
Sum: 9
如果我插入一个hashset,为什么我得到9作为总和?
答案:不同的主题占据IntStream
的不同部分
例如,值1和1。 2设法最终在不同的线程上。
答案 4 :(得分:1)
有状态lambda表达式是其结果取决于在执行流水线期间可能改变的任何状态的表达式。
让我们通过以下示例了解这一点:
List<Integer> list = Arrays.asList(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15);
List<Integer> result = new ArrayList<Integer>();
list.parallelStream().map(s -> {
synchronized (result) {
if (result.size() < 10) {
result.add(s);
}
}
return s;
}).forEach( e -> {});
System.out.println(result);
当您运行此代码5次时,输出将始终保持不变。背后的原因是这里在地图更新结果数组中处理Lambda表达式。由于这里的结果数组取决于特定子流的数组大小,因此每次调用此并行流时,结果都会改变。
为了更好地理解并行流 : 并行计算包括将一个问题分为多个子问题,同时解决这些问题(并行处理,每个子问题都在单独的线程中运行),然后将解决方案的结果组合到子问题中。当流并行执行时,Java运行时将流划分为多个子流。聚合操作迭代并并行处理这些子流,然后合并结果。
希望这会有所帮助!