多线程实战

文章讲述了在处理不定量数据调用接口并保存至数据库的场景中,从单线程循环到多线程并发执行的演变过程。提出了通过线程池(如ThreadPoolExecutor)和并发控制(如synchronized)来优化性能和避免数据库问题。线程池的配置,如核心线程数、最大线程数、等待队列和拒绝策略的选择,以及submit和invoke方法的使用场景也进行了详细阐述。最后强调了使用线程池时的注意事项,包括命名、等待线程结束和避免数据丢失。
摘要由CSDN通过智能技术生成

多线程实战

1、业务场景

​ 不定量的数据去调用另外一个接口(接口一次只能承受20个数据,否则会造成超时),并将返回的数据保存到数据库

2、第一次方案

​ 通过for循环 缺点是耗时很大,一次调用5s的话 100个就是5次调用 25s,且循环插入 数据库,造成不必要的sql连接

3、第二次方案

​ 通过多线程,将任务保存到tasklist 并行执行 (一条线程的逻辑是 先调用接口得到20条数据后插入到数据库中)

​ 存在的问题:多个线程同时对一个表进行批量插入操作,导致数据库出现问题

4、第三次方案

​ 通过synchronized 锁住对象(最后保存的list),一开始是在20次循环中保存的对象 ,导致耗时较长,后来统一将数据存到list中

​ 解决方案:现将20条数据保存到一个list中,再将listadd 到 有锁的那个list中,这样的话不会增加耗时

为什么要用线程池

1、创建/销毁线程需要消耗系统资源,线程池可以复用已创建的线程。
2、控制并发的数量。并发数量过多,可能会导致资源消耗过多,从而造成服务器崩溃。(主要原因)可以对线程做统一管理。

实现ExecutorService:
  1. Executors: 它是一个工具类,提供了一些静态方法用于创建不同类型的线程池,比如newFixedThreadPoolnewCachedThreadPoolnewScheduledThreadPool等。

  2. ThreadPoolExecutor: 它是一个可扩展的线程池,可以根据需要自定义一些参数,例如线程池大小,任务队列大小等等。

  3. ForkJoinPool: 它是Java 7中新增的一种线程池,提供了更高效的并行处理能力,适用于处理大量的小任务。

  4. ScheduledThreadPoolExecutor: 它是一个定时任务线程池,可以在指定的时间执行任务,适用于需要周期性运行的任务。

代码:
参数:
public ThreadPoolExecutor(
  int corePoolSize, //核心线程数量 
  int maximumPoolSize, //最大线程数量
  long keepAliveTime, //等待时间
  TimeUnit unit, //时间单位
  BlockingQueue<Runnable> workQueue,
  ThreadFactory threadFactory) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, defaultHandler);}
具体参数解释:
corePoolSize:
核心线程数量,当有新任务在execute()方法提交时,会执行以下判断:
1、如果运行的线程少于 corePoolSize,则创建新线程来处理任务,即使线程池中的其他线程是空闲的;
2、如果线程池中的线程数量大于等于 corePoolSize 且小于 maximumPoolSize,则只有当workQueue满时才创建新的线程去处理任务;
3、如果设置的corePoolSize 和 maximumPoolSize相同,则创建的线程池的大小是固定的,这时如果有新任务提交,
		若workQueue未满,则将请求放入workQueue中,等待有空闲的线程去从workQueue中取任务并处理;
如果运行的线程数量大于等于maximumPoolSize,这时如果workQueue已经满了,则通过handler所指定的策略来处理任务;
所以,任务提交时,判断的顺序为 corePoolSize –> workQueue –> maximumPoolSize。

keepAliveTime:
线程池维护线程所允许的空闲时间。当线程池中的线程数量大于corePoolSize的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了keepAliveTime。

workQueue:
等待队列,当任务提交时,如果线程池中的线程数量大于等于corePoolSize的时候,把该任务封装成一个Worker对象放入等待队列。

threadFactory:
它是ThreadFactory类型的变量,用来创建新线程。默认使用Executors.defaultThreadFactory() 来创建线程。使用默认的ThreadFactory来创建线程时,会使新创建的线程具有相同的NORM_PRIORITY优先级并且是非守护线程,同时也设置了线程的名称。

handler:
它是RejectedExecutionHandler类型的变量,表示线程池的饱和策略。如果阻塞队列满了并且没有空闲的线程,这时如果继续提交任务,就需要采取一种策略处理该任务。线程池提供了4种策略:

AbortPolicy:直接抛出异常,这是默认策略;
CallerRunsPolicy:用调用者所在的线程来执行任务;
DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务;
DiscardPolicy:直接丢弃任务;
队列类型
1. `SynchronousQueue`:这是一种无缓冲的队列。生产者线程将元素插入队列时,必须等待消费者线程从队列中获取元素。如果当前没有消费者线程在等待获取元素,则生产者线程会被阻塞,等待消费者线程到来。这种队列适用于在线程池中使用较少线程的情况下,可以保证任务以FIFO顺序执行。

2. `LinkedBlockingQueue`:这是一种无限缓冲的队列。它可以保存任意数量的元素,但在生产者线程试图向队列中插入元素时,如果队列已满,则生产者线程会被阻塞。这种队列适用于在生产者和消费者之间解耦的情况下,可以较好地控制任务的数量。

