🏀🏀🏀来都来了,不妨点个关注!
🎧🎧🎧博客主页:欢迎各位大佬!
文章目录
1. 什么是线程池
说起池,我们就会联想到之前学过的字符串常量池,数据库连接池等,关于“池”还是很常见的,池的目的就是为了减少每次获取资源的消耗,提高对资源的利用率。
线程池就是提前将线程准备好,创建线程不再是从系统中申请,而是直接从线程池中取,线程不用了也是还给线程池。
1.1 为什么要使用线程池
上面我们说了使用池主要是为了减少每次获取资源的消耗,提高对资源的利用率。我们就需要从使用线程池的好处出发解释:
- 降低资源消耗:频繁的创建和销毁线程对资源消耗较大,使用线程池可以复用已经创建的线程。
- 提高响应速度:当任务到达时,任务可以不需要等待线程的创建就能立即执行。
- 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以对线程进行统一的分配、调优和监控。
1.2 如何创建线程池
1.2.1 通过ThreadPoolExecutor构造方法创建线程池
ThreadPoolExecutor是创建线程池最原始也是最推荐的方式,它提供了7个参数来灵活配置线程池。
ThreadPoolExecutor executor = new ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler);
1.2.2 通过Executors工厂类创建线程池
Executors工厂类提供了多种便捷的线程池创建方法,这些方法内部都是通过ThreadPoolExecutor类实现的,但简化了配置过程。
- FixedThreadPool:创建一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待。
ExecutorService pool = Executors.newFixedThreadPool(int nThreads);
其中的nThreads是线程池中线程的数量。
- CachedThreadPoo:创建一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程。
ExecutorService Pool = Executors.newCachedThreadPool();
这种线程池适合执行大量短期异步任务。
- SingleThreadExecutor:创建单个线程的线程池,它可以保证先进先出的执行顺序。
ExecutorService pool = Executors.newSingleThreadExecutor();
这种线程池适合需要顺序执行的任务。
- ScheduledThreadPool:创建一个可以执行延迟任务的线程池。
ExecutorService pool = Executors.newScheduledThreadPool(int corePoolSize);
其中的corePoolSize是核心线程数。
2. ThreadPoolExecutor 类
ThreadPoolExecutor 类,是 ExecutorService 接口的一个实现类,是原装的线程池类,上述的所有工厂方法都是对这个类进行进一步的封装。
这里我们可以打开官方文档,对ThreadPoolExecutor进行详细的了解。
2.1 构造方法
可以看到ThreadPoolExecutor 类的构造方法里的参数有很多,这里我们拿最后一个,最全的进行解释:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler);
- corePoolSize: 核心线程数,即使此时线程池中没有任务也不会销毁
- maximumPoolSize:最大线程数,
- keepAliveTime:非核心线程存活时间
- unit:keepAliveTime参数的时间单位,(比如毫秒,秒,分钟)
- workQueue:任务队列,用于存放等待执行的任务的阻塞队列,可以选择不同类型的队列,如ArrayBlockingQueue、LinkedBlockingQueue等。
- threadFactory:线程工厂,用于创建线程使用的工厂类
- handler:线程池的拒绝策略,当线程池里的任务满了继续往里面添加任务时如何拒绝。
对于核心线程和最大线程数我们可以那公司的员工进行举例:
把公司比作一个线程池,里面有正式工和实习生,正式工就类似于核心线程数,他们负责处理任务,当任务特别多的时候,公司就会招实习生进来打杂,实习生就类似于非核心线程数,正式工+实习生的数量则是最大线程数,而当公司任务变少了不忙了,公司为了节省成本和资源就会将实习生裁掉,而实习生在公司待的时间就是非核心线程存活时间。如下图:
2.2 四种拒绝策略
ThreadPoolExecutor.AbortPolicy(中止策略):这是默认的拒绝策略,如果任务满了,继续添加任务,直接抛出RejectedExecutionException异常。
ThreadPoolExecutor.CallerRunsPolicy(调用者策略):添加的线程自己执行这个任务。
ThreadPoolExecutor.DiscardPolicy(丢弃策略): 丢弃最新的任务,不处理新任务,直接丢弃掉。
ThreadPoolExecutor.DiscardOldestPolicy(丢弃最老策略): 丢弃最老的任务,会丢弃任务队列中的头结点(通常是存活时间最长且未被处理的任务),然后尝试执行新的任务。
3.实现一个线程池
下面线程实现的是一个固定线程数的线程池,和我们上面的FixedThreadPool类似,如下:
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;
class MyThreadPool {
private BlockingDeque<Runnable> queue = new LinkedBlockingDeque<>();
public void submit(Runnable runnable) throws InterruptedException {
queue.put(runnable);
}
public MyThreadPool(int n) {
for (int i = 0; i < n; i++) {
Thread t = new Thread( () -> {
try {
while (true) {
Runnable runnable = queue.take();
runnable.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t.start();
}
}
}
public class ThreadDemo26 {
public static void main(String[] args) throws InterruptedException {
MyThreadPool myThreadPool = new MyThreadPool(10);
for (int i = 0; i < 1000; i++) {
int number = i;
myThreadPool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello" + number);
}
});
}
}
}
运行结果如下:
实现过程:
- 手动实现线程池,核心的数据结构是 BlockingQueue,用于存放各个可执行的任务
- submit()方法,是提交任务,将任务添加到队列中
- 实现一个固定线程数的线程池,MyThreadPool(int n),其中 n 为固定线程数量,在线程池构造方面里面,通过 for 循环,创建 n 个 工作线程,n 个线程并发执行,每个线程的任务是从队列中获取的,并进行执行,即在 while(true) 循环中,既有取队列操作,又有执行的操作;将 while(true) 中,循环条件设置为 true,是不让线程在执行完任务后终止,保持工作线程活跃的状态,如果去掉 true,可以看到,只会执行 n 次,因为在执行完后线程终止了不再执行,线程池数量不够任务数量,就无法处理后续任务
- 在主线程中,创建线程池,并通过 for 循环 与 submit() 向阻塞队列提交 1000 个任务,工作线程从队列中获取任务并进行执行
- 为什么要将 i 赋值 给 number,而不直接打印 i,这里涉及到 lambda 表达式的变量捕获规则,lambda 表达式捕获的变量必须是 final 修饰或者是实际 final,实际 final 是不能被修改的,在这里因为 i 变量被修改了,创建一个新的变量保存 i,即可解决
3.1 如何给线程池设置合理的线程数
在实际开发中怎么和线程池设置合理的线程数呢?
我们知道线程池中的线程数并不是越多越好,每一个线程都需要CPU的调度,都会对资源产生消耗,所以,一个线程池的线程数的数量如何设置,需要结合实际情况出发:
- CPU密集型任务:目标是尽量减少线程的上下文切换,以优化CPU的使用率,所以线程数一般设置为CPU的数量或者+1,
- IO密集型任务:对于线程经常处于等待状态(等待IO操作),可以设置更多的线程来提高并发性(比如两倍),从而增加CPU的利用率。
3.2 线程池处理任务的流程
线程池处理任务的流程图如下:
- 提交一个任务后,当此时的空闲线程数小于核心线程数(corePoolSize),就会创建一个核心线程来执行任务。
- 如果此时的空闲线程数大于核心线程数。就会将任务添加到工作队列。
- 如果工作队列满了,并且此时的线程数小于最大线程数,就会创建一个非核心线程来执行任务。
- 当此时的线程数等于最大线程数,再创建线程就会大于最大线程数了,此时会执行拒绝策略。
3.3 线程池提交任务
当有新任务需要执行的时候,这些任务会被提交到线程池,在我们上面自己实现的线程池中使用的是submit()方法,提交任务的方法有两种,如下:
- execute()方法:这种方式提交的任务是Runnable的,并且没有返回值,当任务执行出现异常,通常是直接抛出异常。
- submit()方法:这种方式既可以提交Runnable类型的任务,也可以提交Callable类型的任务。对于Callable类型的任务,submit()方法可以返回Future类型的结果,用于获取线程任务执行的结果。当任务产生异常时,异常通常会被捕获。
3.4 线程池的关闭
线程池的关闭可以通过shutdown()和shutdownNow()两个方法,它们的原理是遍历线程池中的工作线程,然后逐个调用线程的 interrupt 方法来中断线程,所以无法响应中断的任务可能永远无法终止。区别如下:
- shutdown() :此方法会等待线程池中的所有任务执行完后关闭线程。
- shutdownNow() :此方法会尝试立即停止所有正在执行的任务,并返回那些未开始执行的任务列表。
本次分享就结束了,感谢支持!