1. 什么是线程池
线程池是一种用于管理和复用线程的机制,它可以在需要执行多个任务时,通过维护一定数量的线程来提供并发执行的能力。线程池可以有效控制线程的创建、销毁和复用,从而减少线程创建与销毁的开销,并且能够更好地管理系统资源,提高应用程序的性能和效率。
2.使用线程池的优势
1.提高系统性能:通过复用线程,减少线程创建和销毁的开销,并且可以控制并发线程的数量,避免系统资源耗尽。
2.提高响应速度:线程池可以快速地获取可用线程来执行任务,减少任务等待的时间,提高应用程序的响应速度。
3.提供线程管理和监控:线程池提供了对线程的管理和监控功能,可以监控线程的状态、运行情况和异常,便于调试和故障排查。
3.JUC的线程池架构![请添加图片描述](https://img-blog.csdnimg.cn/66bc1eb5bdc44482aec414b16a3de8e3.png)
JUC 就是 java.util .concurrent 工具包的简称,该工具包是从 JDK 1.5 开始加入到 JDK,用于完成高并发、处理多线程的一个工具包
1. Executor
Executor是Java中一个用于执行任务的接口,它定义了一种方式来执行异步任务,将任务的提交和执行进行解耦。Executor接口包含一个名为execute的方法,用于提交一个Runnable任务给执行器。
Executor框架提供了一种标准的、可重用的方式来执行任务,尤其适用于处理大量小型任务的情况,可以有效地管理和复用线程资源。
Executor框架的核心接口是Executor和ExecutorService:
1.Executor接口:该接口定义了一个execute方法,用于接收一个Runnable任务并立即执行。
void execute(Runnable command);
2.ExecutorService接口:这是Executor的子接口,扩展了一些功能,比如可以提交Callable任务、创建可调度的任务等。
Future<?> submit(Runnable task);
<T> Future<T> submit(Callable<T> task);
Java提供了一些已实现了Executor和ExecutorService接口的类,例如:
1.ThreadPoolExecutor:用于创建线程池,灵活地管理线程资源。
2.ScheduledThreadPoolExecutor:在ThreadPoolExecutor的基础上增加了任务调度的功能,可以按照一定的时间间隔或时间点执行任务。
ScheduledExecutorService
ScheduledExecutorService 是一个接口,它继承于于 ExecutorService。它是一个可以完成“延时”“周期性”任务的调度线程池接口,其功能和 Timer/TimerTask 类似。
ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor 继承于 ThreadPoolExecutor,它提供了 ScheduledExecutorService线程池接口中“延时执行”和“周期执行”等抽象调度方法的具体实现。
ScheduledThreadPoolExecutor 类 似 于 Timer , 但 是 在 高 并 发 程 序 中ScheduledThreadPoolExecutor 的性能要优于 Timer
使用Executor接口和相关的实现类可以方便地管理任务的执行和线程资源的分配,提高多线程编程的效率和可维护性。
4.Executors快捷创建线程池
方 法 名 | 方 法 名 功能简介 |
---|---|
newSingleThreadExecutor() | 创建只有一个线程的线程池 |
newFixedThreadPool(int nThreads) | 创建固定大小的线程池 |
newCachedThreadPool() | 创建一个不限制线程数量的线程池,任何提交的任务都将立即执行,但是空闲线程会得到及时回收 |
newScheduledThreadPool() | 创建一个可定期或者延时执行任务的线程池 |
newSingleThreadExecutor线程池特点
- 单线程化的线程池中的任务,是按照提交的次序,顺序执行的。
- 池中的唯一线程的存活时间是无限的。
- 当池中的唯一线程正繁忙时,新提交的任务实例会进入内部的阻塞队列(无界的阻塞队列)。
1.newFixedThreadPool线程池的特点
- 如果线程数没有达到“固定数量”,则每次提交一个任务池内就创建一个新线程,直到线程达到线程池的固定的数量。
- 线程池的大小一旦达到“固定数量”就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。
- 在接收异步任务的执行目标实例时,如果池中的所有线程均在繁忙状态,对于新任务会进入阻塞队列中(无界的阻塞队列)。
2.newCachedThreadPool 特点
- 在接收新的异步任务 target 执行目标实例时,如果池内所有线程繁忙,此线程池会添加新线程来处理任务。
- 此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小
- 如果部分线程空闲,也就是存量线程的数量超过了处理任务数量,那么就会回收空闲(60 秒不执行任务)线程。
- “可缓存线程池”的弊端:线程池没有最大线程数量限制,如果大量的异步任务执行目标实例同时提交,可能导致创线程过多会而导致资源耗尽。
3.newScheduledThreadPool
使用ScheduledExecutorService可以方便地创建和管理定时任务,线程池会自动分配和管理线程资源,同时提供了灵活的任务调度方式,适用于定时任务的处理。
用法:
//方法一:创建一个可调度线程池,池内仅含有一条线程
public static ScheduledExecutorService newSingleThreadScheduledExecutor();
//方法二:创建一个可调度线程池,池内含有 N 条线程, N 的值为输入参数 corePoolSize
/* Executors.newScheduledThreadPool(int nThreads)是一个静态方法,用于创建一个ScheduledExecutorService对象,具有固定大小的线程池。
该方法将返回一个ScheduledExecutorService对象,它ScheduledThreadPoolExecutor类的实例。ScheduledThreadPoolExecutor是ThreadPoolExecutor的子类,实现了ScheduledExecutorService接口,提供了任务调度的功能。*/
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) ;
通过调用ScheduledExecutorService的方法,如schedule()、scheduleAtFixedRate()、scheduleWithFixedDelay()等,可以提交定时任务和周期性任务。
scheduleAtFixedRate 方法的定义如下:
public ScheduledFuture<?> scheduleAtFixedRate(
Runnable command, //异步任务 target 执行目标实例;
long initialDelay, //首次执行延时;
long period, //两次开始执行最小间隔时间;
TimeUnit unit //所设置的时间的计时单位,如 TimeUnit.SECONDS 常量;
);
scheduleWithFixedDelay方法的定义如下:
public ScheduledFuture<?> scheduleWithFixedDelay(
Runnable command,//异步任务 target 执行目标实例;
long initialDelay, //首次执行延时;
long delay, //前一次执行结束到下一次执行开始的间隔时间(间隔执行延迟时间);
TimeUnit unit //所设置的时间的计时单位,如 TimeUnit.SECONDS 常量;
);
两个方法区别:
- scheduleAtFixedRate 是从任务开始时算起
- scheduleWithFixedDelay 是从任务结束时算起
4.下面代码是关于四个方法可以自行测试:
package Thred.Pool;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadPool {
public static final int SLEEP_GAP = 500;
public static class TargetTask implements Runnable{
static AtomicInteger taskNo = new AtomicInteger(1);
protected String taskName;
public TargetTask() {
taskName = "task-" + taskNo.get();
taskNo.incrementAndGet();
}
@Override
public void run() {
System.out.println("任务:" + taskName + " doing");
try {
Thread.sleep(SLEEP_GAP);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(taskName + " 运行结束.");
}
}
public static void main(String[] args) {
ThreadPool a =new ThreadPool();
// a.testSingleThreadExecutor();
// a.testNewFixedThreadPool();
// a.testNewCacheThreadPool();
//a.testNewScheduledThreadPool();
a.testThreadPoolExecutor();
}
public void testSingleThreadExecutor(){
ExecutorService pool = Executors.newSingleThreadExecutor();
for (int i = 0; i < 5 ; i++) {
pool.execute(new TargetTask());
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
pool.shutdown();
}
public void testNewFixedThreadPool(){
ExecutorService pool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 5; i++) {
pool.execute(new TargetTask());
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
pool.shutdown();
}
public void testNewCacheThreadPool(){
ExecutorService pool = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
pool.execute(new TargetTask());
pool.submit(new TargetTask());
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
pool.shutdown();
}
public void testNewScheduledThreadPool(){
ScheduledExecutorService scheduled =Executors.newScheduledThreadPool(2);
for (int i = 0; i < 2; i++) {
scheduled.scheduleAtFixedRate(new TargetTask(),0,500, TimeUnit.MILLISECONDS);
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
scheduled.shutdown();
}
public void testThreadPoolExecutor(){
ThreadPoolExecutor executor =new ThreadPoolExecutor(
2,
4,
100,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(2),new ThreadPoolExecutor.DiscardOldestPolicy());
for (int i = 0; i < 8; i++) {
final int taskIndex =i;
Runnable task =new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " taskIndex = " + taskIndex );
try {
Thread.sleep(10000);
// Thread.sleep(Long.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
executor.execute(task);
}
while (true) {
//每隔 1 秒,输出线程池的工作任务数量、总计的任务数量
System.out.printf("- activeCount: %d - taskCount: %d\r\n", executor.getActiveCount(), executor.getTaskCount());
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}}
5.线程池的标准创建方式
大部分企业的开发规范,都会禁止使用快捷线程池,要求通过标准构造器 ThreadPoolExecutor 去构造工作线程池。要求使用ThreadPoolExecutor创建线程池
// 使用标准构造器,构造一个普通的线程池
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数,即使线程空闲(Idle),也不会回收;
int maximumPoolSize, // 线程数的上限;
long keepAliveTime, TimeUnit unit, // 线程最大空闲(Idle)时长
BlockingQueue workQueue, // 任务的排队队列
ThreadFactory threadFactory, // 新线程的产生方式
RejectedExecutionHandler handler) // 拒绝策略
线程池管理器(ThreadPool Manager):
线程池管理器负责线程池的创建、销毁和管理。它包含一些操作线程池的方法,如提交任务、关闭线程池、设置核心线程数等。线程池管理器可以是ExecutorService接口的实现类,例如ThreadPoolExecutor。
线程工厂(Thread Factory):
线程工厂是用于创建新线程的工厂类,它定义了如何创建线程的方式。线程池在需要创建新线程时会通过线程工厂来创建,并将其添加到线程池中。
有两种方式可以来创建新线程
1.ThreadFactory threadFactory = Executors.defaultThreadFactory();
这个是用来来获取默认的线程工厂实例
2.自定义线程工厂来创建
Executors.defaultThreadFactory()方法返回的是一个默认的线程工厂实例,它创建的线程对象具有默认的配置。如果你需要自定义线程的一些属性,例如线程名称、优先级等,可以使用ThreadFactory接口的其他实现类来创建线程。
以下是一个创建自定义线程工厂的示例代码:
import java.util.concurrent.*;
public class CustomThreadFactoryExample {
public static void main(String[] args) {
int corePoolSize = 5;
int maximumPoolSize = 10;
long keepAliveTime = 1;
TimeUnit unit = TimeUnit.SECONDS;
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100);
ThreadFactory threadFactory = new CustomThreadFactory();
ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize,
keepAliveTime, unit, workQueue, threadFactory);
// 提交任务给线程池...
}
static class CustomThreadFactory implements ThreadFactory {
private final AtomicInteger counter = new AtomicInteger(1);
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("MyThread-" + counter.getAndIncrement());
thread.setPriority(Thread.NORM_PRIORITY);
// 设置其他线程属性...
return thread;
}
}
}
在上述代码中,创建了一个名为CustomThreadFactory的自定义线程工厂类,实现了ThreadFactory接口,并覆盖了其中的newThread()方法。在newThread()方法中,可以对新创建的线程对象进行自定义设置,例如设置线程名称、优先级等属性。
通过在ThreadPoolExecutor的构造方法中传入自定义的线程工厂实例,可以让线程池使用该工厂来创建线程对象。
拒绝策略(Rejected Execution Policy):
当线程池已经达到最大线程数且任务队列已满时,新提交的任务会触发拒绝策略。拒绝策略定义了如何处理无法执行的任务,常见的策略有抛出异常、丢弃任务、丢弃最早的任务等。
6.线程池的任务调度流程![请添加图片描述](https://img-blog.csdnimg.cn/d1ce6fe2ed77439cb6362c3d7025d6f4.png)
(1)当前工作线程数小于核心线程池数量,执行器总是优先创建一个任务线程,而不是从线程队列中取一个空闲线程。
(2)线程池中任务数大于核心线程池数,任务将被加入到阻塞队列中,一直到阻塞队列满。在核心线程池数量已经用完、阻塞队列没有满的场景下,线程池不是为新任务创建一个新线程。
(3)当完成一个任务的执行时,执行器总是优先从阻塞队列中取下一个任务,并开始其执行,一直到阻塞队列为空,其中所有的缓存任务被取光。
(4)在核心线程池数量已经用完、阻塞队列也已经满了的场景下,如果线程池接收到新的任务,将会为新任务创建一个线程(非核心线程),并且立即开始执行新任务。
(5)在核心线程都用完、阻塞队列已满的情况下,一直会创建新线程去执行新任务,直到池内的线程总数超出 maximumPoolSize。如果线程池的线程总数超时 maximumPoolSize,则线程池会拒绝接收任务,当新任务过来时,会为新任务执行拒绝策略。
在创建线程池时,如果线程池的参数如核心线程数量、最大线程数量、 BlockingQueue 等配置不合理,就会出现任务不能被正常调度的问题。
下面是一个错误的线程池配置示例:
@org.junit.Test
public void testThreadPoolExecutor() {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, //corePoolSize
100, //maximumPoolSize
100, //keepAliveTime
TimeUnit.SECONDS, //unit
new LinkedBlockingDeque<>(100));//workQueue
for (int i = 0; i < 5; i++) {
final int taskIndex = i;
executor.execute(() ->
{
Print.tco("taskIndex = " + taskIndex);
try {
Thread.sleep(Long.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
//每隔 1 秒,输出线程池的工作任务数量、总计的任务数量
while (true) {
Print.tco("- activeCount:" + executor.getActiveCount() +
" - taskCount:" + executor.getTaskCount());
sleepSeconds(1);
}
}
运行程序,结果如下:
12:01:49.667 [pool-1-thread-1] INFO com.lxs.demo.a_thread.ThreadPoolDemo - taskIndex = 0
12:01:49.667 [pool-1-thread-2] INFO com.lxs.demo.a_thread.ThreadPoolDemo - taskIndex = 1
12:01:50.671 [main] INFO com.lxs.demo.a_thread.ThreadPoolDemo - - activeCount:2 - taskCount:5
12:01:51.671 [main] INFO com.lxs.demo.a_thread.ThreadPoolDemo - - activeCount:2 - taskCount:5
虽然设置了maximumPoolSize为100的线程池,提交了2个任务只会执行2个
7. 线程池的拒绝策略
使用有界队列的时候,如果队列满了,提交任务到线程池的时候就会被拒绝。总体来说,任务被拒绝有两种情况:
(1)线程池已经被关闭。
(2)工作队列已满且 maximumPoolSize
已满。无论以上哪种情况任务被拒,线程都会调用RejectedExecutionHandler
实 例 的rejectedExecution
方法。RejectedExecutionHandler
是拒绝策略的接口, JUC 为该接口提供了以下几种实现:
- AbortPolicy:拒绝策略
- DiscardPolicy:抛弃策略
- DiscardOldestPolicy:抛弃最老任务策略
- CallerRunsPolicy:调用者执行策略
- 自定义策略
JUC 线程池拒绝策略的接口与类之间的关系图
(1) AbortPolicy
使用该策略时,如果线程池队列满了则新任务被拒绝,并且会抛出 RejectedExecutionException异常。该策略是线程池的默认的拒绝策略。
(2) DiscardPolicy
该策略是 AbortPolicy 的 Silent(安静)版本,如果线程池队列满了,新任务会直接被丢掉,并且不会有任何异常抛出。
(3) DiscardOldestPolicy
抛弃最老任务策略,也就是说如果队列满了,会将最早进入队列的任务抛弃,从队列中腾出空间,再尝试加入队列。因为队列是队尾进队头出,队头元素是最老的,所以每次都是移除对头元素后再尝试入队。
(4) CallerRunsPolicy
调用者执行策略。在新任务被添加到线程池时,如果添加失败,那么提交任务线程会自己去执行该任务,不会使用线程池中的线程去执行新任务。
在以上的四种内置策略中,线程池默认的拒绝策略为 AbortPolicy,如果提交的任务被拒绝,线程池抛出 RejectedExecutionException 异常,该异常是非受检异常(运行时异常),很容易忘记捕获。如果关心任务被拒绝的事件,需要在提交任务时捕获 RejectedExecutionException 异常。
(5)自定义策略
如果以上拒绝策略都不符合需求,则可自定义一个拒绝策略,实现 RejectedExecutionHandler接口的 rejectedExecution 方法即可。
自定义拒绝策略的例子,代码如下:
//自定义拒绝策略
public static class CustomIgnorePolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
// 可做日志记录等
Print.tco(r + " rejected; " + " - getTaskCount: " + e.getTaskCount());
}
}
@org.junit.Test
public void testCustomIgnorePolicy() {
int corePoolSize = 2; //核心线程数
int maximumPoolSize = 4; //最大线程数
long keepAliveTime = 10;
TimeUnit unit = TimeUnit.SECONDS;
//最大排队任务数
BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(2);
//线程工厂
ThreadFactory threadFactory = new SimpleThreadFactory();
//拒绝和异常策略
RejectedExecutionHandler policy = new CustomIgnorePolicy();
ThreadPoolExecutor pool = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime, unit,
workQueue,
threadFactory,
policy);
// 预启动所有核心线程
pool.prestartAllCoreThreads();
for (int i = 1; i <= 10; i++) {
pool.execute(new TargetTask());
}
//等待10秒
sleepSeconds(10);
Print.tco("关闭线程池");
pool.shutdown();
}
运行以上代码,大致结果如下:
8.线程池状态
线程池的 5 种状态,具体如下:
(1) RUNNING:线程池创建之后的初始状态,这种状态下可以执行任务。
(2)SHUTDOWN:该状态下线程池不再接受新任务,但是会将工作队列中的任务执行完毕。
(3) STOP:该状态下线程池不再接受新任务,也不会处理工作队列中的剩余任务,并且将会中断所有工作线程。
(4) TIDYING:该状态下所有任务都已终止或者处理完成,将会执行terminated( )钩子方法。
(5) TERMINATED:执行完terminated( )钩子方法之后的状态。
线程池的状态转换规则为:
(1)线程池创建之后状态为 RUNNING。
(2)执行线程池的 shutdown 实例方法,会使线程池状态从 RUNNING 转变为 SHUTDOWN。
(3)执行线程池的 shutdownNow 实例方法,会使线程池状态从 RUNNING 转变为 STOP。
(4)当线程池处于 SHUTDOWN 状态,执行器 shutdownNow 方法,会将其状态转变为 STOP状态。
(5)等待线程池的所有工作线程停止,工作队列清空之后,线程池状态会从 STOP 转变为TIDYING。
(6)执行完 terminated( ) 钩子方法之后,线程池状态从 TIDYING 转变为 TERMINATED 。
9.Junit单元测试不能支持测试多线程
节选自
https://blog.csdn.net/w605283073/article/details/92016433
即test方法运行在主线程中,外层函数执行完test等操作后执行System.exit来退出虚拟机,这个时候thread1和thread2可能还没执行完,就被销毁了。
也就是使用测试时候,可能线程中的任务还没执行完就因为退出虚拟机而被销毁了!