一、线程池是什么
之前我们已经认识过"池":String,字符串常量池;MySQL JDBC,数据库连接池(DataSource)…
线程诞生的原因是进程太重量了,导致创建进程/销毁进程比较低效 (内存资源的申请和释放)!而线程就是共享了内存资源,新的线程复用之前的资源,不必重新申请了 (快了)~~
但是如果线程创建的频率高了,此时线程创建销毁的开销仍然不能忽略!
此时就可以使用线程池来进一步优化这里的速度了!!!
用户态:每个进程都是自己执行自己的逻辑;
内核态:一个系统里只有这一份内核在执行逻辑,这个内核要给所有的进程都提供一些服务!
最终的结论:使用线程池是纯用户态操作,要比创建线程 (经历内核态的操作)要快~~
线程池最大的好处就是减少每次启动、销毁线程的损耗!
总结三点:
- 降低资源消耗:减少线程的创建和销毁带来的性能开销。
- 提高响应速度:当任务来时可以直接使用,不用等待线程创建。
- 可管理性:进行统一的分配、监控,避免大量的线程间因互相抢占系统资源导致的阻塞现象。
二、标准库中的线程池
- 使用 Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池
- 返回值类型为 ExecutorService
- 通过 ExecutorService.submit 可以注册一个任务到线程池中
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Demo {
public static void main(String[] args) {
ExecutorService pool = Executors.newCachedThreadPool();
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("这是任务");
}
});
}
}
Executors 创建线程池的几种方式
- newFixedThreadPool:创建固定线程数的线程池
- newCachedThreadPool:创建线程数目动态增长的线程池
- newSingleThreadExecutor:创建只包含单个线程的线程池
- newScheduledThreadPool:设定延迟时间后执行命令,或者定期执行命令,是进阶版的Timer
Executors 本质上是 ThreadPoolExecutor 类的封装。
ThreadPoolExecutor 提供了更多的可选参数,可以进一步细化线程池行为的设定。
三、线程池的执行流程
1)当新加入一个任务时,先判断当前线程数是否大于核心线程数,如果结果为 false,则新建线程并执行任务;
2)如果结果为 true,则判断任务队列是否已满,如果结果为 false,则把任务添加到任务队列中等待线程执行;
3)如果结果为 true,则判断当前线程数量是否超过最大线程数?如果结果为 false,则新建线程执行此任务;
4)如果结果为 true,执行拒绝策略。
四、实现线程池
3.1 思路与细节
一个线程池可以同时提交N个任务,对应的线程池中有M个线程来负责完成这N个任务,如何把N个任务分配给M个线程呢?
生产者消费者模型正好可以解决这个问题!
先搞一个阻塞队列,每个被提交的任务都被放到阻塞队列中。搞M个线程来取队列元素。如果队列空了,M个线程自然阻塞等待;如果队列不为空,每个线程都取任务、执行任务,完了再来取下一个…直到队列为空,线程继续阻塞~~
注意:
1)阻塞队列能够保证线程安全~
2)Thread的线程跑起来之后,在执行过程中,是不会被提前销毁的!run() 执行完了才会被销毁~
3)线程什么时候结束呢?不用结束!因为无法判定啥时候会有新的线程过来!
一般还是在服务器开发中会更常用到线程池,这时线程池肯定是要持续工作的了~
如果非要结束,单独写一个shutdown方法,强制interrupt所有的工作线程 (如果你要写shutdown,后面是需要操作这些线程实例的,所以需要用数组保存起来)
3.2 完整代码
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
class MyThreadPool {
private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
public void submit(Runnable runnable) throws InterruptedException {
queue.put(runnable);
}
public MyThreadPool(int m) {
// 在构造方法中, 创建出 M 个线程. 负责完成工作.
for (int i = 0; i < m; i++) {
Thread t = new Thread(() -> {
while (true) {
try {
Runnable runnable = queue.take();
runnable.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
}
}
}
public class Demo {
public static void main(String[] args) throws InterruptedException {
MyThreadPool pool = new MyThreadPool(10);
for (int i = 0; i < 1000; i++) {
int taskId = i; // i是一直在变化的,所以我们创建一个新的变量在Runnable进行捕获!
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("执行当前任务: " + taskId + " 当前线程: " + Thread.currentThread().getName());
}
});
}
}
}
五、标准库里的构造方法
标准库里提供的 ThreadPoolExecutor 其实要更复杂一些,尤其是构造方法,可以支持很多参数,可以支持很多选项,让我们来创建出不同风格的线程池~~
标准库里带的拒绝策略:重点
(领导:鹏哥;员工:汤老湿)
1)AbortPolicy:中止策略,线程池会抛出异常并中止执行此任务;
2)CallerRunsPolicy:把任务交给添加此任务的线程来执行;
3)DiscardPolicy:忽略此任务(最新加入的任务);
4)DiscardOldestPolicy:忽略最先加入队列的任务(最老的任务)。
例题: 使用ThreadPoolExecutor创建一个忽略最新任务的线程池,创建规则:
1.核心线程数为5
2.最大线程数为10
3.任务队列为100
4.拒绝策略为忽略最新任务
代码实现:
public class Thread_Demo {
public static void main(String[] args) throws InterruptedException {
// 依题意创建线程池
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5, // 核心线程数
10, // 最大线程数
3, // 线程空闲时长
TimeUnit.SECONDS, // 线程空闲时长的时间单位
new LinkedBlockingQueue<>(100), // 任务队列
new ThreadPoolExecutor.DiscardPolicy()); // 拒绝策略为忽略最新任务
// 测试执行
for (int i = 0; i < 2000; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "已执行.");
},"thread-" + (i + 1)).start();
}
}
}