线程池

ThreadPoolExecutor

  1. 为什么使用线程池,优势?

    1. 线程池做的工作主要是 控制运行的线程的数量 ,处理过程中将任务加入队列,然后在线程创建后启动这些任务,如果显示超过了最大数量,超出的数量的线程排队等候,等其他线程执行完毕,再从队列中取出任务来执行.

    2. 它的主要特点为:线程复用 | 控制最大并发数 | 管理线程

  2. 线程池如何使用(Java中的线程池是通过 Executor 框架实现的,该框架中用到 Executor,Executors,ExecutorService,ThreadPoolExecutor这几个类)。

1660975886058

  1. 方法详解与代码实现

    三个方法:

    • Executors.newFixedThreadPool(int) : 给定线程数量的线程池
    • Executors.newSingleThreadExecutor( ) : 只有一个线程的线程池
    • Executors.newCachedThreadPool( ) : 一池N线程

newFixedThreadPool

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @Date 2022/8/20 14:16
* @Author c-z-k
*/
public class ThreadPoolDemo {
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(5);

for (int i = 0; i < 10; i++) {
threadPool.submit(()->{
System.out.println(Thread.currentThread().getName()+ "\t 办理业务~!");
});
}

}
}

运行输出:

pool-1-thread-2 办理业务
pool-1-thread-4 办理业务

pool-1-thread-1 办理业务
pool-1-thread-4 办理业务

pool-1-thread-3 办理业务
pool-1-thread-4 办理业务

pool-1-thread-1 办理业务
pool-1-thread-2 办理业务

pool-1-thread-5 办理业务
pool-1-thread-3 办理业务

结论:发现 定长线程池 确实最多只会有五个线程。拥有控制最大并发数的功能,超出的任务线程会在队列中等待

点进 构建方法:

1
2
3
4
5
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}

发现使用的是 LinkedBlockingQueue() ;该队列的特点是 由链表结构组成的有界(但大小默认值 Integer.MAX_VALUE)阻塞队列.

newSingleThreadExecutor

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @Date 2022/8/20 14:16
* @Author c-z-k
*/
public class ThreadPoolDemo {
public static void main(String[] args) {
// ExecutorService threadPool = Executors.newFixedThreadPool(5);
ExecutorService threadPool = Executors.newSingleThreadExecutor();

for (int i = 0; i < 10; i++) {
threadPool.submit(()->{
System.out.println(Thread.currentThread().getName()+ "\t 办理业务~!");
});
}

}
}

运行输出:

pool-1-thread-1 办理业务
pool-1-thread-1 办理业务

pool-1-thread-1 办理业务
pool-1-thread-1 办理业务

pool-1-thread-1 办理业务
pool-1-thread-1 办理业务

pool-1-thread-1 办理业务
pool-1-thread-1 办理业务

pool-1-thread-1 办理业务
pool-1-thread-1 办理业务

点进 构建方法:

1
2
3
4
5
6
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}

是一个单线程化的线程池,所有任务进来都会保证按顺序进行。核心线程数和最大线程数都设为1 使用的是 LinkedBlockingQueue();

newCachedThreadPool

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package tech.chen.juccode.a05;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
* @Date 2022/8/20 14:16
* @Author c-z-k
*/
public class ThreadPoolDemo {
public static void main(String[] args) {
// ExecutorService threadPool = Executors.newFixedThreadPool(5);
// ExecutorService threadPool = Executors.newSingleThreadExecutor();
ExecutorService threadPool = Executors.newCachedThreadPool();

for (int i = 0; i < 10; i++) {
threadPool.submit(()->{
System.out.println(Thread.currentThread().getName()+ "\t 办理业务~!");
});
}

}
}

运行输出:

pool-1-thread-1 办理业务
pool-1-thread-3 办理业务

pool-1-thread-2 办理业务
pool-1-thread-4 办理业务

pool-1-thread-2 办理业务
pool-1-thread-5 办理业务

pool-1-thread-6 办理业务
pool-1-thread-8 办理业务

pool-1-thread-7 办理业务
pool-1-thread-9 办理业务

点进 构建方法:

1
2
3
4
5
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}

可缓存的线程池,如果将线程池的长度超过处理需要,可灵活回收空闲线程,若无可回收,则创建心的线程。核心线程数为0 ,最大线程数可以说无上限,但是使用的 是 SynchronousQueue() 阻塞队列;线程空闲 60 秒 ,则自动回收。

