什么是线程池
在之前JDBC编程中,通过DataSource获取Connection的时候就已经用到了池的概念。这里的池指的是数据库连接池。当Java程序需要数据库连接的时候就从池子中拿一个空闲的连接对象给Java程序,Java程序用完连接之后就会返回给连接池。线程池就是在池子里放的是线程本身,当程序启动的时候就创建出若干个线程,如果有任务就处理,没任务就阻塞等待。
为什么用线程池
当创建一个线程时,系统申请资源,将其加入到PCB的链表中,当需要销毁线程时释放资源,从PCB链表中移除。线程池的作用就是为了减少这些关于申请和释放PCB的操作,尽量保证程序在用户态执行,减少系统创建线程的开销。
JDK提供的线程池
①Executors.newCachedThreadPool()
:处理大量短时间工作任务的线程池;
②Executors.newFixedThreadPool()
:创建一个固定大小线程池;
③Executors.newSingleThreadExecutor()
: 创建一个只有一个工作线程的线程池;
④Executors.newSingleThreadScheduledExecutor()
:创建一个带时间定时的线程池;
⑤Executors.newScheduledThreadPool()
:创建一个指定大小并带时间定时的线程池;
⑥Executors.newWorkStealingPool()
:创建一个指定大小(不传入参数,为当前机器CPU核心数)的线程池,并行地处理任务。
// 1. 用来处理大量短时间工作任务的线程池,如果池中没有可用的线程将创建新的线程,如果线程空闲60秒将收回并移出缓存
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
// 2. 创建一个操作无界队列且固定大小线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
// 3. 创建一个操作无界队列且只有一个工作线程的线程池
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
// 4. 创建一个单线程执行器,可以在给定时间后执行或定期执行。
ScheduledExecutorService singleThreadScheduledExecutor = Executors.newSingleThreadScheduledExecutor();
// 5. 创建一个指定大小的线程池,可以在给定时间后执行或定期执行。
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
// 6. 创建一个指定大小(不传入参数,为当前机器CPU核心数)的线程池,并行地处理任务,不保证处理顺序
Executors.newWorkStealingPool();
无界队列:指的是对于队列中的元素个数不加限制,可能会出现能存被消耗殆尽的情况。
工厂模式
在JDK中通过Executors工具类调用一系列方法完成不同功能线程池的创建涉及一种设计模式:工厂模式。
这种模式就是为了解决构造方法创建对象的不足,比如下面这种情况:
想通过id和age属性分别创建不同的学生对象,但由于重载方法的参数列表相同,所以会报错。可以定义静态方法起不同的方法名,根据传来的数据按照工厂方法里的逻辑返回对应的对象。
class Student {
private int id;
private int age;
private String name;
// // 通过id和name属性来构造一个学生对象
// public Student (int id , String name) {
// this.id = id;
// this.name = name;
//
// }
// // 通过age 和 name 属性来构造一个学生对象
// public Student (int age , String name) {
// this.age = age;
// this.name = name;
// }
// 通过id和name属性来构造一个学生对象
public static Student createByIdAndName (int id, String name) {
Student student = new Student();
student.setId(id);
student.setName(name);
return student;
}
// 通过age 和 name 属性来构造一个学生对象
public static Student createByAgeAndName (int age, String name) {
Student student = new Student();
student.setAge(age);
student.setName(name);
return student;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
如何使用
当创建好一个JDK提供的线程池之后,线程池中就存好了一些已创建好的线程,只需要往线程池中提交任务即可。当任务被提交到线程池之后,任务就会被自动执行。
public class Demo03_ThreadPool_Use {
public static void main(String[] args) throws InterruptedException {
// 创建一个线程池
ExecutorService threadPool = Executors.newFixedThreadPool(3);
// 提交任务到线程池
for (int i = 0; i < 10; i++) {
int taskId = i;
threadPool.submit(() -> {
System.out.println("我是任务 " + taskId + ", " + Thread.currentThread().getName());
});
}
// 模拟等待任务
TimeUnit.SECONDS.sleep(5);
System.out.println("第二阶段开始");
// 提交任务到线程池
for (int i = 10; i < 20; i++) {
int taskId = i;
threadPool.submit(() -> {
System.out.println("我是任务 " + taskId + ", " + Thread.currentThread().getName());
});
}
}
}
自定义线程池
1.可以提交任务到线程池,就需要一种数据结构来保存提交的任务。可以考虑用阻塞队列来保存任务。
2.创建线程池时需要指定初始线程数量,这些线程不停扫描阻塞队列,如果有任务就立即执行。可以考虑使用线程池对象的构造方法,接收要创建的线程数据,并在构造方法中完成线程的创建。
代码实现
public class MyThreadPool {
// 1. 定义一个阻塞队列来保存我们的任务
BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(3);
// 2. 对外提供一个方法,用来往队列中提交任务
public void submit (Runnable task) throws InterruptedException {
queue.put(task);
}
// 3. 构造方法
public MyThreadPool(int capacity) {
if (capacity <= 0) {
throw new RuntimeException("线程数量不能小于0.");
}
// 完成线程的创建,扫描队列,取出任务并执行
for (int i = 0; i < capacity; i++) {
// 创建线程
Thread thread = new Thread(() -> {
while (true) {
try {
// 取出任务(扫描队列的过程)
Runnable take = queue.take();
// 执行任务
take.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
// 启动线程
thread.start();
}
}
}
测试
public class Demo04_MyThreadPool {
public static void main(String[] args) throws InterruptedException {
// 创建自定义的线程池
MyThreadPool threadPool = new MyThreadPool(3);
// 往线程池中提交任务
for (int i = 0; i < 10; i++) {
int taskId = i;
threadPool.submit(() -> {
System.out.println("我是任务 " + taskId + ", " + Thread.currentThread().getName());
});
}
// 模拟等待任务
TimeUnit.SECONDS.sleep(3);
System.out.println("第二阶段开始");
// 提交任务到线程池
for (int i = 10; i < 20; i++) {
int taskId = i;
threadPool.submit(() -> {
System.out.println("我是任务 " + taskId + ", " + Thread.currentThread().getName());
});
}
}
}
ThreadPoolExecutor类的构造方法
通过工厂方式获取的线程池,最终都是ThreadPoolExecutor类的对象。打开源码可以看到,要创建这样一个对象,需要传递7个参数。
int corePoolSize :核心线程数,创建线程时包含的最小线程数量;
int maximumPoolSize:最大线程数,也可以叫临时线程数。当核心线程数不够用的时候,允许系统可以创建的最多线程数;
longkeepAliveTime:临时线程的空闲时长;
TimeUnit unit:空闲的时间单位,和keepAliveTime一起使用;
BlockingQueue< Runnable >workQueue: 用来保存任务的阻塞队列;
ThreadFactory threadFactory:线程工厂,如何创建线程,用系统默认的即可;
RejectedExecutionHandler handler:拒绝策略,当线程池处理不了过多的任务时会触发。
我们用一个场景将这7个参数串起来:
工作原理
1.当任务添加到线程池中时,先判断任务数是否大于核心线程数;
2.如果不大于直接执行,否则加入阻塞队列;
3.当队列满了之后,会按照指定的线程最大数创建临时线程;
4.当阻塞队列满了且临时线程创建完成,再提交任务就会执行拒绝策略。
5.当任务减少,临时线程达到空闲时常时,会被回收。
⚠️⚠️⚠️注意:当需要创建临时线程的时候,会一次性创建到指定的临时线程数。
拒绝策略
API中提供了四种拒绝策略。根据业务场景选择合适的拒绝策略即可。
线程池的使用
为什么不推荐使用系统自带的线程池?
1.由于使用了无界队列,可能会有内存耗尽的风险;
2.创建的临时线程数是整型的最大值(Executors.newCachedThreadPool()),无法把控,也有可能造成资源耗尽;
所以在使用线程池时,我们要根据不同的业务要求指定参数和拒绝策略,规避内存被耗尽的风险。
public class Demo05_ThreadPoolExecutor {
public static void main(String[] args) throws InterruptedException {
// 创建线程池并指定参数
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(1, 1, 1, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1), new ThreadPoolExecutor.DiscardPolicy());
// 添加任务到线程池
for (int i = 0; i < 10; i++) {
int taskId = i;
poolExecutor.submit(() -> {
poolExecutor.submit(() -> {
System.out.println("我是任务 " + taskId + ", " + Thread.currentThread().getName());
});
});
}
// 模拟等待任务
TimeUnit.SECONDS.sleep(3);
System.out.println("第二阶段开始");
// 提交任务到线程池
for (int i = 10; i < 20; i++) {
int taskId = i;
poolExecutor.submit(() -> {
System.out.println("我是任务 " + taskId + ", " + Thread.currentThread().getName());
});
}
}
}
继续加油~