3. `ArrayBlockingQueue`:这是一种有限缓冲的队列。它具有固定的容量,可以保存一定数量的元素。当队列已满时,生产者线程会被阻塞。这种队列适用于在生产者和消费者之间解耦的情况下,需要限制任务的数量。

4. `PriorityBlockingQueue`: 这是一种带有优先级的队列,它可以根据元素的优先级来决定元素在队列中的位置。具有更高优先级的元素会被先处理。在使用此队列时需要注意元素的排序和比较方式。
在执行execute()方法时如果状态一直是RUNNING
1、如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务;
2、如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中;
3、如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务;
4、如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。
这里要注意一下addWorker(null, false)也就是创建一个线程,但并没有传入任务,因为任务已经被添加到workQueue中了,所以worker在执行的时候,会直接从workQueue中获取任务。所以,在 workerCountOf(recheck) == 0 时执行 addWorker(null, false) 也是为了保证线程池在RUNNING状态下必须要有一个线程来执行任务。
核心线程和最大线程数怎么设置
CPU密集型 可以理解为 就是处理繁杂算法的操作,对硬盘等操作不是很频繁,比如一个算法非常之复杂,可能要处理半天,而最终插入到数据库的时间很快。

IO密集型可以理解为简单的业务逻辑处理,比如计算1+1=2,但是要处理的数据很多,每一条都要去插入数据库,对数据库频繁操作。

核心线程:
CPU密集型:核心线程数=CPU核心数(或 核心线程数=CPU核心数+1)。
I/O密集型:核心线程数=2*CPU核心数(或 核心线程数=CPU核心数/(1-阻塞系数))。

最大线程:
CPU密集型应用,最大线程设置为 N+1。
IO密集型经验应用,最大线程设置为 2N+1 (N为CPU数量,下同)。
参考链接:https://blog.csdn.net/jpkopkop/article/details/127619057
submit和 invoke的区别
1. 方法签名:`submit`方法的签名为`<T> Future<T> submit(Callable<T> task)`
													或`Future<?> submit(Runnable task)`,
				而`invoke`方法的签名为`<T> T invokeAny(Collection<? extends Callable<T>> tasks)`
													或`<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)`。

2. 返回值类型:`submit`方法返回一个`Future`对象,该对象可以用于获取任务的执行结果或取消任务的执行。
							而`invoke`方法返回一个结果或结果列表,它们可以直接用于获取任务的结果。

3. 异常处理:在执行任务时,`submit`方法可以通过`Future`对象来捕获并处理任务执行过程中的异常。
											而`invoke`方法则抛出任何一个任务抛出的异常,如果有多个任务都抛出异常,则只有第一个异常被抛出。

4. 任务顺序:`submit`方法将任务添加到线程池的任务队列中,并异步执行。因此,任务的执行顺序是不确定的。
					而`invoke`方法会同步执行任务,并且会等待所有任务执行完毕,因此任务的执行顺序是按照它们在集合中的顺序执行的。

根据以上区别,我们可以得出一些使用场景的建议:

1. 如果需要异步执行任务,并且需要获取任务执行结果或取消任务的执行,可以使用`submit`方法。
2. 如果任务列表中的每个任务都非常重要,并且需要按照它们在集合中的顺序执行,则可以使用`invokeAll`方法。
3. 如果需要同时执行多个任务,并且只需要获取第一个任务的执行结果,则可以使用`invokeAny`方法。注意,如果任务执行期间抛出异常,则第一个抛出异常的任务的结果将被返回。
总之,需要根据具体情况来选择方法。 `submit`和`invoke`方法在执行任务时具有不同的行为,因此需要根据应用程序的需要来选择一种方法。
使用完记得shutdown
ExecutorService threadPool = new ThreadPoolExecutor(poolSize, poolSize,
                        0L, TimeUnit.MILLISECONDS,
                        new LinkedBlockingQueue<Runnable>(), r -> new Thread(r, "线程池名称"));
                try {
                    List<Callable<Map<String, Object>>> materialTaskList = 业务逻辑(x x,applylength,size,xx,request);
                    threadPool.invokeAll(materialTaskList, 100000, TimeUnit.MILLISECONDS);
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    threadPool.shutdown();
                    threadPool.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
                   sql插入;}
    public List<Callable<Map<String, Object>>> 业务逻辑(参数) {
        List<Callable<Map<String, Object>>> taskList = new ArrayList<>();
        for (int i = 0; i < applylength; i += size) {
            if((applylength-i)<size){
                int counts= applylength-i;
                Callable<Map<String, Object>> task = () -> {
                    record( 参数);
                    return new HashMap<>();
                };
                taskList.add(task);
            }else {
                Callable<Map<String, Object>> task = () -> {
                    record( 参数);
                    return new HashMap<>();
                };
                taskList.add(task);
            }
        }
        return taskList;
    }
}

List<List<参数>> list = new ArrayList<>(); // 创建列表
List<List<参数>> 参数 = Collections.synchronizedList(list); // 创建线程安全的列表

synchronized (参数) { // 使用 synchronized 关键字保证同步
  参数.add(list1);
}

注意点:

1、使用线程池的时候要定义名称,否则在查询日志的时候不知道是那个出了问题

2、在shutdow后增加threadPool.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);否则主线程执行完,多线程还没有执行完

3、多线程 的 核心线程数不要设置过大 会导致数据丢失

4、超时时间是看具体业务需求 如果要保证强一致性就不要设超时时间

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值