JDK8 Stream用法分析-双端队列和工作密取

       最近在做项目的时候使用到了jdk8的parallelStream想来加快程序的执行效率,没有想到得到的集合里面有很多null,同时偶尔也会出现ArrayIndexOutOfBoundsException错误,下面就逐步进行解析。

 

Jdk8有如下几个新特性:

>Lambda表达式:允许把函数作为一个方法的参数传递进去。

>方法引用:直接引用已有Java类或对象(示例)的方法或构造器。

>Date Time API:加强对日期和时间的处理。

>Stream API:Stream是对集合对象功能的增强,它专注于对集合对象进行各种非常便利、高效的聚合操作,它提供串行(stream)和并行(parallelStream)两种模式进行汇聚操作,并发模式能够充分利用多核处理器的优势,使用fork/join方式来拆分任务和加速处理过程。

stream():为集合创建串行流

parallelStream():为集合创建并行流

forEach:用来迭代流中的每个数据

举个例子,把一个list中的值赋值到另外一个list中:

@Test
public void streamTest1() {

    int LOOPS = 30;
    List<Integer> sourceList = new ArrayList<>();
    List<Integer> streamResList = new ArrayList<>();
    List<Integer> parallelStreamResList = new ArrayList<>();
    for(int i = 0 ; i < LOOPS; i++) {
        sourceList.add(i);
    }

    System.out.println("sourceList.size = " + sourceList.size());
    System.out.println("sourceList = " + sourceList);

    sourceList.stream().forEach(item -> {
        streamResList.add(item);
    });

    System.out.println("streamResList.size = " + streamResList.size());
    System.out.println("streamResList = " + streamResList);

    sourceList.parallelStream().forEach(item -> {
        parallelStreamResList.add(item);
    });

    System.out.println("parallelStreamResList.size = " + parallelStreamResList.size());
    System.out.println("parallelStreamResList = " + parallelStreamResList);

}

输出结果:

结果解析:

由于stream是串行执行的,最终的大小和内容都符合预期,但是parallelStream得到的结果却是不符合预期,数量少了两个(size=28,实际上少了5,9,24三个数),并且数组中还要null,先不考虑parallelStream的效率,结果不对是为何?
先说结论:parallelStreamResList用的是ArrayList,非线程安全导致结果不符合预期。如果把ArrayList改成Vector或者Collections.synchronizedList(new ArrayList())或者CopyOnWriteArrayList等线程安全即可。

 

ArrayList是线程不安全的?

一言以蔽之,ArrayList中没有加锁的动作(Synchronized或Lock),会造成数据不一致。

List<Integer> streamResList = new ArrayList<>(); 

/**
 * Constructs an empty list with an initial capacity of ten.
 */
public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}

构建一个初始容量大小为10的空列表。再看一下add动作:

public boolean add(E e) {
    ensureCapacityInternal(size + 1);  // Increments modCount!!
    elementData[size++] = e;
    return true;
}

确认容量的大小;然后将元素写入到数组末端;看一下ensureCapacityInternal方法:

private void ensureCapacityInternal(int minCapacity) {
    if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
        minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
    }

    ensureExplicitCapacity(minCapacity);
}

如果尚未扩容,比较默认容量DEFAULT_CAPACITY和minCapacity并取最大值,然后调用ensureExplicitCapacity确认精确容量:

private void ensureExplicitCapacity(int minCapacity) {
    modCount++;

    // overflow-conscious code
    if (minCapacity - elementData.length > 0)
        grow(minCapacity);
}

通过grow扩容:

private void grow(int minCapacity) {
    // overflow-conscious code
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);
    if (newCapacity - minCapacity < 0)
        newCapacity = minCapacity;
    if (newCapacity - MAX_ARRAY_SIZE > 0)
        newCapacity = hugeCapacity(minCapacity);
    // minCapacity is usually close to size, so this is a win:
    elementData = Arrays.copyOf(elementData, newCapacity);
}

新大小newCapacity扩容到oldCapacity的1.5倍,并通过Arrays.copyOf得到新的扩容后的数据。当然问题还是出现在add动作的时候的size的读取和赋值过程中,在多个线程读写size的时候(执行size++动作),造成数据不一致,size偏小。

vector的add方法,通过synchronized关键字加锁:

public synchronized boolean add(E e) {
    modCount++;
    ensureCapacityHelper(elementCount + 1);
    elementData[elementCount++] = e;
    return true;
}

CopyOnWriteArrayList的add方法,通过Lock进行加锁:

public boolean add(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        Object[] elements = getArray();
        int len = elements.length;
        Object[] newElements = Arrays.copyOf(elements, len + 1);
        newElements[len] = e;
        setArray(newElements);
        return true;
    } finally {
        lock.unlock();
    }
}

 

parallelStream的并发原理:

parallelStream是一个并行执行的流,使用ForkJoinPool并行方式来拆分任务和加速处理过程。ForkJoinPool的使用分治法将大人物拆分成子任务,分别放到不同的双端队列总,并未每个队列创建单独的线程来执行队列里的任务。同时采用了工作窃取算法来最大限度地提高并行处理能力。

