【2023】java多线程之线程池讲解汇总(包括代码举例)

超、凢脫俗 2023-10-13 10:39 18阅读 0赞

一、JAVA使用线程池主要的三个原因

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

线程池的创建方式总共有7种,总体来说分为2类:

1. 通过 ThreadPoolExecutor 创建的线程池;

2. 通过 Executors 创建的线程池。

二、线程池创建方式:

  • Executors.newFixedThreadPool:创建⼀个固定⼤⼩的线程池,可控制并发的线程数,超出的线程会在队列中等待;
  • Executors.newCachedThreadPool:创建⼀个可缓存的线程池,若线程数超过处理所需,缓存⼀段时间后会回收,若线程数不够,则新建线程;
  • Executors.newSingleThreadExecutor:创建单个线程数的线程池,它可以保证先进先出的执⾏顺序;
  • Executors.newScheduledThreadPool:创建⼀个可以执⾏延迟任务的线程池;
  • Executors.newSingleThreadScheduledExecutor:创建⼀个单线程的可以执⾏延迟任务的线程池;
  • Executors.newWorkStealingPool:创建⼀个抢占式执⾏的线程池(任务执⾏顺序不确定)【JDK1.8 添加】。
  • ThreadPoolExecutor:最原始的创建线程池的⽅式,它包含了 7 个参数可供设置,后⾯会详细讲。

#

添加任务的方式:
*使用submit可以执行有返回值的任务或者是无返回值的任务;而execute只能执行不带返回值的任务。 *****

使用线程工厂创建线程:

在这里插入图片描述

提供的功能:

1. 设置(线程池中)线程的命名规则。

2. 设置线程的优先级。

3. 设置线程分组。

4. 设置线程类型(用户线程、守护线程)。

三、创建线程池

1. 创建固定大小线程池
在这里插入图片描述

2. 创建可缓存的线程池,感觉线程任务数量创建

  1. 优点:在一定时间内可以重复使用这些线程,产生相应的线程池。
  2. 缺点:适用于短时间有大量任务的场景,它的缺点是可能会占用很多的资源。
    在这里插入图片描述

3. 创建延时定时任务线程池

  1. scheduleAtFixedRate 是以上⼀次任务的开始时间,作为下次定时任务的参考时间的(参考时间+延迟任务=任务执⾏)。*
  2. scheduleWithFixedDelay 是以上⼀次任务的结束时间,作为下次定时任务的参考时间的。*
  3. schedule():可以设置延时时间,定时执行;
    在这里插入图片描述
    4. 创建单线程线程池
    在这里插入图片描述
  4. 创建可抢占式线程池
    在这里插入图片描述

⭐6. 使用ThreadPoolExecutor创建
ThreadPoolExecutor参数说明:
在这里插入图片描述
拒绝策略

  • JDK4种+1种自定义策略
    在这里插入图片描述
    具体代码
    在这里插入图片描述

执行流程

  1. 线程总数量 < corePoolSize,无论线程是否空闲,都会新建一个核心线程执行任务(让核心线程数量快速达到corePoolSize,在核心线程数量 <
    corePoolSize时)。注意,这一步需要获得全局锁。
  2. 线程总数量 >= corePoolSize时,新来的线程任务会进入任务队列中等待,然后空闲的核心线程会依次去缓存队列中取任务来执行(体现了线程复用)。
  3. 当缓存队列满了,说明这个时候任务已经多到爆棚,需要一些“临时工”来执行这些任务了。于是会创建非核心线程去执行这个任务。注意,这一步需要获得全局锁。
  4. 缓存队列满了, 且总线程数达到了maximumPoolSize,则会采取上面提到的拒绝策略进行处理。
    在这里插入图片描述

阻塞队列

我们假设一种场景,生产者一直生产资源,消费者一直消费资源,资源存储在一个缓冲池中,生产者将生产的资源存进缓冲池中,消费者从缓冲池中拿到资源进行消费,这就是大名鼎鼎的生产者-消费者模式

该模式能够简化开发过程,一方面消除了生产者类与消费者类之间的代码依赖性,另一方面将生产数据的过程与使用数据的过程解耦简化负载。

我们自己coding实现这个模式的时候,因为需要让多个线程操作共享变量(即资源),所以很容易引发线程安全问题,造成重复消费死锁,尤其是生产者和消费者存在多个的情况。另外,当缓冲池空了,我们需要阻塞消费者,唤醒生产者;当缓冲池满了,我们需要阻塞生产者,唤醒消费者,这些个等待-唤醒逻辑都需要自己实现。

BlockingQueue是Java util.concurrent包下重要的数据结构,区别于普通的队列BlockingQueue提供了线程安全的队列访问方式,并发包下很多高级同步类的实现都是基于BlockingQueue实现的。

BlockingQueue一般用于生产者-消费者模式,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。BlockingQueue就是存放元素的容器

阻塞队列提供了四组不同的方法用于插入、移除、检查元素:


































方法\处理方式 抛出异常 返回特殊值 一直阻塞 超时退出
插入方法 add(e) offer(e) put(e) offer(e,time,unit)
移除方法 remove() poll() take() poll(time,unit)
检查方法 element() peek() - -

阻塞队列:BlockingQueue workQueue

  • 维护着等待执行的Runnable任务对象。
  1. LinkedBlockingQueue

    1. 链式阻塞队列,底数据结构是链表,默认大小是Integer.MAX_VALUE,也指定大小
  2. ArrayBlockingQueue

    1. 数组阻塞队列,底层数据结构是数组,需要指定队列的大小。
  3. SynchronousQueue

    1. 同步队列,内部容量为0,每个put操作必须等待一个take操作,反之亦然。
  4. DelayQueue

    1. 延迟队列,该队列中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。
  5. PriorityBlockingQueue

    1. 无界阻塞队列,优先级的判断通过构造函数传入的Compator对象来决定。

阻塞队列的原理

阻塞队列主要是利用了Lock锁的多条件(Condition)阻塞控制。

首先是构造器,除了初始化队列的大小和是否是公平锁之外,还对同一个锁(lock)初始化了两个监视器,分别是notEmpty和notFull。这两个监视器的作用目前可以简单理解为标记分组,当该线程是put操作时,给他加上监视器notFull,标记这个线程是一个生产者;当线程是take操作时,给他加上监视器notEmpty,标记这个线程是消费者。

发表评论

表情:
评论列表 (有 0 条评论,18人围观)

还没有评论,来说两句吧...

相关阅读