Java 8 Stream.collect方法中是否可使用线程不安全的合并器?
这个问题其实戳中了Java并行Stream里一个很容易被误解的点,咱们结合例子和官方定义来理清楚。
首先先回顾一下并行Stream里collect三参数方法的官方说明:
/**
Parameters:
supplier - a function that creates a new result container. For a parallel execution, this function may be called multiple times and must return a fresh value each time.
accumulator - an associative, non-interfering, stateless function for incorporating an additional element into a result
combiner - an associative, non-interfering, stateless function for combining two values, which must be compatible with the accumulator function
Returns: the result of the reduction
*/<R> R collect(Supplier<R> supplier, BiConsumer<R,? super T> accumulator, BiConsumer<R,R> combiner)
你提到的用StringBuilder作为容器的例子很典型,咱们先把代码贴出来:
List<String> vowels = List.of("a", "e", "i", "o", "u"); // 串行流 - 不需要合并操作 StringBuilder result = vowels.stream().collect( StringBuilder::new, (x, y) -> x.append(y), (a, b) -> a.append(",").append(b) ); System.out.println(result.toString()); // 并行流 - 需要合并各线程的部分结果 StringBuilder result1 = vowels.parallelStream().collect( StringBuilder::new, (x, y) -> x.append(y), (a, b) -> a.append(",").append(b) ); System.out.println(result1.toString());
你疑惑的点在于:既然合并器(combiner)是为了并行处理结果合并,为什么能用线程不安全的StringBuilder?其实这里的核心是并行流的执行机制从根源上避免了并发修改容器的场景:
- 首先,并行流会通过
supplier为每个工作线程生成独立的容器实例(也就是每个线程都有专属的StringBuilder),accumulator只负责把当前线程处理的元素追加到自己的容器里,完全不会和其他线程的容器交互——哪怕容器是线程不安全的也没关系,因为根本没有多线程同时访问同一个容器的情况。 - 到了合并阶段,combiner的作用是把多个线程生成的独立容器合并成最终结果。这里的关键规则是:每个容器实例在合并过程中,同一时间只会被一个combiner操作处理。也就是说,不会出现两个线程同时修改同一个
StringBuilder的情况,所以哪怕StringBuilder本身不是线程安全的,在这个场景下也不会有并发问题。
你提到的求和例子也能验证这个逻辑:并行流会先让每个线程分别累加一部分数值,得到多个独立的中间结果(比如0+1、0+2、0+3...),然后combiner再把这些中间结果逐个合并。合并的时候,每个中间结果只会被一个combiner操作处理,不需要同步,也不会有并发修改的问题,所以整个合并过程依然能利用并发优势,不会退化成串行。
总结一下:只要你遵循collect方法的要求——supplier每次都返回全新的容器,accumulator和combiner都是无状态、不干扰的函数,那么即使使用线程不安全的容器(比如StringBuilder)作为合并载体,也是安全的。并行流的执行机制保证了每个容器实例不会被多个线程同时访问修改。
内容来源于stack exchange