双端队列和工作密取

       Java6增加了两种容器类型,Deque(发音为"Deck")和BlockingQueue,它们分别对Queue和BlockingQueue进行了扩展。Deque是一个双端队列,实现了队列头和队列尾的高效插入和移除。具体实现包括ArrayDeque和LinkedBlockingDeque。

       正如阻塞队列适用于生产者-消费者模式,双端队列同样适用于另一种相关模式,即工作密取(Working Stealing)。在生成者-消费者设计中,所有消费者有一个共享的工作队列,而在工作密取设计中,每个消费者都有各自的双端队列。如果一个消费者完成了自己双端队列中全部工作,那么它可以从其他消费者双端队列末尾秘密地获取工作。密取工作模式比传统的生产者-消费者模式具有更高的可伸缩性,这是因为工作者线程不会在单个共享的任务队列上发生竞争。在大多数时候,它们都只是访问自己的双端队列,从而极大地减小了竞争。当工作者线程需要访问另一个队列时,它会从队列的尾部而不是从头部获取工作,因此进一步降低了队列上的竞争程度。

       使用场景:工作密取非常适合于既是消费者也是生产者问题--当执行某个工作时可能导致出现更多的工作。例如,在网页爬虫程序中在处理一个页面时,通常会发现有更多的页面需要处理。类似的还有许多搜索图的算法,例如在垃圾回收阶段对堆进行标记,都可以通过工作密取机制来实现高效并行。当一个工作线程找到新的任务单元时,它会将其放到自己队列的末尾(或者在工作共享设计模式中,放入其他工作者线程的队列中)。当双端队列为空是,它会在另一个线程的队列队尾查找新的任务,从而确保每个线程都保持在忙碌状态。

 

最后看一下执行效率:

      本来以为parallelStream的并行处理方式一定会比stream高效,但是在普通的赋值场景下,线程安全的容器在并发的情况下需要加锁,效率上反而没有串行的效率高,但是在CPU密集和IO密集型的计算方式下,并行的确实会高,下面看一下测试代码:

@Test
public void streamTestN() {
    int NUM = 500000;
    int LOOPS = 10;
    long sumStream = 0l;
    long sumParallelStream = 0l;
    Random random = new Random();

    for (int j = 0; j < LOOPS; j++) {

        //List<DriverDto> resList = Lists.newArrayList();
        //List<DriverDto> resList1 = Lists.newArrayList();
        List<DriverDto> resList = new Vector<>();
        List<DriverDto> resList1 = new Vector<>();
        //List<DriverDto> resList = Collections.synchronizedList(new ArrayList<>());
        //List<DriverDto> resList1 = Collections.synchronizedList(new ArrayList<>());
        //List<DriverDto> resList = new CopyOnWriteArrayList<>();
        //List<DriverDto> resList1 = new CopyOnWriteArrayList<>();
        List<DriverDto> sourceList = Lists.newArrayList();
        for (int i = 0; i < NUM; i++) {
            DriverDto dto = new DriverDto();
            dto.setDriverId(random.nextInt(2 ^ 20));
            dto.setAge(random.nextInt(100));
            sourceList.add(dto);
        }

        System.out.println("parallelStream begin.");
        long start = System.currentTimeMillis();
        sourceList.parallelStream().forEach(model -> {
            DriverDto dto = new DriverDto();
            dto.setDriverId(model.getDriverId());
            dto.setAge(model.getAge());
            resList.add(model);
            try {
                Thread.sleep(1);//IO密集型的任务
                compute();//计算密集型的任务
            } catch (Exception e) {

            }
        });
        long end = System.currentTimeMillis();
        System.out.println("parallelStream end. cost = " + (end - start) + " ms");
        sumParallelStream += end - start;

        System.out.println("stream begin.");
        long start1 = System.currentTimeMillis();
        sourceList.stream().forEach(model -> {
            DriverDto dto = new DriverDto();
            dto.setDriverId(model.getDriverId());
            dto.setAge(model.getAge());
            resList1.add(model);
            try {
                Thread.sleep(1); //IO密集型的任务
                compute();//计算密集型的任务
            } catch (Exception e) {

            }
        });
        long end1 = System.currentTimeMillis();
        System.out.println("stream end. cost = " + (end1 - start1) + " ms");
        sumStream += end1 - start1;

        System.out.println("resList.size() = " + resList.size());
        System.out.println("resList1.size() = " + resList1.size());
    }

    System.out.println("sumStream = " + sumStream/LOOPS);
    System.out.println("sumParallelStream = " + sumParallelStream/LOOPS);

}

private static void compute() {
    for(long i = 0 ;i < 100000l; i++) {
        long x = i * i;
    }
}

class DriverDto implements Serializable {
    private static final long serialVersionUID = 1;
    //司机Id
    private long driverId;
    //age
    private int age;

    public long getDriverId() {
        return driverId;
    }

    public void setDriverId(long driverId) {
        this.driverId = driverId;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

仅赋值动作(串行平均耗时低),int NUM = 500000:

sumStream = 27
sumParallelStream = 57

加上IO和CPU计算(并行平均耗时低),int NUM = 5000:

sumStream = 6235
sumParallelStream = 788

所以,没有绝对的方式,看具体的应用场景。


Author:忆之独秀

Email:leaguenew@qq.com

注明出处:https://blog.csdn.net/lavorange/article/details/90737929

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值