【JavaEE】多线程 -- 线程池
目录
- 线程池是什么
- 感性理解线程池的目的
- 以用户态和内核态角度来观察线程池的好处
- 标准库中的线程池
- 为什么创建线程池对象需要调用方法来创建
- ThreadPoolExecutor 类构造方法的参数
- 拒绝策略(面试重点)
- 模拟实现线程池
线程池是什么
- 我们之前说了创建线程的目的就是为了解决多进程创建开销太大的问题. 所以我们用了创建比进程开销小的线程来解决.
- 可是随着时代的发展, 服务器需要处理的请求越来越多. 创建的线程也越来越多(线程去处理这些请求),那么这样大量频繁的创建线程, 开销也是很大了.
- 所以我们引入了线程池的概念, 也就是把线程提前创建好, 放在一个池里. 需要用线程的时候直接去线程池取, 不要再去找操作系统创建. 这样我们操作系统的开销就小了.
感性理解线程池的目的
- 线程池就是提前把线程创建好,此时使用线程就可以直接从线程池里拿(这里面涉及到任务的创建,就是规划好每个线程要做什么),线程生命周期结束后再还给线程池。
- 依据上图描述:线程池最大的好处就是减少每次启动、销毁线程给系统资源带来的损耗,因为系统同时要给这么多的程序提供服务,有的时候服务不一定那么及时。
以用户态和内核态角度来观察线程池的好处
- 可以看到我们创建一个线程, 会频繁切换用户态和内核态, 线程的主要创建在内核态.
- 这样频繁的切换用户态和内核态是开销很大的.
- 有了线程池后, 我提前创建多个线程放在池里面, 用户态不需要再切换到内核态创建, 直接去线程池中拿(纯用户态操作)
标准库中的线程池
- Java标准库中给用户提供了现成的线程池。
- 使用 Executors . newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池。
- 返回值类型为:ExecutorService
- 使用 ExecutorService . submit() 方法可以创建一个任务到线程池中——(还未启动 new 状态)
public class idea23 {public static void main(String[] args) {ExecutorService pool = Executors.newFixedThreadPool(10);//创建10个线程的线程池pool.submit(new Runnable() {@Overridepublic void run() {System.out.println("创建一个new状态的线程");}});pool.shutdown();}
}
- 这里注意线程池核心线程默认是不会自动退出的,即使所有提交的任务都已执行完毕,它们也会保持等待状态。所以程序一直不会结束
- 要解决这个问题,只需在提交任务后调用线程池的 shutdown() 方法即可:
- 为什么创建线程池对象不直接调用构造方法, 而是调用一个静态方法来创建对象
为什么创建线程池对象需要调用方法来创建
- 我们都知道在数学上, 坐标可以通过平面直角坐标轴和极坐标来表示, 他们都用两个未知数来表示, 这个时候他们构造方法的参数个数一样, 类型一样, 方法名也一样. 那么就无法进行重载
- 既然是构造方法无法重载导致无法构造对象, 那么我们就不直接依赖构造方法来构造对象, 而是用工厂类的不同工厂方法名来调用同一个无参构造方法, 里面的参数用set来设定. 直接返回在工厂方法创建的对象即可.
- 上面这些 “工厂方法” 都是基于 ThreadPoolExecutor 类的封装
- Executors 类 本质上也是基于 ThreadPoolExecutor 类的封装
- ThreadPoolExecutor 类提供了更多的可选参数,可以进一步细化创建不同性质的线程池
这里我们再看一下我们Excutors的其他构造方法
ThreadPoolExecutor 类构造方法的参数
- ThreadPoolExecutor 使用可用的几个线程池之一执行每个提交的任务,通常使用 Executors 工厂方法配置。面试常问的知识点!!!
’
- 对于时间单位的参数, 用TimeUnit类的静态变量来表示
拒绝策略(面试重点)
- 我们ThreadPoolExecutor的第5个参数是一个阻塞队列, 用于存储管理任务. 当这个队列满了的时候, 此时就会触发最后一个参数, 拒绝策略.
- 有的人或许有疑问, 阻塞队列满了不应该等着吗. 我们上一节说确实是这样的, 但是在日常开发中, 我们死等的策略还是比较少见的. 我们日常开发要求尽量执行新任务, 所以下面的策略都是尽量让新任务执行
- 4个拒绝策略
- 这里总结一下我们ThreadPoolExcutor构造方法的几个参数
- 使用我们第一个拒绝策略来演示ThreadPoolExecutor构造方法
package thread;import java.util.concurrent.*;public class demo23 {public static void main(String[] args) {int corePoolSize = 10; //核心线程数int maximumPoolSize = 15; //最大线程数(核心+非核心)long keepAliveTime = 5; //非核心线程空闲时间// 使用Runnable作为队列元素类型,因为线程池执行的是Runnable任务BlockingQueue<Runnable> queue = new ArrayBlockingQueue<>(10);// 线程工厂,使用默认的ThreadFactory threadFactory = Executors.defaultThreadFactory();// 当线程池满时继续添加任务抛出异常RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();ThreadPoolExecutor pool = new ThreadPoolExecutor(corePoolSize,maximumPoolSize,keepAliveTime,TimeUnit.SECONDS, //生命周期时间单位queue,threadFactory, //添加线程工厂参数handler);for(int i = 0; i < 10; i++) {pool.submit(new Runnable() {@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + "线程执行");}}, i);//线程名}pool.shutdown();}
}
模拟实现线程池
- 我们从上面线程池的构造方法中知道线程池内部是用阻塞队列来存储和管理任务的, 所以我们使用阻塞队列来存储线程要执行的任务. 并且阻塞队列在多线程下是线程安全的(内置了锁)
- 创建一个线程后, 让这个线程while(true)执行取任务操作, 只要阻塞队列还有任务, 线程就不断取任务执行.
- submit方法往阻塞队列中方放任务
class MyThreadPool {private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(); //阻塞队列存储管理线程要执行的任务public MyThreadPool(int n ){for(int i = 0; i < n; i++) {Thread t = new Thread(() -> {try {while (true) { //线程一直往阻塞队列中取任务Runnable task = queue.take();task.run();}} catch (InterruptedException e) {throw new RuntimeException(e);}});t.setDaemon(true); //前台线程结束, 我们创建的后台线程也结束(这里把我们创建的线程设置为后台线程)t.start();}}public void submit(Runnable task) throws InterruptedException {queue.put(task); //往线程池中添加任}}
- 测试
public class demo32{public static void main(String[] args) throws InterruptedException {MyThreadPool myThreadPool = new MyThreadPool(10);for(int i = 0; i < 1000; i++){final int id = i;myThreadPool.submit(()->{Thread cur = Thread.currentThread();System.out.println(cur.getName() + "," + id);});}Thread.sleep(1000); //让主线程等一下线程池中10个线程把任务执行完, 主线程是前台线程, 如果主线程先结束了, 那么后台线程就也结束了}}