这两个月来因为工作和家庭的事情,导致一直都很忙,没有多少时间去汲取养分,也就没有什么产出,最近稍微轻松了一点,后续的【进阶之路】会慢慢回到正轨。

开门见山的说,第一次接触到多线程处理同一个任务,是使用IO多线程下载文件,之后也一直没有再处理这一块的任务,直到前几天有同事问我,为什么多线程处理一个list集合会出现各种bug,以及如何使用多线程的方式处理同一个list集合。

第一、为什么会出现类似于重复处理某一个模块的问题?

我们都知道,在Java中,每个线程都有自己独立的工作内存,线程对共享变量的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写。

如果线程1的修改内容想被线程2得到,那么线程1工作内存中修改后的共享变量需要先刷新到主内存中,再把主内存中更新过的共享变量更新到工作内存2中。

这个时候一般我们是考虑使用java中各种同步化的方法,首先,因为是需要高效处理list集合,所以可以排除synchronized方法,于是我想到了使用CompletionService操作异步任务。

大家可以在这篇文章看到具体的详解:

【进阶之路】线程池拓展与CompletionService操作异步任务

一、CompletionService

首先,按照之前文章的方法自定义一个WeedThreadPool

public class WeedThreadPool extends ThreadPoolExecutor {
private final ThreadLocal<Long> startTime =new ThreadLocal<>();
private final Logger log =Logger.getLogger("WeedThreadPool");
//统计执行次数
private final AtomicLong numTasks =new AtomicLong();
//统计总执行时间
private final AtomicLong totalTime =new AtomicLong();
/**
* 这里是实现线程池的构造方法,我随便选了一个,大家可以根据自己的需求找到合适的构造方法
*/
public WeedThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
}

然后就是实现线程池处理list集合的方法

public class WeedExecutorServiceDemo {
BlockingQueue<Runnable> taskQueue;
final static WeedThreadPool weedThreadPool = new WeedThreadPool(3, 10, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(100));
// 开始时间 public static void main(String[] args) throws InterruptedException, ExecutionException {
//记录任务开始时间
long start = System.currentTimeMillis();
CompletionService<List<Integer>> cs = new ExecutorCompletionService<>(weedThreadPool);
int tb=1;
//生成集合
List<List<Integer>> list1 =new ArrayList();
for (int i = 0; i < 10; i++) {
List<Integer> list =new ArrayList();
//随机生成任务处理
int hb=tb;
tb =tb*2;
int finalTb = tb;
cs.submit(new Callable<List<Integer>>(){ @Override
public List<Integer> call() throws Exception {
for (int j = hb; j< finalTb; j++){
list.add(j);
}
System.out.println(Thread.currentThread().getName()+"["+list+"]"); return list;
}
});
}
//注意在处理完毕后结束任务
weedThreadPool.shutdown();
for (int i = 0; i < 10; i++) {
Future<List<Integer>> future = cs.take();
if (future != null) {
list1.add(future.get());
System.out.println(future.get());
}
}
System.err.println("执行任务消耗了 :" + (System.currentTimeMillis() - start) + "毫秒");
System.out.println("結果["+list1.size()+"]==="+list1);
}
}

处理结果:

从结果上来看,还是比较美好的,通过CompletionService能够比较快速地分段处理任务,我之前也有提过,合理的线程池大小设计有助于提高任务的处理效率,网上通用的设置方法一般是这样的:

最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 )* CPU数目

进而得出

最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1)* CPU数目

二、ForkJoinPool

当然,除了使用CompletionService之外,也可以使用ForkJoinPool来设计一个处理方法。

ForkJoinPool和ThreadPoolExecutor都是继承自AbstractExecutorService抽象类,所以它和ThreadPoolExecutor的使用几乎没有多少区别。其核心思想是将大的任务拆分成多个小任务,然后在将多个小任务处理汇总到一个结果上。

ForkJoinPool框架通过初始化ForkJoinTask来执行任务,并提供了以下两个子类:

  • RecursiveAction:用于没有返回结果的任务。
  • RecursiveTask :用于有返回结果的任务。

我们实现的过程中可以使用RecursiveTask方法来分段处理list集合。

public class RecursiveTaskDemo {

