Stream.parallel()对流的并行处理
本文主要提炼自《OnJava 进阶卷》第五章以及第六章,同时参考了部分操作系统知识。
背景
我需要完成的功能如下:在管理员需要查询一篇文章的审核状态时,对于状态字段审核表中存储的是1/2/3/4,但是管理员需要看到的是:未审核/审核中/通过/驳回,所以需要完成一个转换。我这里使用了Mybatis-Plus框架,对单表查询极度友好,联表查询则无能为力,所以在这里偷了个懒,没有在数据库层面使用联表查询,而是在应用层面完成了这一需求。具体步骤如下,首先查询得到二十条文章数据,保存为集合articleList;然后查询得到其对应状态信息(对于此类读远大于写的数据可使用缓存提高速度);接下来使用StreamAPI对articleList进行遍历,完成映射。
代码V1.0
这里为了简化操作使用示意代码。
package advance.concurrent;
import com.google.common.collect.Streams;
import streams.Randoms;
import java.util.*;
import java.util.stream.Stream;
/**
* @author: Caldarius
* @date: 2023/9/17
* @description:
*/
//数据库审核表的直接映射
class Check{
private long id;
//审核状态
private byte checkState;
//内容的ID
private long contentID;
//get/set
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public byte getCheckState() {
return checkState;
}
public void setCheckState(byte checkState) {
this.checkState = checkState;
}
public long getContentID() {
return contentID;
}
public void setContentID(long contentID) {
this.contentID = contentID;
}
@Override
public String toString() {
return "Check{" +
"id=" + id +
", checkState=" + checkState +
", contentID=" + contentID +
'}';
}
}
//审核的VO对象,由审核的PO对象转化而来
class CheckVo{
private long id;
private byte contentType;
//注意这里的类型变味了String
private String checkState;
private long contentID;
//get/set
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public byte getContentType() {
return contentType;
}
public void setContentType(byte contentType) {
this.contentType = contentType;
}
public String getCheckState() {
return checkState;
}
public void setCheckState(String checkState) {
this.checkState = checkState;
}
public long getContentID() {
return contentID;
}
public void setContentID(long contentID) {
this.contentID = contentID;
}
@Override
public String toString() {
return "CheckVo{" +
"id=" + id +
", contentType=" + contentType +
", checkState='" + checkState + '\'' +
", contentID=" + contentID +
'}';
}
}
//数据字典条目,存储数字到字符串的映射
class dictionaryItem{
private int id;
//数据字典类型,用于标记数据字典条目的类型。例如:性别、民族等等
private int typeID;
private String itemName;
//get/set
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getTypeID() {
return typeID;
}
public void setTypeID(int typeID) {
this.typeID = typeID;
}
public String getItemName() {
return itemName;
}
public void setItemName(String itemName) {
this.itemName = itemName;
}
}
public class StreamParallel {
public static void main(String[] args) {
//构建待审核的文章列表。实战中这部分来自于数据库。
List<Check> articleCheckList = new ArrayList<>();
Random random = new Random(34);
for (int i=0;i<150;i++){
Check c = new Check();
c.setCheckState((byte) random.nextInt(3));
c.setId(i);
c.setContentID(i+9);
System.out.println(c);
articleCheckList.add(c);
}
//构建数字到审核状态名称的映射
Map<Byte,String> stateMap = new HashMap<>();
stateMap.put((byte) 1,"待审核");
stateMap.put((byte) 2,"审核中");
stateMap.put((byte) 3,"通过");
stateMap.put((byte) 4,"驳回");
//记录初始时间
Long startTime = System.currentTimeMillis();
List<CheckVo> checkVoList = articleCheckList
.stream()
.map(item -> {
CheckVo checkVo = new CheckVo();
checkVo.setId(item.getId());
checkVo.setCheckState(stateMap.get(item.getCheckState()));
checkVo.setContentID(item.getContentID());
try {
//模拟复杂计算
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return checkVo;
}).peek(System.out::println).toList();
Long endTime = System.currentTimeMillis();
System.out.println(endTime-startTime+"ms");
}
}
//output2351ms:2351ms
不足
对于articlCheckList的遍历以及checkVo对象的创建之间是完全独立的,执行顺序不会影响执行的结果,可以轻易将其分解为互相独立的任务。这时候可以采用并行的方式提升速度。
代码V2.0
package advance.concurrent;
import com.google.common.collect.Streams;
import streams.Randoms;
import java.util.*;
import java.util.stream.Stream;
/**
* @author: Caldarius
* @date: 2023/9/17
* @description:
*/
//数据库审核表的直接映射
class Check{
private long id;
//审核状态
private byte checkState;
//内容的ID
private long contentID;
//get/set
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public byte getCheckState() {
return checkState;
}
public void setCheckState(byte checkState) {
this.checkState = checkState;
}
public long getContentID() {
return contentID;
}
public void setContentID(long contentID) {
this.contentID = contentID;
}
@Override
public String toString() {
return "Check{" +
"id=" + id +
", checkState=" + checkState +
", contentID=" + contentID +
'}';
}
}
//审核的VO对象,由审核的PO对象转化而来
class CheckVo{
private long id;
private byte contentType;
//注意这里的类型变味了String
private String checkState;
private long contentID;
//get/set
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public byte getContentType() {
return contentType;
}
public void setContentType(byte contentType) {
this.contentType = contentType;
}
public String getCheckState() {
return checkState;
}
public void setCheckState(String checkState) {
this.checkState = checkState;
}
public long getContentID() {
return contentID;
}
public void setContentID(long contentID) {
this.contentID = contentID;
}
@Override
public String toString() {
return "CheckVo{" +
"id=" + id +
", contentType=" + contentType +
", checkState='" + checkState + '\'' +
", contentID=" + contentID +
'}';
}
}
//数据字典条目,存储数字到字符串的映射
class dictionaryItem{
private int id;
//数据字典类型,用于标记数据字典条目的类型。例如:性别、民族等等
private int typeID;
private String itemName;
//get/set
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getTypeID() {
return typeID;
}
public void setTypeID(int typeID) {
this.typeID = typeID;
}
public String getItemName() {
return itemName;
}
public void setItemName(String itemName) {
this.itemName = itemName;
}
}
public class StreamParallel {
public static void main(String[] args) {
//构建待审核的文章列表。实战中这部分来自于数据库。
List<Check> articleCheckList = new ArrayList<>();
Random random = new Random(34);
for (int i=0;i<150;i++){
Check c = new Check();
c.setCheckState((byte) random.nextInt(3));
c.setId(i);
c.setContentID(i+9);
System.out.println(c);
articleCheckList.add(c);
}
//构建数字到审核状态名称的映射
Map<Byte,String> stateMap = new HashMap<>();
stateMap.put((byte) 1,"待审核");
stateMap.put((byte) 2,"审核中");
stateMap.put((byte) 3,"通过");
stateMap.put((byte) 4,"驳回");
//记录初始时间
Long startTime = System.currentTimeMillis();
List<CheckVo> checkVoList = articleCheckList
.stream()
//将流转化为并行流
.parallel()
.map(item -> {
CheckVo checkVo = new CheckVo();
checkVo.setId(item.getId());
checkVo.setCheckState(stateMap.get(item.getCheckState()));
checkVo.setContentID(item.getContentID());
try {
//模拟复杂计算
Thread.sleep(10);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return checkVo;
}).peek(System.out::println).toList();
Long endTime = System.currentTimeMillis();
System.out.println(endTime-startTime+"ms");
}
}
//output:231ms
这里可以看到再转化为并行流之后,性能取得了较大提升。
原理浅析
Stream在某些时候是很容易被并行化的,这主要得益于流所使用的内部迭代方式以及一种特殊的迭代器——分流器。这就呈现了一个非常令人振奋的现象,通过简单的调用parallel()方法,就可以轻易实现并发。但是注意,这种方法是建立在一个比较理想的情况的。在这个例子中,任务分解之后各部分是独立进行的,非常容易进行分解。其次,线程之间不存在协作,执行的顺序不会影响结果。最后也不存在多个线程同时修改同一个变量的情况。所以不能不分情况地使用parallel(),要从以上三个方面思考过后采取合适的方法。而且分流器默认情况下对数组的切分是较为均匀的,对链表的切分则相当糟糕。所以不建议对链表类型采用默认分流器。但是就目前应用场景来看这个默认方法是可以胜任的。
最后还有一个非常关键的问题,那就是这种方法的应用场景。读者朋友在读代码时可能注意到了这一行代码Thread.sleep(10);
这里我用让线程休眠的方式模拟程序进行复杂计算的场景,也就是常说的计算密集型场景。在这个场景下,并行体现了绝对优势。但是,对于普通程序来讲,性能则提升较少,甚至会比单线程串行的时候更低。这是因为JVM创建和管理线程以及操作系统进行线程切换都是需要时间的,如果一个任务执行的很快,那么可能在我们创建多线程的时候单线程程序可能就已经将其执行完毕了。此外,对于多核处理器来说,处理器缓存cache与内存的一致性问题也会在很大程度上影响此类程序并行的速度。而且就cache命中率来讲,并行程序是要低于并发程序的。在实际使用中,建议多做测试,以取得更有利的选择。
综上所述,基于parallel()
方法的简单并行流比较适合计算密集型程序使用。
至于更多并发编程相关的问题,作者会逐步整理的。本文主要分享下parallel()
方法在理想情况下的理想使用。