线程池的七大参数

  1. corePoolSize : 线程池中的常驻核心线程数

    • 在创建了线程池后,当有请求任务来之后,就会安排池中的线程去执行请求任务,近似理解为今日当值线程

    • 当线程池中的线程数目达到 corePoolSize 后,就会把到达的任务放入到缓存队列当中.

  2. maximumPoolSize : 线程池能够容纳同时执行的最大线程数,此值大于等于1

  3. keepAliveTime : 多余的空闲线程存活时间,当空闲时间达到 keepAliveTime 值时,多余的线程会被销毁直到只剩下corePoolSize个线程为止(非核心线程)

  4. unit : keepAliveTime的单位

  5. workQueue : 任务队列,被提交但尚未被执行的任务(候客区)

  6. threadFactory : 表示生成线程池中工作线程的线程工厂,用户创建新线程,一般用默认即可

  7. handler : 拒绝策略,表示当线程队列满了并且工作线程大于等于线程池的最大显示 数(maxnumPoolSize)时如何来拒绝

    • AbortPolicy(默认) : 当队列满了,正在执行任务的线程数也满的时候,又新进来了任务线程,此时会直接报异常 RejectException
    • CallerRunPolicy : 将新进来的线程任务,返回给调用者,例如 main.
    • DiscardOldestPolicy : 将最早进入队列的任务删除,之后再尝试加入队列
    • DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常.如果允许任务丢失,这是最好的拒绝策略

线程池工作原理

1660978169987

线程池用过吗?

线程池的拒绝策略请你谈谈

  1. 等待队列也已经排满了,再也塞不下新的任务了。同时,线程池的 maximumPoolSize 也到达了,无法接续为新任务服务,这时我们需要拒绝策略机制合理的处理这个问题

  2. JDK内置的拒绝策略

  • AbortPolicy(默认):直接抛出RejectedException异常阻止系统正常运行

  • CallerRunsPolicy:”调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是返回给调用者进行处理

  • DiscardOldestPolicy:将最早进入队列的任务删除,之后再尝试加入队列

  • DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常.如果允许任务丢失,这是最好的拒绝策略

    以上内置策略均实现了RejectExecutionHandler接口

工作中我们一般怎么用

答案是一个都不用,我们生产上只能使用自定义的。

参考阿里巴巴java开发手册
【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。 说明:使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
【强制】线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。说明:Executors返回的线程池对象的弊端如下:

  1. FixedThreadPool 和 SingleThreadPool:允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
  2. CachedThreadPool 和 ScheduledThreadPool:允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
  1. AbortPolicy : 最大不会抛出异常的值 = maximumPoolSize + new LinkedBlockingDeque(3) =8个。如果超过8个,默认的拒绝策略会抛出异常
  2. CallerRunPolicy : 如果超过8个,不会抛出异常,会返回给调用者去
  3. DiscardOldestPolicy : 如果超过8个,将最早进入队列的任务删除,之后再尝试加入队列
  4. DiscardPolicy : 直接丢弃任务,不予任何处理也不抛出异常.如果允许任务丢失,这是最好的拒绝策略

如何合理配置线程池

  1. CPU密集型

    1660978695654

    简单说就是,开多了也没用,cpu就那样,搞得太多反而会降低效率。

  2. IO密集型

1660978764228

线程池的运行流程

  1. 提交任务后会首先进行当前工作线程数与核心线程数的比较,如果当前工作线程数小于核心线程数,则直接调用 addWorker() 方法创建一个核心线程去执行任务;

  2. 如果工作线程数大于核心线程数,即线程池核心线程数已满,则新任务会被添加到阻塞队列中等待执行,当然,添加队列之前也会进行队列是否为空的判断;

  3. 如果线程池里面存活的线程数已经等于核心线程数了,且阻塞队列已经满了,再会去判断当前线程数是否已经达到最大线程数 maximumPoolSize,如果没有达到,则会调用 addWorker() 方法创建一个非核心线程去执行任务;

  4. 如果当前线程的数量已经达到了最大线程数时,当有新的任务提交过来时,会执行拒绝策略

总结来说就是优先核心线程、阻塞队列次之,最后非核心线程。