    private static final ExecutorService executor = new ThreadPoolExecutor(2, 3, 10, TimeUnit.SECONDS, new LinkedBlockingQueue(10));
private static final int totalRow = 53000;
private static final int splitRow = 10000; public static void main(String[] args) throws InterruptedException, ExecutionException {
long start = System.currentTimeMillis();
//先循环生成待待处理集合
List<Integer> list = new ArrayList<>(totalRow);
for (int i = 0; i < totalRow; i++) {
list.add(i);
}
//计算出需要创建的任务数
int loopNum = (int)Math.ceil((double)totalRow/splitRow);
ForkJoinPool pool = new ForkJoinPool(loopNum);
ForkJoinTask<List> submit = pool.submit(new MyTask(list, 0, list.size())); List<List<Integer>>list1=new ArrayList<>();
list1.add(submit.get());
System.err.println("执行任务消耗了 :" + (System.currentTimeMillis() - start) + "毫秒");
System.out.println("結果["+list1.size()+"]==="+list1);
}
//继承RecursiveTask
static class MyTask extends RecursiveTask<List> {
private List<Integer> list;
private int startRow;
private int endRow; public MyTask(List<Integer> list, int startRow, int endRow) {
this.list = list;
this.startRow = startRow;
this.endRow = endRow;
} /**
* 递归处理数据,计算
* @return
*/
@Override
protected List compute() {
if (endRow - startRow <= splitRow) {
List<Integer> ret = new ArrayList<>();
for (int i = startRow; i < endRow; i++) {
//递归处理数据
ret.add(list.get(i));
}
System.out.println(Thread.currentThread().getName()+"["+ret+"]");
return ret;
}
int loopNum = (int)Math.ceil((double)totalRow/splitRow);
int startRow = 0;
List<MyTask> myTaskList = new ArrayList<>();
for (int i = 0; i < loopNum; i++) {
if (startRow > totalRow) {
break;
}
int endRow = Math.min(startRow + splitRow, totalRow);
System.out.println(String.format("startRow:%s, endRow:%s", startRow, endRow));
myTaskList.add(new MyTask(list, startRow, endRow));
startRow += splitRow;
}
//调用不同线程上独立执行的任务
invokeAll(myTaskList);
List<Integer> ret = new ArrayList<>();
//归并
for (MyTask myTask : myTaskList) {
ret.addAll(myTask.join());
}
return ret;
}
}
}

处理结果:

通过上文展示的方法,大家可以在不加锁的方式来增加任务处理的效率,遇到类似于爬虫数据处理、数据迁移等场景都可以采用,实测效果还不错。当然,根据处理结果来分析,CompletionService的效率大概更高一些~。

大家好,我是练习java两年半时间的南橘,下面是我的微信,需要之前的导图或者想互相交流经验的小伙伴可以一起互相交流哦。

