为什么要有线程池
线程的创建虽然比进程轻量, 但是在频繁创建情况下, 系统的开销是不可忽略的.
创建个线程池, 我们就可以从线程池拿线程, 这是纯粹的用户态操作.
如果从系统创建线程, 则涉及到用户态与内核态间的切换, 真正的创建是在内核态完成的.
而纯用户态操作时间是可控的, 涉及到内核态时间就不可控.
结论 : 线程池可以提高线程创建效率, 减少每次启动, 销毁线程的损耗.
标准库中的线程池
标准库中提供了现成的线程池, 可以通过下面代码来创键:
ExecutorService poll = Executors.newFixedThreadPool(10);
使用 Executors.newFixedThreadPool(10) 能创建出固定包含 10 个线程的线程池.
返回值类型为 ExecutorService.
通过 ExecutorService.submit 可以添加一个任务到线程池中.
为什么这里不是直接 new 对象, 而是通过调用 Executors 类的静态方法来创建对象.
这里就涉及到了工厂模式, 啥是工厂模式呢?
创建对象并非直接 new, 而是使用一些其他的方法 (通常是静态方法) 协助我们把对象创建出来.
工厂模式是用来填构造方法的坑的.
举个例子 :
在一个类里, 要想提供多种不同的构造对象的方式就得基于重载.
但重载规定参数列表必须不同, 这就出现了一个问题, 如果我们想通过相同的参数构造不同的对象呢?
比如这个类 :
为了解决上述问题, 我们可以构造一个工厂类 :
这便是工厂模式.
Executors 创建线程池的几种方式
1.newFixedThreadPool: 创建固定线程数的线程池
public class Test { public static void main(String[] args) { ExecutorService poll = Executors.newFixedThreadPool(10); poll.submit(new Runnable() { //将任务添加进线程池中 @Override public void run() { System.out.println("111"); } }); System.out.println("222"); } }
可以看到线程池执行完任务后并没有结束, 而是一直运行, 其实它里面内置了线程来执行任务, 是前台线程, 会阻止线程的结束.
2.newCachedThreadPool: 创建线程数目动态增长的线程池.
public class Test { public static void main(String[] args) { ExecutorService poll = Executors.newCachedThreadPool(); poll.submit(new Runnable() { @Override public void run() { System.out.println("111"); } }); System.out.println(222); } }
3.newSingleThreadExecutor: 创建只包含单个线程的线程池.
public class Test { public static void main(String[] args) { ExecutorService poll = Executors.newSingleThreadExecutor(); poll.submit(new Runnable() { @Override public void run() { System.out.println("111"); } }); System.out.println(222); } }
4.newScheduledThreadPool: 设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer.
public class Test { public static void main(String[] args) { ScheduledExecutorService poll = Executors.newScheduledThreadPool(1); poll.schedule(new Runnable() { @Override public void run() { System.out.println("111"); } },3, TimeUnit.SECONDS); //设置3S后执行 System.out.println(222); } }
线程池的简单模拟
class MyThreadPool { private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>(); public void submit(Runnable runnable) throws InterruptedException { queue.put(runnable); } //实现一个固定线程的线程池 public MyThreadPool(int n) throws InterruptedException { for(int i = 0; i < n; i++) { //循环n次, 创建n个线程 Thread t = new Thread(() -> { while(true) { //保证每个线程一直在取任务 try { Runnable runnable = queue.take(); runnable.run(); //执行任务 } catch (InterruptedException e) { e.printStackTrace(); } } }); t.start(); //启动线程 } } } public class ThreadDemo3 { public static void main(String[] args) throws InterruptedException { MyThreadPool myThreadPool = new MyThreadPool(10); for(int i = 0; i < 10000; i++) { int m = i; myThreadPool.submit(new Runnable() { @Override public void run() { System.out.println("打印数字: " + m); //注意这里不能为i } }); } } }
为什么打印数字要用 m 而不直接用 i 呢? 定义 m 变量是多此一举吗?
这里和 lambda 表达式有关, 这里虽然没有用到 lambda 表达式, 但是这里用到了匿名内部类, lambda 表达式本质上也是匿名内部类.
lambda 表达式中捕获的变量必须是 final 修饰的 或者是 “实际 final”(没有被final修饰, 但是代码中没有对该变量进行修改过)
这里 i 就是一直被修改的变量, 而 m 不一样, 我们每次进入循环都会再创建一个 m , 这样保证每次循环里的 m 都不一样, 每个 m 都不会改变, 这就满足了“实际 final”.
Executors 本质上是 ThreadPoolExecutor 类的封装, 也就是说 Executors 是一个工厂类.
ThreadPoolExecutor 的参数介绍
我们可以在 java.util.concurrent 下找到该类.
来看看它的构造方法:
要明白构造方法, 首先要明白它的参数意义, 我们就拿它参数最多的构造方法来说明 :
- corePoolSize : 核心线程数(最主要的线程, 不会被销毁)
- maximumPoolSize : 最大线程数(核心线程数 + 临时线程数)
如果当前任务比较多, 线程池就会多创建一些 “临时线程”, 当任务少了, 比较空闲了, 线程池就会把多出来的临时线程销毁掉.(核心线程不会动)
- keepAliveTime : 保持存活的最大时间.(当任务比较少时, 整体空闲下来的时候, 临时线程不会立刻被销毁, 而是会存活一段时间, 等待任务, 如果这段时间内还没有接到新任务, 那就会被销毁)
- BlockingQueue workQueue : 线程池要管理很多任务, 这些任务是通过阻塞队列来组织的, 我们可以手动指定的给线程池一个队列, 此时就可以很方便的控制 / 获取队列的信息了, submit方法就是将任务放到该队列中.
- ThreadFactory threadFactory : 工厂类, 就是创建线程的辅助类.
- RejectedExecutionHandler handler : 线程池的拒接策略.(如果线程池的池子满了, 继续往里添加任务, 如何拒绝)
线程池的执行流程
当添加新任务时, 首先进行判断, 判断核心线数是否为满, 如果没满, 就创建核心线程执行任务, 如果核心线程已满, 则判断任务队列是否有地方存放该任务, 如果有, 就将任务保存在任务队列中, 等待执行, 如果满了, 再判断最大可容纳的线程数, 如果没有超过这个数量, 就创建非核心线程执行任务, 如果超出, 就调用 handler 实现拒绝策略.
标准库中提供的四种拒绝策略(经典面试题)
ThreadPoolExecutor.AbortPolicy : 如果线程池满了, 继续添加任务,则会直接抛出异常.
ThreadPoolExecutor.CallerRunsPolicy : 添加的线程自己负责执行该任务.(哪个线程将该任务给它, 哪个线程就去负责执行)
ThreadPoolExecutor.DiscardOldestPolicy : 丢弃最老的任务.(运行时间最长的任务)
ThreadPoolExecutor.DiscardPolicy : 丢弃最新任务.(也就是要添加的任务)