【进阶之路】多线程条件下分段处理List集合的几种方法的更多相关文章

  1. Linux下查看alert日志文件的两种方法

    --linux下查看alert日志文件的两种方法: --方法1: SQL> show parameter background_dump_dest; NAME TYPE VALUE ------ ...

  2. Linux下查看压缩文件内容的 10 种方法

    Linux下查看压缩文件内容的 10 种方法 通常来说,我们查看归档或压缩文件的内容,需要先进行解压缩,然后再查看,比较麻烦.今天给大家介绍 10 不同方法,能够让你轻松地在未解压缩的情况下查看归档或 ...

  3. 《Java多线程面试题》系列-创建线程的三种方法及其区别

    1. 创建线程的三种方法及其区别 1.1 继承Thread类 首先,定义Thread类的子类并重写run()方法: package com.zwwhnly.springbootaction.javab ...

  4. 学习之路十四:客户端调用WCF服务的几种方法小议

    最近项目中接触了一点WCF的知识,也就是怎么调用WCF服务,上网查了一些资料,很快就搞出来,可是不符合头的要求,主要有以下几个方面: ①WCF的地址会变动,地址虽变,但是里面的逻辑不变! ②不要引用W ...

  5. 在linux环境下编译运行OpenCV程序的两种方法

    原来以为在Ubuntu下安装好了OpenCV之后,自己写个简单的程序应该很容易吧,但是呢,就是为了编译一个简单的显示图片的程序我都快被弄崩溃了. 在谷歌和上StackOverFlow查看相关问题解答之 ...

  6. Windows下自由创建.htaccess文件的N种方法

    .htaccess是apache的访问控制文件,apache中httpd.conf的选项配合此文件,完美实现了目录.站点的访问控 制,当然最多的还是rewrite功能,即URL重写,PHP中实现伪静态 ...

  7. 详解linux下批量替换文件内容的三种方法(perl,sed,shell)

    在建设本网站的时候,发现新建了很多的网页,突然发现,每个文件都需要进行修改一样的内容,一个一个打开很是麻烦,所以,总结了一下如何快速修改一个目录下多个文件进行内容替换.第三种方法用的不多 方法一 使用 ...

  8. [ARM-Linux开发]Linux下加载.ko驱动模块的两种方法:insmod与modprobe

    假设要加载的驱动程序模块名为SHT21.ko 加载驱动模块 方法一:  进入SHT21.ko驱动模块文件所在的目录,然后直接  insmod SHT21.ko  即可 方法二:  将SHT21.ko文 ...

  9. 树莓派进阶之路 (025) - ubuntu下使用VNC连接树莓派raspberry(转)

    转载:http://www.embbnux.com/2014/03/24/on_ubuntu_use_vnc_connect_raspberry/ ‎ 参考:  http://elinux.org/R ...

随机推荐

  1. Day11_57_自定义泛型

    自定义泛型 package com.shige.Generic; //自定义泛型 public class CustomizeGeneric { public static void main(Str ...

  2. SpringCloud(三)Hystrix断路器

    Hystrix断路器 概述 分布式系统面临的问题 复杂分布式体系结构中的应用程序有数十个依赖关系,每个依赖关系在某些时候将不可避免地失败 服务雪崩 多个微服务之间调用的时候,假设微服务A调用微服务B和 ...

  3. Python学习笔记-PuLP库(3)线性规划实例

    本节以一个实际数学建模案例,讲解 PuLP 求解线性规划问题的建模与编程. 1.问题描述 某厂生产甲乙两种饮料,每百箱甲饮料需用原料6千克.工人10名,获利10万元:每百箱乙饮料需用原料5千克.工人2 ...

  4. Java IO流知识总结

    Java IO的原理 I/O是Input和Output的缩写,用来处理设备之间的数据传输.如读/写文件,网络通讯等. Java程序中,对于数据的输入/输出操作以"流(stream)" ...

  5. POJ3040给奶牛发工资

    题意:       有n种硬币,每种硬币有mi个,然后让你给奶牛发工资,每周发至少c元(就是不找零钱的意思)然后问你能发几周?(硬币之间都是倍数关系) 思路:       这个题目做了两天,丢脸,看完 ...

  6. Windowsw核心编程 第13章 Windows内存结构

    第1 3章 Wi n d o w s的内存结构 13.1 进程的虚拟地址空间 每个进程都被赋予它自己的虚拟地址空间.对于 3 2位进程来说,这个地址空间是4 G B,因为3 2位指针可以拥有从0 x  ...

  7. Python 图片转字符图

    pip install Image argparse pillow from PIL import Image import argparse #命令行输入参数处理 parser = argparse ...

  8. android 代码中使用textAppearance

    一开始在代码中我以为使用tvAge.setTextAppearance(context, resid);这样的的方式就能行, 运行之后发现这个设置并未生效,于是到处搜索在代码中设置系统样式的的解决方法 ...

  9. 【js】Leetcode每日一题-制作m束花所需的最少天数

    [js]Leetcode每日一题-制作m束花所需的最少天数 [题目描述] 给你一个整数数组 bloomDay,以及两个整数 m 和 k . 现需要制作 m 束花.制作花束时,需要使用花园中 相邻的 k ...

  10. [BUUCTF-Pwn]刷题记录1

    [BUUCTF-Pwn]刷题记录1 力争从今天(2021.3.23)开始每日至少一道吧--在这里记录一些栈相关的题目. 最近更新(2021.5.8) 如果我的解题步骤中有不正确的理解或不恰当的表述,希 ...